Problem
A composed instance carries its own sizing from the spec. In React that sizing is applied and the instance renders at the size the spec declares. In Web Components it is not, and the instance renders at whatever size its own stylesheet pins — visibly larger or smaller than the design.
A card example places an image at 120x120. React renders 120x120. Web Components renders 120x336: the image keeps the height its stylesheet declares for its aspect ratio and overflows the space it was given.
Two things combine:
- A generated custom element's host is
display: contents, so it has no box and cannot be styled from outside. Composition works around this by wrapping the instance in a div carrying the styles, but that wrapper sits outside the shadow root, so the root element inside is unaffected and simply overflows it.
- The component's own stylesheet pins a measured pixel height beside its aspect ratio, and the measured height wins. React only escapes this because its scaffold merges the instance style onto the same element that carries the block class, where an inline style overrides the rule.
So the two surfaces disagree for the same spec, and the Web Components output is wrong.
Solution
A composed instance renders at the size the spec declares, on both surfaces.
Acceptance criteria
Problems
- A custom element host is
display: contents deliberately, so it takes no styling from outside; changing that affects layout participation for every generated component
- The wrapper the composer emits is outside the shadow root and cannot reach the root element inside it
- Component stylesheets emit a measured pixel dimension alongside an aspect ratio, which overrides the ratio
- Whatever is chosen has to hold for arbitrary instance styles, not just width and height
Impacted code
packages/webcomponents-from-specs/src/Spec/composition.ts — emits the wrapper div carrying instance styles
packages/webcomponents-from-specs/src/WebComponents/ — scaffold generation, where :host { display: contents } is set
packages/cli/src/transforms/css/ — emits the measured dimension alongside the ratio
Decision
The custom element is the box, and it carries the component's own styles.
A generated component currently renders two boxes: the custom element, set to
display: contents so it has no box at all, and an inner root div that carries every
style. Nothing outside can size the component, because the only element reachable from
outside is the one deliberately given no box.
The custom element becomes the real box instead, styled by the rules that target the
root today. A caller then sizes it as it would any element — <x-image style="height: 120px"> — with no wrapper, no custom properties, and no vocabulary to maintain. An
inline style beats the component's own rule by specificity, which is exactly why React
already renders correctly.
This is how component libraries normally do it; display: contents on a host reads as a
deliberate "this component is layout-transparent" choice, not a default, and applying it
to every component is the unusual part of the current output.
Shadow DOM and slots are unaffected. Slots stay first class in the Web Components
transform — that is not in question here, and light DOM was rejected for that reason.
What it requires
The root's styles are class-keyed in the stylesheet the css transform emits, so the
change spans two repos:
specs — the css transform emits the root rule against the custom element itself
for the Web Components target, rather than only as a class
specs-from-figma — scaffolds stop emitting display: contents; composition stops
emitting the wrapper div and applies instance styles to the custom element
Rejected along the way:
- Custom properties for sizing — a fixed vocabulary that grows with every property,
and this is not only about width and height
- Rewriting the emitted stylesheet's selectors at scaffold time — string surgery on
generated CSS
- Per-instance rules reaching inside the shadow tree (
::part and similar) — ::part
is a consumer theming surface, not a channel for sizing one composed example; a :host
rule cannot be authored from outside the component at all
- Light DOM — would give exact parity with React, but discards slots
Related: composed example markup now applies dimensions on a FIXED axis, which is what made this divergence visible.
Implementation details are tracked internally.
Problem
A composed instance carries its own sizing from the spec. In React that sizing is applied and the instance renders at the size the spec declares. In Web Components it is not, and the instance renders at whatever size its own stylesheet pins — visibly larger or smaller than the design.
A card example places an image at 120x120. React renders 120x120. Web Components renders 120x336: the image keeps the height its stylesheet declares for its aspect ratio and overflows the space it was given.
Two things combine:
display: contents, so it has no box and cannot be styled from outside. Composition works around this by wrapping the instance in a div carrying the styles, but that wrapper sits outside the shadow root, so the root element inside is unaffected and simply overflows it.So the two surfaces disagree for the same spec, and the Web Components output is wrong.
Solution
A composed instance renders at the size the spec declares, on both surfaces.
Acceptance criteria
Problems
display: contentsdeliberately, so it takes no styling from outside; changing that affects layout participation for every generated componentImpacted code
packages/webcomponents-from-specs/src/Spec/composition.ts— emits the wrapper div carrying instance stylespackages/webcomponents-from-specs/src/WebComponents/— scaffold generation, where:host { display: contents }is setpackages/cli/src/transforms/css/— emits the measured dimension alongside the ratioDecision
The custom element is the box, and it carries the component's own styles.
A generated component currently renders two boxes: the custom element, set to
display: contentsso it has no box at all, and an inner root div that carries everystyle. Nothing outside can size the component, because the only element reachable from
outside is the one deliberately given no box.
The custom element becomes the real box instead, styled by the rules that target the
root today. A caller then sizes it as it would any element —
<x-image style="height: 120px">— with no wrapper, no custom properties, and no vocabulary to maintain. Aninline style beats the component's own rule by specificity, which is exactly why React
already renders correctly.
This is how component libraries normally do it;
display: contentson a host reads as adeliberate "this component is layout-transparent" choice, not a default, and applying it
to every component is the unusual part of the current output.
Shadow DOM and slots are unaffected. Slots stay first class in the Web Components
transform — that is not in question here, and light DOM was rejected for that reason.
What it requires
The root's styles are class-keyed in the stylesheet the css transform emits, so the
change spans two repos:
specs— the css transform emits the root rule against the custom element itselffor the Web Components target, rather than only as a class
specs-from-figma— scaffolds stop emittingdisplay: contents; composition stopsemitting the wrapper div and applies instance styles to the custom element
Rejected along the way:
and this is not only about width and height
generated CSS
::partand similar) —::partis a consumer theming surface, not a channel for sizing one composed example; a
:hostrule cannot be authored from outside the component at all
Related: composed example markup now applies dimensions on a FIXED axis, which is what made this divergence visible.