webcomponents
Emits the Web Components target: a Lit custom element for every spec, its contract, its element API, its stylesheets, and its Storybook stories — into webcomponents/, a directory shaped like the package it could become.
The peer of react, and deliberately the same shape. What differs is only what genuinely differs between the platforms.
Usage
specs webcomponents [options]No arguments. The target is the command.
What it writes
Two trees: its own, and the shared assets every target uses.
webcomponents/├── src/│ ├── _runtime.ts shared helpers, one per tree│ └── components/│ └── ActionList/│ ├── contract.ts props, defaults, enums│ ├── metadata.ts slot shapes and rules, when it has slots│ ├── api.ts tag name, parts, element-tag map│ ├── scaffold.ts the custom element│ ├── host.css the host's own styling│ ├── light.css light-DOM styling for slotted content│ ├── stories.ts Storybook CSF│ └── Item/ a subcomponent, nested as its spec is│ ├── contract.ts│ ├── metadata.ts│ ├── scaffold.ts│ ├── host.css│ ├── light.css│ └── stories.ts│ └── compositions/│ └── CheckoutSmall/ a composition, emitted as a component is│ ├── contract.ts│ ├── api.ts│ ├── scaffold.ts│ ├── host.css│ ├── light.css│ └── stories.ts└── …
assets/└── cssvars/ ├── cssvars.css └── modes.jsonA subcomponent is emitted as completely as a component, minus the element API: its tag is namespaced by its parent, so the parent’s api.ts carries it. The directory is already named for the component, so the files inside it do not repeat the name.
assets/cssvars/ holds the same platform-neutral custom properties react writes. Whichever target runs produces it; running both is not a conflict.
Storybook glue lands in storybook/lib/, not here. A Lit element renders inside a React Storybook through a small React host component — which belongs to the Storybook project, because a Lit component library has no business importing React for a harness it never uses at runtime.
Each file in detail
| File | What it carries |
|---|---|
contract.ts | The props interface, the enums those props draw from, and a defaults constant — byte-for-byte what react emits |
metadata.ts | Slot shapes and the visibility rule governing each — emitted only when the component has slots |
api.ts | The custom-element surface: tag name, the ::part() names, and the HTMLElementTagNameMap entry |
scaffold.ts | The Lit element: reflected properties, part-annotated markup from the merged layout tree, conditional slots |
host.css and light.css | The element’s own styling, and the styling for content a consumer slots in |
stories.ts | Storybook CSF: controls typed from the contract, a story per variant axis, a sticker sheet |
cssvars.css | The library’s custom properties — what every var() above resolves against |
Every one of them opens with a Generated. Do not edit header and is overwritten on each run. The seam for code you own is a sibling component.ts, which no command writes and the stories import instead of the scaffold when it exists. What gets emitted covers all of it.
Two stylesheets, not one
host.css styles the custom element itself. light.css styles content a consumer slots into it, which the shadow root cannot reach.
The split is not a preference. A shadow boundary means one stylesheet cannot do both jobs, and a single file would silently fail on whichever side it was not written for.
Where this differs from React
The custom element is the component’s root. React can replace a root’s tag when a role calls for a native control; this target cannot, because the root is already the element the consumer wrote.
So a root control role emits its semantic element inside the shadow root, wrapping the root’s content, and the shadow root delegates focus to it. A button role produces a real <button> there rather than ARIA on the host — role="button" buys the announcement and nothing else, while a native element supplies keyboard activation, :disabled blocking events, :focus-visible, and form participation.
The inner element is styled to nothing and takes no box, so the host keeps the root’s layout and appearance exactly as the stylesheet describes them.
A state the role expresses natively lives on that inner element, out of reach of any :host() selector — and a host is not a form control, so :host(:disabled) cannot match either. The host therefore carries a plain styling attribute for the concept, so the stylesheet still has something to select. See precedence.
Options
Identical to react:
--components <keys...>
Only emit the named component folders. Unknown keys warn and are skipped. Scoped runs skip library-level output.
--no-stories
Emit the element, its contract, its API and its stylesheets, but no Storybook stories.
--watch
Watch the specs directory and re-emit on every change. Debounced, non-overlapping, and a failed component is logged rather than fatal. Same behaviour as react --watch, including re-emitting the whole set on every change.
specs webcomponents --watch-o, --output <path>
The specs directory to read from. Defaults to spec.directory in config/settings.yaml.
--config <path>
Use a specific config/ directory.
--verbose
Report each component as it is emitted.
What it reads
| From | For |
|---|---|
specs/components/<component>/api.yaml | props, anatomy, roles, actions, subcomponents |
specs/components/<component>/variants.yaml | the layout tree and per-variant styling |
specs/components/<component>/examples.yaml | instance and slot-content examples, for stories |
config/conventions/web-components.yaml | this platform’s conventions |
config/conventions/specs.yaml | state classification, accessibility and value props |
A component with no variants.yaml is skipped with a warning rather than emitted empty.
Compositions
A composition is a spec whose Figma source is a frame — a screen or a pattern that arranges components rather than a component itself. specs/compositions/<key>/ emits to webcomponents/src/compositions/<Name>/, mirroring the spec layout, and the components it composes are imported across that boundary from ../../components/<Name>.
A composition presents as one story, in its own nav group:
Web Components/Compositions/Checkout/Smallso screens browse separately from the component list. A frame declares no variant properties, so there is one Default story and no sticker sheet.
Compositions are Pro. On the free tier they are skipped — the run says so once and emits the components as normal — rather than emitted as a frame with its contents stripped out, which would be a styled empty box named after a screen.
Free and Pro
The same seams as react: the element, its contract, its API, its stylesheets and its stories are free; composition, glyphs, background images, compound stories, sticker sheets, roles and actions are licensed.
See Also
- react — the peer target
- What gets emitted — each artifact in detail
- Roles — and precedence for how a shadow host carries state