Typed CSS variables and design tokens
CSS custom properties are the public styling API of a Craft component. Craft extracts a contract from inline meta.styles, propagates unsatisfied variables through component templates, and applies supplied values to the component root. The browser's native inheritance then carries them to descendants.
Required and optional variables
An unguarded use is required. A declaration or inline fallback is optional:
import { article, craftComponent } from '@craft-ng/component';
const Card = craftComponent(
'Card',
{
styles: `
:scope { --card-radius: 1rem; }
.card {
color: var(--card-ink);
background: var(--card-bg, white);
border-radius: var(--card-radius);
}
`,
},
() => ({}),
() => article({ class: 'card' }, 'Card'),
);
Card({ cssVars: { '--card-ink': 'navy' } });--card-ink is required, while --card-bg and --card-radius are optional. Styles supplied through cssVars are written as custom properties on the component root; different instances can therefore use different values while sharing one scoped stylesheet.
External stylesheets
An imported stylesheet is typed as string, so TypeScript cannot inspect it. Declare its contract explicitly with required():
craftComponent(
'ExternalCard',
{
stylesUrl: styles,
cssVars: {
'--external-card-ink': required<string>(),
'--external-card-gap': '1rem',
},
},
() => ({}),
template,
);The craft-css-vars-contract lint rule resolves the CSS import and checks that the explicit contract and file remain synchronized.
Child-variable dispositions
At a child call site, every variable can be handled deliberately:
Badge({ cssVars: { '--badge-ink': 'navy' } });
Badge({ cssVars: { '--badge-ink': inherit } });
Badge({ cssVars: { '--badge-ink': omit } });
Badge({ cssVars: { '--badge-ink': forward('navy') } });
Badge({ cssVars: { '--badge-bg': forward() } });- A value supplies the child directly.
inherituses a declaration in the current component's own styles and emits no inline value.omitintentionally stops propagation and emits nothing.forward(value)gives the parent API a default that callers can override.forward()re-exposes an optional value without adding a default.
Use assertCssVarsSatisfied(routes) next to the other route proofs. It rejects a routed root when a required variable has propagated all the way to a mount that has no component call site.
@property: validation versus requiredness
Craft reads authored @property blocks; it does not generate them. A registered property with a non-wildcard syntax needs an initial-value, so it is optional by construction:
@property --meter-value {
syntax: '<number>';
inherits: true;
initial-value: 0;
}Registration provides browser validation, animation support, and an initial value. The tradeoff is that it gives up the compile-time “nobody supplied this” error. inherits: false cannot be used for a variable supplied or forwarded by a parent.
@property is document-global even when its values cascade normally. A component may therefore register only variables in its own namespace (Meter → --meter-*). Register shared design tokens once in the application's global stylesheet, whose lifetime matches the document.
Scope safety
Craft rejects component CSS that can silently become global:
@import,:root,html, andbody;- unprefixed
@keyframes,@counter-style, font palettes, or font families; @propertyregistrations outside the component namespace;!importantin component styles.
Private global names use the exact component scope, for example @keyframes Spinner-spin. Craft validates these names rather than rewriting CSS declaration values at runtime.