Skip to content

Instance styles never reach a Web Component's root, so a sized instance renders at its stylesheet's size #370

Description

@nathanacurtis

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

  • An instance with declared sizing renders at that size in Web Components
  • React and Web Components render the same composed example at the same size
  • A component whose stylesheet declares an aspect ratio is sized by its caller, not by a measured pixel value
  • No change to how an instance without declared sizing renders

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.

Activity

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

Metadata

Metadata

Assignees

Labels

react-from-specsTransformer from specs into React code

Type

Fields

Priority

None yet

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions