Summary#
A control reads the app's theme for colour, radius and type, so it looks like the rest of the app without being told to. What a theme cannot say is anything about that control's own geometry — how much air its menu has, how wide its popover opens, how heavy its shadow sits. Those are the control's business, and historically they were literals inside it: an app could recolour the thing and not make it denser.
A styles { } block is the control's answer. It declares each knob with a kind, a name and a default; the app
overrides the ones it cares about and ignores the rest. Nothing changes unless the app says so.
The block is optional, and a control that declares none is unaffected in every direction.
Signature#
control <Name> {
styles {
<kind> <KnobName> = <default>;
…
}
}<kind> is one of length · color · shadow · number · time · text.
Description#
Declaring a control's style knobs#
control DataGrid {
contractVersion "1.1"
participation headless
props { string title; }
styles {
/// How tall one row is.
length RowHeight = "2.25rem";
color HeaderBg = Colors.Muted;
}
}The kind is the value's SHAPE, not a CSS property. length RowHeight says the knob holds a length; it does not
say what the control does with it. The platform deliberately never learns that a row has a height — it learns only
enough to check that an override is the right kind of thing. A control is free to use one knob in five places.
A default is required. A knob with no default is a hole the control has to write a fallback around, which is the hand-maintained chain this block exists to remove.
A default may reference a theme token, and usually should:
color HeaderBg = Colors.Muted;That does not freeze the token's value at compile time — it stays a reference. So the control follows the app's theme, dark mode included, until the app overrides the knob specifically. This is how a control ships a considered look without shipping a look of its own.
Reading them in the shim#
The generated .d.ts gives the control a typed scope keyed by its own declared names:
export interface DataGridHost {
tokens: TokenScope; // the APP's design language
styles: StyleScope<"RowHeight" | "HeaderBg">; // THIS control's knobs
}so the shim asks for a knob by the name it declared:
el.style.height = host.styles.cssVar('RowHeight'); // "var(--control-datagrid-rowheight, 2.25rem)"The union is the point: a misspelled knob is a compile error in the shim, where a misspelled CSS custom property would simply resolve to nothing and paint as though the value were absent.
Use cssVar in a style so an override stays live without a re-mount. Use get only for a decision in
JavaScript — it reads the value now, and a value read once does not follow a later change.
An opaque control cannot declare a styles { } block at all. An island that paints its own way is handed no
style scope, so its knobs would have no reader — while still looking, from the app's side, like a surface it could
theme. Declaring one is a compile error rather than a block that quietly does nothing.
Overriding, app-wide#
A theme's Control block sets a knob for every instance in the app:
control DataGrid {
contractVersion "1.1"
participation headless
props { string title; }
styles { length RowHeight = "2.25rem"; }
}
theme Doc {
Control {
DataGrid { RowHeight = "3rem"; }
}
}Both names are checked against the declaration. A control the app does not have, or a knob that control never declared, is a compile error that lists what it does declare — not a value that quietly applies to nothing.
Overriding, per call site#
This is the tier a theme cannot express: two instances of the same control, on the same page, styled differently — a dense grid in a sidebar and a roomy one in the main column.
control DataGrid {
contractVersion "1.1"
participation headless
props { string title; }
styles {
length RowHeight = "2.25rem";
color HeaderBg = Colors.Muted;
}
}
theme Doc { Colors { Muted = "#F0EEE8"; Surface = "#FFFFFF"; } }
[Page("/orders")] [AllowAnonymous]
component OrdersPage() {
render {
Stack {
DataGrid(title: "Orders", styles: new() { RowHeight = "3rem", HeaderBg = Colors.Surface });
DataGrid(title: "Recent");
}
}
}The second call is untouched — an override applies to the instance that wrote it, and a knob it says nothing about keeps whatever the tiers below give it. An override is not a reset.
Names are checked here exactly as they are in a theme: a knob this control never declared is a compile error listing the ones it does declare.
What a style value may be#
A literal or a theme token, in every one of the three places a knob's value is written — the control's default, the theme override, and the call site:
styles: new() { RowHeight = "3rem", HeaderBg = Colors.Surface }A token stays a reference, so an overridden knob still follows the app's theme and its dark mode.
Nothing else, and that is deliberate rather than a gap. A style value is written into CSS once — there is no channel that re-evaluates it while the page is mounted — so an expression that could change would silently paint whatever it happened to be. An app that wants a value to vary declares a theme token and references it; the cascade already follows that.
Which override wins — precedence#
Ordinary CSS cascade, in the ordinary direction:
| the control's default | the var() fallback — lowest |
| the app's theme override | a declaration in the app's stylesheet |
| the call-site override | inline on the instance — highest |
Nothing arbitrates this and nothing asks "did the app override it?". The shim asks for the variable and paints with whatever came back.
Choosing between a style, a prop and a token#
| Use | When |
|---|---|
| a theme token | the value is the app's design language and every control should read it — a brand colour, a radius scale |
a styles knob | the value is this control's own geometry, and an app might reasonably want it different |
| a prop | it changes BEHAVIOUR, or it is a small closed set of choices the call site makes (density, readOnly) |
The test that separates the last two: if changing it would change what the control DOES, it is a prop. If it only changes how the same thing looks, it is a style.
Examples#
A control whose knobs default to the app's theme, with the app overriding one of them:
control Callout {
contractVersion "1.1"
participation headless
props { string text; }
styles {
length Pad = "0.75rem";
color Surface = Colors.Muted;
color Accent = Colors.Primary;
}
}
theme Doc {
Colors { Primary = "#4F46E5"; Muted = "#F0EEE8"; }
Control {
Callout { Pad = "1.25rem"; }
}
}Surface and Accent keep following the theme; only Pad is claimed by the app.
See also#
- control — foreign UI controls (charts, grids, maps) — the
controlblock these are declared in - theme tokens — the app's own design tokens, which a default may reference
- style props — how style values are written elsewhere in the UI surface