Reference · Apps and the UI kit
Wolkit style (.wolcss)
A small style language whose values are theme tokens, compiled ahead of time and checked for mistakes.
On this page
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 · Your first Wolkit app
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.
.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 > .childfor 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-widthandmin-heightofautoare 0, not the content size. Useshrink: 0or a minimum where text must not be squeezed.- A container with
overflow: scrollorautonever shrinks its children; it reports its content extent for scrolling instead.scrollandvlistnodes start withoverflow: autoandflex-direction: column; aspacerstarts withgrow: 1. - Percentages resolve against the parent's inner box when it is known and behave as
autootherwise; 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: baselinebehaves asstartin 1.0.display: noneremoves the node from layout; its rectangle is empty.- An
overlayleaves 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
vlistpositions 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 --docSheets 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.
Related
- Your first Wolkit appA to-do list in eight short steps: a window, a text field, a list, a style sheet, a test without a display and a package.
- Wolkit: the UI kitViews from signals, components, the interface guideline parts, a navigator, services and a test harness, in one Crystal shard.
- libWolkit: the UI engineThe engine under Wolkit: scene, style, layout, text, paint, input and accessibility, behind a small versioned C interface.
- Crystal shards and wol shardOne signed list of Crystal libraries, each pinned to one commit, and the commands that write the pins and the lock for you.
- Crystal rules (RCR)The rules every Crystal file in WolOS follows, the standard library policy, the pinned toolchain and the advisories list.
From the developer documentation, wonderlandos/wolos docs/modules/wolkit-style.md.