Skip to content

fix(ssr): forward the slot prop to the component wrapper - #863

Open
muntalee wants to merge 2 commits into
stenciljs:mainfrom
muntalee:fix/ssr-forward-slot-to-wrapper
Open

muntalee wants to merge 2 commits into
stenciljs:mainfrom
muntalee:fix/ssr-forward-slot-to-wrapper

Conversation

@muntalee

@muntalee muntalee commented Oct 21, 2026 •

Copy link
Copy Markdown

Pull request checklist

  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been reviewed and added / updated if needed (for bug fixes / features)
  • Build (npm run build) was run locally for affected output targets
  • Tests (npm test) were run locally and passed
  • Prettier (npm run prettier) was run locally and passed

Pull request type

  • Bugfix

What is the current behavior?

@stencil/ssr renders every component inside a <div style="display: contents"> wrapper and only spreads the component's props onto the inner element. A browser only assigns the direct children of a shadow host to a slot, so a Stencil component passed to a named slot of another component is assigned to the default slot through its wrapper, and the named slot stays empty. Plain elements are not wrapped, so they work.

Issue URL: #858

Fixes #858

What is the new behavior?

  • The slot prop is also set on the wrapper element, in all three wrapper templates (serializeScopedComponent and the two in serializeShadowComponent). It is kept on the component as well, as other code may look it up there.
  • Adds a my-card example component with named slots and a named-slot-shadow scenario to the shared React test scenarios. It passes a scoped component, a declarative shadow DOM component and a plain element to the slots of my-card, and asserts where the browser assigns them, in the server rendered HTML and after hydration.
  • Adds unit tests that check that every generated wrapper forwards slot, for scoped and declarative-shadow-dom with both strategies. The scoped snapshot is updated: 8 lines, each only gains , slot: props.slot.

Does this introduce a breaking change?

  • Yes
  • No

Other information

The first commit adds the scenario and fails in next-app on purpose, the second commit fixes it.

Results of the new scenario, without and with the fix:

Example project Without the fix With the fix
next-app (Next 14, compiler) all 4 tests fail pass
react-app (Vite) the server HTML test fails pass
remix-app the server HTML test fails pass
next-15-runtime-based pass (does not use the transform) pass

On Vite and Remix only the server rendered HTML is wrong, because a component serialized as scoped is wrapped in both strategies and the client render does not add the wrapper. On Next the wrapper is also rendered by the client, so it stays wrong after hydration.

I also checked the repro from the issue with the same change applied to its installed @stencil/ssr@0.4.0. start goes from span to div, span (the div wraps example-input), and the default slot goes from div, p to p.

I ran the unit tests of all output targets and the e2e suites of next-app, next-15-runtime-based, remix-app and react-app. I did not run the Vue, Nuxt and Angular e2e suites.

The key props in the scenario are there because React warns about missing keys for the children passed to the generated wrapper, which fails the shared "no errors were logged" check. That warning is unrelated to this change and also happens without it.

Adds a `my-card` example component with named slots and a `named-slot-shadow` scenario to the
shared React test scenarios. It passes a scoped and a declarative shadow DOM Stencil component,
and a plain element, to the slots of `my-card` and asserts where the browser assigns them, both in
the server rendered markup and after hydration.

The scenario fails in the Next.js example project, which uses the `@stencil/ssr` compiler, until
the follow up commit forwards the `slot` prop to the component wrapper.
The transform renders every component inside a `<div style="display: contents">` and only spreads
the props onto the component itself. A browser only assigns the direct children of a shadow host
to a slot, so a component passed to a named slot of another component was assigned to the default
slot through its wrapper, and the named slot stayed empty.

Set the `slot` prop on the wrapper as well, in all three wrapper templates. It stays on the
component, as other code may look it up there.

Fixes stenciljs#858
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: @stencil/ssr Next.js strategy wraps components in display:contents div, breaking named slot assignment

1 participant