WolOS
Reference contents

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.

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

IdTokenUnder @contrast
0surfaceunchanged
1sidebarunchanged
2cardunchanged
3linetext at 55%
4textunchanged
5dimtext
6accentunchanged
7accentTextunchanged
8glasssurface
9controlunchanged
10successunchanged
11warningunchanged
12dangerunchanged

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

ScaleNames and valuesTheme 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
Radiusnone 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
Easesease (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):

RoleSizeUsual weight
display34px700
large26px600
title17px600
body13px400
secondary11px400
caption11px400

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

ShadowLayers (offset y, blur, alpha)
nonenone
sm1px 3px 16%
md3px 10px 20%
lg6px 18px 24%; 1px 4px 16%

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

SpringStiffnessDampingMass
gentle170261
snappy320301
bouncy260141

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

IdPropertyValuesInheritedAffectsAnimNotes
0displayflex | nonenolayoutnone removes the node from layout and paint.
1flex-directionrow | column | row-reverse | column-reversenolayoutMain axis of the node's children.
2flex-wrapnowrap | wrapnolayoutWhether children break onto more lines.
3grow<number>nolayoutShare of the free main-axis space.
4shrink<number>nolayoutShare of the overflow taken back; ignored in scroll and auto containers.
5basisauto | <length> | <percent>nolayoutStarting main size before grow and shrink.
6row-gap<space>nolayoutSpace between lines (and between children of a column).
7column-gap<space>nolayoutSpace between children of a row.
8align-itemsstretch | start | center | end | baselinenolayoutCross-axis placement of children; baseline behaves as start in 1.0.
9align-selfauto | stretch | start | center | end | baselinenolayoutOverrides the parent's align-items for this node.
10align-contentstretch | start | center | end | space-between | space-aroundnolayoutPlacement of lines when wrapping.
11justify-contentstart | center | end | space-between | space-around | space-evenlynolayoutMain-axis placement of children.
12padding-top<space>nolayoutInner space, top.
13padding-right<space>nolayoutInner space, right.
14padding-bottom<space>nolayoutInner space, bottom.
15padding-left<space>nolayoutInner space, left.
16margin-topauto | <space>nolayoutOuter space, top.
17margin-rightauto | <space>nolayoutOuter space, right.
18margin-bottomauto | <space>nolayoutOuter space, bottom.
19margin-leftauto | <space>nolayoutOuter space, left.
20widthauto | <length> | <percent>nolayoutBorder-box width.
21heightauto | <length> | <percent>nolayoutBorder-box height.
22min-widthauto | <length> | <percent>nolayoutLower bound of the width; auto is 0.
23min-heightauto | <length> | <percent>nolayoutLower bound of the height; auto is 0.
24max-widthnone | <length> | <percent>nolayoutUpper bound of the width.
25max-heightnone | <length> | <percent>nolayoutUpper bound of the height.
26aspect-ratioauto | <number> | <number> / <number>nolayoutWidth over height, used when one side is open.
27positionrelative | absolutenolayoutabsolute leaves the flow and is placed by its insets in the parent's box.
28topauto | <length> | <percent>nolayoutOffset from the top edge of an absolute node.
29rightauto | <length> | <percent>nolayoutOffset from the right edge of an absolute node.
30bottomauto | <length> | <percent>nolayoutOffset from the bottom edge of an absolute node.
31leftauto | <length> | <percent>nolayoutOffset from the left edge of an absolute node.
32overflowvisible | hidden | scroll | autonolayoutClipping and scrolling of children.
33z-index<integer>nopaintPaint order among siblings.

Paint

IdPropertyValuesInheritedAffectsAnimNotes
34border-width<pixels>nolayoutBorder width; counts as padding for layout.
35backgroundnone | <color> | linear-gradient(<angle>, <color>, <color> [, <color>])nopaintyesFill; a gradient has two or three evenly spaced stops.
36border-color<color>nopaintyesBorder colour.
37border-top-left-radius<radius>nopaintyesCorner radius, top left.
38border-top-right-radius<radius>nopaintyesCorner radius, top right.
39border-bottom-right-radius<radius>nopaintyesCorner radius, bottom right.
40border-bottom-left-radius<radius>nopaintyesCorner radius, bottom left.
41box-shadownone | <shadow> [, <shadow>]nopaintyesElevation: sm, md or lg; two layers at most (lg is two).
42opacity<number> | <percent>nopaintyesGroup opacity.
43outline-width<pixels>nopaintyesFocus ring width.
44outline-color<color>nopaintyesFocus ring colour.
45transformnone | translate(<px>[, <px>]) scale(<n>[, <n>]) rotate(<deg>)nopaintyesPaint-only transform; layout is unchanged.
46cursordefault | pointer | text | grab | grabbing | not-allowed | progressyespaintPointer shape.
47pointer-eventsauto | nonenopaintnone makes the node transparent to the pointer.
48visibilityvisible | hiddenyespainthidden keeps the space but paints nothing.
49backdropnone | glassnopaintA request to the compositor to blur what is behind the node.

Text

IdPropertyValuesInheritedAffectsAnimNotes
50color<color>yespaintyesText colour.
51font-familyui | monoyeslayoutThe interface face or the fixed-width face.
52font-size<role> | <pixels, at least 11>yeslayoutA type role (display, large, title, body, secondary, caption) or a size from 11 px.
53font-weight400 | 500 | 600 | 700 | normal | boldyeslayoutWeight.
54font-stylenormal | italicyeslayoutSlant.
55line-heightnormal | <number>yeslayoutA multiple of the font size.
56letter-spacingnormal | <pixels>yeslayoutExtra space between letters, -2 to 8 px.
57text-alignstart | center | end | justifyyeslayoutAlignment of lines in the box.
58text-decorationnone | underline | strikethroughnopaintLine drawn with the text.
59text-overflowclip | ellipsisnolayoutWhat shows when a line is cut.
60white-spacenormal | nowrap | pre-wrapyeslayoutWrapping and spaces.
61max-linesnone | <integer>nolayoutLines shown before the text is cut.
62font-featurenone | tnumyeslayouttnum turns on tabular numbers.
63user-selectauto | none | textyespaintWhether the text can be selected.

Motion

IdPropertyValuesInheritedAffectsAnimNotes
64transition<property>[, <property>] [fast | normal | slow | <ms>] [ease | linear | in | out | in-out]nopaintAnimate these properties when the node enters this style; at most 500 ms.
65springnone | gentle | snappy | bouncynopaintMove numeric properties with a spring instead of a curve.

Shorthands

ShorthandValuesSetsNotes
padding<space>{1,4}padding-top, padding-right, padding-bottom, padding-leftOne 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-leftSame forms as padding.
gap<space>{1,2}row-gap, column-gapOne value for both, or row then column.
inset(auto | <length>){1,4}top, right, bottom, leftSame forms as padding.
border-radius<radius>{1,4}border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radiusCorners in the order top left, top right, bottom right, bottom left; fewer values repeat as in CSS.
border<pixels> [<color>]border-width, border-colorWidth and colour of a solid border.
outline<pixels> [<color>]outline-width, outline-colorWidth 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.

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

From the developer documentation, wonderlandos/wolos docs/modules/wolkit-style.md.