# styles — a control's own look knobs

> 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.

<!-- id: ui-control-styles · area: ui · stability: stable · html: https://osysharp.com/reference/ui/control-styles/ -->

## Summary        {#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      {#signature}
```osy syntax
control <Name> {
  styles {
    <kind> <KnobName> = <default>;
    …
  }
}
```

`<kind>` is one of `length` · `color` · `shadow` · `number` · `time` · `text`.

## Description    {#description}

### Declaring a control's style knobs   {#declaring}

```osy title="a grid that lets an app set its row height" test app=ui-control-styles
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:

```osy title="a default that stays a reference to the app's theme token" syntax
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    {#reading}

The generated `.d.ts` gives the control a typed scope keyed by its own declared names:

```ts
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:

```ts
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    {#overriding-app-wide}

A theme's `Control` block sets a knob for every instance in the app:

```osy title="the app decides its grid is roomier" test app=ui-control-styles-theme
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    {#overriding-per-call}

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.

```osy title="one grid roomy, one left alone" test app=ui-control-styles-callsite
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    {#style-values}

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:

```osy syntax
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   {#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    {#choosing}

| 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       {#examples}

A control whose knobs default to the app's theme, with the app overriding one of them:

```osy title="inherits the theme, then one knob is claimed" test app=ui-control-styles-full
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       {#see-also}
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — the `control` block these are declared in
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the app's own design tokens, which a default may reference
- [style props](https://osysharp.com/reference/ui/styling/) — how style values are written elsewhere in the UI surface
