Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

styles — a control's own look knobs

control X { styles { length RowHeight = "2.25rem"; color HeaderBg = Colors.Muted; } }

A `styles { }` block declares the look values a control owns — its paddings, widths, shadows — as named knobs an app can override. It is how a control exposes its geometry without turning every request into another prop, and without an app having to guess at CSS variables the control never promised.

stable4 examples compiled by CIuicontrolsthemeauthoring

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 defaultthe var() fallback — lowest
the app's theme overridea declaration in the app's stylesheet
the call-site overrideinline 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#

UseWhen
a theme tokenthe value is the app's design language and every control should read it — a brand colour, a radius scale
a styles knobthe value is this control's own geometry, and an app might reasonably want it different
a propit 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#

Related

control — foreign UI controls (charts, grids, maps)

A `control` block declares the contract of a foreign UI widget — a chart, a data grid, a map — that a small JavaScript…

theme tokens

A `theme` block names your app's design tokens — colors, spacing, radii, and more — as reusable values. A token can…

style props

Inside a `variants` block, each `Name = value` is a style prop from a fixed vocabulary the renderer maps to CSS — paint…