# Wolkit style (.wolcss)

> A small style language whose values are theme tokens, compiled ahead of time and checked for mistakes.

Source: https://wolos.org/reference/wolkit-style

## What it is

`.wolcss` is the style language of Wolkit apps: selectors and properties like a web style sheet, but every colour, size and curve is a theme token, not a literal value.

## Why

A literal colour breaks dark mode, high contrast and the accent colour. With tokens only, an app follows every theme with no code, and the compiler can catch a mistake before the app runs.

## How

Write classes in `src/styles/app.wolcss`, run `make styles`, then load `build/styles.wsb` with `Wolkit.use_styles`. The properties, lints and errors are listed below; this reference is generated from the engine's own code.

## Related

[Wolkit reference](/reference/wolkit) · [Your first Wolkit app](/reference/wolkit-tutorial)

## Reference

The style language of libWolkit: a small CSS-like subset whose values are theme tokens, not literals. Two authoring tiers meet in one binary table (`WSB1`): inline arguments checked by the shard's macros, and `src/styles/*.wolcss` sheets compiled by `wolkit-stylec` with the same parser the engine's dev loader uses.

```css
.card        { background: card; padding: md; border-radius: m; box-shadow: sm; }
.card:hover  { background: control; }
.row:hover > .actions { opacity: 1; }
@dark        { .card { box-shadow: md; } }
button       { cursor: pointer; transition: background fast ease; }
```

### Selectors and the order of resolution

- `.class`, a node kind (`button`), at most one state (`.card:hover`), and the single form `.parent:state > .child` for hover-reveal actions. Selector lists with commas are fine.
- States: `:hover`, `:active`, `:focus`, `:focus-visible`, `:disabled`, `:selected`, `:checked`, `:expanded`, `:empty`.
- Media wrap rules: `@dark { ... }`; several conditions combine with a space (`@dark @contrast { ... }`). Conditions: `@dark`, `@light`, `@contrast`, `@reduce-motion`, `@reduce-transparency`, `@compact`.
- Node kinds: `box`, `text`, `image`, `spacer`, `divider`, `button`, `toggle`, `slider`, `text-input`, `scroll`, `vlist`, `canvas`, `surface`, `overlay`.
- There is no specificity and there is no `!important` (RU-056). Resolution order is fixed: the rules of the node kind, then the classes in the order the node lists them (later wins), then state and media variants (a variant with more conditions comes later; equals keep the order written), then inline properties.
- A class holds at most 8 variants; a table holds at most 4096 classes and 4 MiB.
- Inherited properties (marked below) are copied from the parent when no rule sets them. Results are interned, so two nodes with the same computed style share one entry.
- The pragma `/* wolkit: off-scale */` lifts the spacing and radius scale for a file (WK-005, WK-006). It is counted by a ratchet and reported as WK-020.

### Tokens, not literals

Colours are the 13 theme tokens of `shared::theme::TOKENS`, in this order (ids are the position). `alpha(token, 20%)` makes a see-through one and `transparent` is clear. A hex literal is WK-001, `rgb()` and the like WK-023 (RU-062).

| Id | Token | Under `@contrast` |
|---|---|---|
| 0 | `surface` | unchanged |
| 1 | `sidebar` | unchanged |
| 2 | `card` | unchanged |
| 3 | `line` | `text` at 55% |
| 4 | `text` | unchanged |
| 5 | `dim` | `text` |
| 6 | `accent` | unchanged |
| 7 | `accentText` | unchanged |
| 8 | `glass` | `surface` |
| 9 | `control` | unchanged |
| 10 | `success` | unchanged |
| 11 | `warning` | unchanged |
| 12 | `danger` | unchanged |

The scales mirror `shared::theme::SCALE` (a test holds them to it):

| Scale | Names and values | Theme variable |
|---|---|---|
| Spacing (padding, margin, gap) | `none` 0px, `xs` 4px, `sm` 8px, `md` 12px, `lg` 16px, `xl` 24px | `--space-1`, `--space-2`, `--space-3`, `--space-4`, `--space-6` |
| Radius | `none` 0px, `s` 6px, `m` 10px, `l` 14px, `xl` 20px, `full` pill | `--radius-s`, `--radius-m`, `--radius-l`, `--radius-xl` |
| Motion (transition) | `fast` 150ms, `normal` 250ms, `slow` 400ms | `--motion-fast`, `--motion-normal`, `--motion-slow` |
| Eases | `ease` (the theme's `--ease`, cubic-bezier(0.2, 0.8, 0.2, 1)), `linear`, `in`, `out`, `in-out` | `--ease` |

A pixel value off the spacing or radius scale is WK-005 or WK-006; a font below 11px is WK-003 (RU-059); a transition over 500 ms is WK-004 (RU-060).

Type roles (`font-size: title`; sizes mirror `shared::theme::scaffold`):

| Role | Size | Usual weight |
|---|---|---|
| `display` | 34px | 700 |
| `large` | 26px | 600 |
| `title` | 17px | 600 |
| `body` | 13px | 400 |
| `secondary` | 11px | 400 |
| `caption` | 11px | 400 |

Shadows are fixed material (black at an alpha), not themable. `box-shadow` holds two layers at most; `lg` is two.

| Shadow | Layers (offset y, blur, alpha) |
|---|---|
| `none` | none |
| `sm` | 1px 3px 16% |
| `md` | 3px 10px 20% |
| `lg` | 6px 18px 24%; 1px 4px 16% |

Springs (`spring: snappy`): stiffness, damping, mass.

| Spring | Stiffness | Damping | Mass |
|---|---|---|---|
| `gentle` | 170 | 26 | 1 |
| `snappy` | 320 | 30 | 1 |
| `bouncy` | 260 | 14 | 1 |

### Properties

`Affects` is what a change dirties: `layout` or `paint` (a paint change never dirties layout). `Anim` marks the properties a `transition` can run on. Ids are append-only (a property's id is part of `WSB1`).

#### Layout

| Id | Property | Values | Inherited | Affects | Anim | Notes |
|---|---|---|---|---|---|---|
| 0 | `display` | flex \| none | no | layout |  | `none` removes the node from layout and paint. |
| 1 | `flex-direction` | row \| column \| row-reverse \| column-reverse | no | layout |  | Main axis of the node's children. |
| 2 | `flex-wrap` | nowrap \| wrap | no | layout |  | Whether children break onto more lines. |
| 3 | `grow` | <number> | no | layout |  | Share of the free main-axis space. |
| 4 | `shrink` | <number> | no | layout |  | Share of the overflow taken back; ignored in `scroll` and `auto` containers. |
| 5 | `basis` | auto \| <length> \| <percent> | no | layout |  | Starting main size before grow and shrink. |
| 6 | `row-gap` | <space> | no | layout |  | Space between lines (and between children of a column). |
| 7 | `column-gap` | <space> | no | layout |  | Space between children of a row. |
| 8 | `align-items` | stretch \| start \| center \| end \| baseline | no | layout |  | Cross-axis placement of children; `baseline` behaves as `start` in 1.0. |
| 9 | `align-self` | auto \| stretch \| start \| center \| end \| baseline | no | layout |  | Overrides the parent's `align-items` for this node. |
| 10 | `align-content` | stretch \| start \| center \| end \| space-between \| space-around | no | layout |  | Placement of lines when wrapping. |
| 11 | `justify-content` | start \| center \| end \| space-between \| space-around \| space-evenly | no | layout |  | Main-axis placement of children. |
| 12 | `padding-top` | <space> | no | layout |  | Inner space, top. |
| 13 | `padding-right` | <space> | no | layout |  | Inner space, right. |
| 14 | `padding-bottom` | <space> | no | layout |  | Inner space, bottom. |
| 15 | `padding-left` | <space> | no | layout |  | Inner space, left. |
| 16 | `margin-top` | auto \| <space> | no | layout |  | Outer space, top. |
| 17 | `margin-right` | auto \| <space> | no | layout |  | Outer space, right. |
| 18 | `margin-bottom` | auto \| <space> | no | layout |  | Outer space, bottom. |
| 19 | `margin-left` | auto \| <space> | no | layout |  | Outer space, left. |
| 20 | `width` | auto \| <length> \| <percent> | no | layout |  | Border-box width. |
| 21 | `height` | auto \| <length> \| <percent> | no | layout |  | Border-box height. |
| 22 | `min-width` | auto \| <length> \| <percent> | no | layout |  | Lower bound of the width; `auto` is 0. |
| 23 | `min-height` | auto \| <length> \| <percent> | no | layout |  | Lower bound of the height; `auto` is 0. |
| 24 | `max-width` | none \| <length> \| <percent> | no | layout |  | Upper bound of the width. |
| 25 | `max-height` | none \| <length> \| <percent> | no | layout |  | Upper bound of the height. |
| 26 | `aspect-ratio` | auto \| <number> \| <number> / <number> | no | layout |  | Width over height, used when one side is open. |
| 27 | `position` | relative \| absolute | no | layout |  | `absolute` leaves the flow and is placed by its insets in the parent's box. |
| 28 | `top` | auto \| <length> \| <percent> | no | layout |  | Offset from the top edge of an absolute node. |
| 29 | `right` | auto \| <length> \| <percent> | no | layout |  | Offset from the right edge of an absolute node. |
| 30 | `bottom` | auto \| <length> \| <percent> | no | layout |  | Offset from the bottom edge of an absolute node. |
| 31 | `left` | auto \| <length> \| <percent> | no | layout |  | Offset from the left edge of an absolute node. |
| 32 | `overflow` | visible \| hidden \| scroll \| auto | no | layout |  | Clipping and scrolling of children. |
| 33 | `z-index` | <integer> | no | paint |  | Paint order among siblings. |

#### Paint

| Id | Property | Values | Inherited | Affects | Anim | Notes |
|---|---|---|---|---|---|---|
| 34 | `border-width` | <pixels> | no | layout |  | Border width; counts as padding for layout. |
| 35 | `background` | none \| <color> \| linear-gradient(<angle>, <color>, <color> [, <color>]) | no | paint | yes | Fill; a gradient has two or three evenly spaced stops. |
| 36 | `border-color` | <color> | no | paint | yes | Border colour. |
| 37 | `border-top-left-radius` | <radius> | no | paint | yes | Corner radius, top left. |
| 38 | `border-top-right-radius` | <radius> | no | paint | yes | Corner radius, top right. |
| 39 | `border-bottom-right-radius` | <radius> | no | paint | yes | Corner radius, bottom right. |
| 40 | `border-bottom-left-radius` | <radius> | no | paint | yes | Corner radius, bottom left. |
| 41 | `box-shadow` | none \| <shadow> [, <shadow>] | no | paint | yes | Elevation: `sm`, `md` or `lg`; two layers at most (`lg` is two). |
| 42 | `opacity` | <number> \| <percent> | no | paint | yes | Group opacity. |
| 43 | `outline-width` | <pixels> | no | paint | yes | Focus ring width. |
| 44 | `outline-color` | <color> | no | paint | yes | Focus ring colour. |
| 45 | `transform` | none \| translate(<px>[, <px>]) scale(<n>[, <n>]) rotate(<deg>) | no | paint | yes | Paint-only transform; layout is unchanged. |
| 46 | `cursor` | default \| pointer \| text \| grab \| grabbing \| not-allowed \| progress | yes | paint |  | Pointer shape. |
| 47 | `pointer-events` | auto \| none | no | paint |  | `none` makes the node transparent to the pointer. |
| 48 | `visibility` | visible \| hidden | yes | paint |  | `hidden` keeps the space but paints nothing. |
| 49 | `backdrop` | none \| glass | no | paint |  | A request to the compositor to blur what is behind the node. |

#### Text

| Id | Property | Values | Inherited | Affects | Anim | Notes |
|---|---|---|---|---|---|---|
| 50 | `color` | <color> | yes | paint | yes | Text colour. |
| 51 | `font-family` | ui \| mono | yes | layout |  | The interface face or the fixed-width face. |
| 52 | `font-size` | <role> \| <pixels, at least 11> | yes | layout |  | A type role (display, large, title, body, secondary, caption) or a size from 11 px. |
| 53 | `font-weight` | 400 \| 500 \| 600 \| 700 \| normal \| bold | yes | layout |  | Weight. |
| 54 | `font-style` | normal \| italic | yes | layout |  | Slant. |
| 55 | `line-height` | normal \| <number> | yes | layout |  | A multiple of the font size. |
| 56 | `letter-spacing` | normal \| <pixels> | yes | layout |  | Extra space between letters, -2 to 8 px. |
| 57 | `text-align` | start \| center \| end \| justify | yes | layout |  | Alignment of lines in the box. |
| 58 | `text-decoration` | none \| underline \| strikethrough | no | paint |  | Line drawn with the text. |
| 59 | `text-overflow` | clip \| ellipsis | no | layout |  | What shows when a line is cut. |
| 60 | `white-space` | normal \| nowrap \| pre-wrap | yes | layout |  | Wrapping and spaces. |
| 61 | `max-lines` | none \| <integer> | no | layout |  | Lines shown before the text is cut. |
| 62 | `font-feature` | none \| tnum | yes | layout |  | `tnum` turns on tabular numbers. |
| 63 | `user-select` | auto \| none \| text | yes | paint |  | Whether the text can be selected. |

#### Motion

| Id | Property | Values | Inherited | Affects | Anim | Notes |
|---|---|---|---|---|---|---|
| 64 | `transition` | <property>[, <property>] [fast \| normal \| slow \| <ms>] [ease \| linear \| in \| out \| in-out] | no | paint |  | Animate these properties when the node enters this style; at most 500 ms. |
| 65 | `spring` | none \| gentle \| snappy \| bouncy | no | paint |  | Move numeric properties with a spring instead of a curve. |

#### Shorthands

| Shorthand | Values | Sets | Notes |
|---|---|---|---|
| `padding` | <space>{1,4} | `padding-top`, `padding-right`, `padding-bottom`, `padding-left` | One value for all sides, two for vertical and horizontal, four for top, right, bottom, left. |
| `margin` | (auto \| <space>){1,4} | `margin-top`, `margin-right`, `margin-bottom`, `margin-left` | Same forms as `padding`. |
| `gap` | <space>{1,2} | `row-gap`, `column-gap` | One value for both, or row then column. |
| `inset` | (auto \| <length>){1,4} | `top`, `right`, `bottom`, `left` | Same forms as `padding`. |
| `border-radius` | <radius>{1,4} | `border-top-left-radius`, `border-top-right-radius`, `border-bottom-right-radius`, `border-bottom-left-radius` | Corners in the order top left, top right, bottom right, bottom left; fewer values repeat as in CSS. |
| `border` | <pixels> [<color>] | `border-width`, `border-color` | Width and colour of a solid border. |
| `outline` | <pixels> [<color>] | `outline-width`, `outline-color` | Width and colour of the focus ring. |

### Layout

A flexbox-like subset. Every box is border-box; sizes are logical `f32` pixels. A node's size comes from its style, the parent's `stretch` or flex result, or its content. Children are laid out by the flexbox algorithm: single and multi line, grow and shrink with min and max freezing, `justify-content`, `align-items`, `align-self`, `align-content`, gaps, auto margins, `row-reverse` and `column-reverse`, absolute children placed by insets in the parent's box, `aspect-ratio`, and overflow. Where it differs from CSS:

- `min-width` and `min-height` of `auto` are 0, not the content size. Use `shrink: 0` or a minimum where text must not be squeezed.
- A container with `overflow: scroll` or `auto` never shrinks its children; it reports its content extent for scrolling instead. `scroll` and `vlist` nodes start with `overflow: auto` and `flex-direction: column`; a `spacer` starts with `grow: 1`.
- Percentages resolve against the parent's inner box when it is known and behave as `auto` otherwise; a percentage margin is a share of the parent's inner width.
- A content-sized width is the smaller of the content's natural width and the room offered; a content-sized height is never clamped.
- `align-items: baseline` behaves as `start` in 1.0.
- `display: none` removes the node from layout; its rectangle is empty.
- An `overlay` leaves the flow. With an anchor it sits on the chosen side (flipped when it does not fit, shifted to stay inside the window); without one it is centred.
- A `vlist` positions its realised children by row index (the node key) from the list's estimated row height and the measured heights of rows laid out so far.

Incremental layout: a change marks the node and its ancestors up to the nearest *relayout boundary* (a node whose width and height are both in pixels, or that is out of the flow). Measured sizes are cached per node.

### Not in 1.0

Grid (planned for 1.1), floats, tables, pseudo-elements, `@import`, `@font-face`, `@keyframes`, `calc()`, filters and blend modes, `position: fixed` and `sticky`, variable-font axes, descendant and sibling selectors. Each is reported as WK-009 or WK-012, never ignored silently.

### Lints

Diagnostics are sentences with `file:line:col`, the source line, a caret and a suggestion; the word "failed" never appears (RU-003). `tests/lints.rs` has one test per id.

| Id | Severity | Mirrors | Checks |
|---|---|---|---|
| WK-001 | error | RU-062 | A hex colour literal; colours are theme tokens. |
| WK-002 | error | RU-056 | `!important`; the order of resolution is fixed and needs no override. |
| WK-003 | error | RU-059 | A font size below 11 px. |
| WK-004 | error | RU-060 | A transition longer than 500 ms. |
| WK-005 | error |  | Spacing off the scale 0, 4, 8, 12, 16, 24 px (padding, margin, gap, inset). |
| WK-006 | error |  | A corner radius off the scale 0, 6, 10, 14, 20 px. |
| WK-007 | error |  | A colour that is not one of the 13 theme tokens. |
| WK-008 | error |  | A property the subset does not have. |
| WK-009 | error |  | A feature that is not in version 1.0 (grid, floats, calc, filters, @import, pseudo-elements, fixed and sticky). |
| WK-010 | error |  | A value the property does not accept. |
| WK-011 | error |  | A state or media name that does not exist. |
| WK-012 | error |  | A selector outside the supported forms. |
| WK-013 | warning |  | A property set twice in one rule; the later one wins. |
| WK-014 | error |  | A class with more than 8 state or media variants. |
| WK-015 | warning |  | A rule without declarations. |
| WK-016 | error |  | The style table is over 4096 classes or 4 MiB. |
| WK-017 | error |  | A font weight other than 400, 500, 600 or 700. |
| WK-018 | error |  | The sheet is not well formed (braces, colons, semicolons, comments). |
| WK-019 | error |  | A scale, ease, spring or role name that does not exist. |
| WK-020 | warning |  | The off-scale pragma is in use; opt-outs are counted by a ratchet. |
| WK-021 | warning |  | The same selector appears twice for a class; the blocks are merged. |
| WK-022 | error |  | A transition on a property that does not animate. |
| WK-023 | error | RU-062 | A raw colour function (rgb, hsl); colours are theme tokens. |
| WK-024 | error |  | Media conditions that can never hold together (@dark with @light). |

### The `WSB1` table

Little endian: `WSB1`, version, flags, the number of kinds and classes, one variant list per kind, then each class (name and variant list), then a 32-bit FNV-1a checksum. A variant is a state (0 or one bit), a media mask, an optional parent selector, a 128-bit mask of the properties it sets, and the value of each set property in id order. The loader treats the bytes as untrusted: every count is bounded before use, every value is range checked, and it never panics. `wolkit-stylec` also writes `src/gen/styles.cr`, one constant per class.

### The compiler

```
wolkit-stylec [--out build/styles.wsb] [--crystal src/gen/styles.cr] [--deny-warnings] SHEET.wolcss...
wolkit-stylec --doc
```

Sheets are read in sorted order, so the same files always give the same table. Class ids follow first appearance. Errors print every diagnostic and write nothing (exit 1); warnings print and pass unless `--deny-warnings` is given. The engine's dev loader (`style::compile`) and `style::load` run the same parser and the same checks.

### Inline properties

Tier 1 arguments reach the engine as `SET_PROP` ops with the ids of `scene::prop` and are mapped by property onto the same declarations (`style::inline`). A role in `font-size` sets the size only; `text_scale` of the environment scales every declared size.
