Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

std::style reference

Typed, compile-time atomic styles. Concepts and the emission model: the styling guide.

import std::style::{
	style, space, Style, Length, Color, Gradient,
	Display, Position, FlexDirection, AlignItems, JustifyContent,
	TextAlign, Cursor, Overflow, WhiteSpace, UserSelect, RadialExtent,
};

Constructors and values

fun style(): Style                 // empty style; chain from here (inside a const)
fun space(step: i32): Length       // spacing scale: space(1) = 0.25rem

impl Length {
	fun px(value: f64): Length
	fun rem(value: f64): Length
	fun em(value: f64): Length     // relative to the element's own font size
	fun pct(value: f64): Length
	fun vh(value: f64): Length     // viewport units
	fun vw(value: f64): Length
	fun auto(): Length
	fun zero(): Length             // bare `0`, not `0px`
	fun var(name: str): Length     // a CSS custom-property reference ("--w")
	fun calc(expression: str): Length  // "100% - 2rem" — no calc(..) wrapper
	fun raw(expression: str): Length   // a COMPLETE value, verbatim: "clamp(..)"
}

impl Color {
	fun white(): Color
	fun black(): Color
	fun transparent(): Color
	fun hex(value: str): Color     // "#663399"
	fun var(name: str): Color      // a custom-property reference ("--accent"); the app declares it
	fun gray(step: i32): Color     // ramps: 50…900
	fun blue(step: i32): Color
	fun red(step: i32): Color
	fun green(step: i32): Color

	fun rgba(red: i32, green: i32, blue: i32, alpha: f64): Color  // a literal, 0-255 / 0.0-1.0
	fun oklch(lightness: f64, chroma: f64, hue: f64): Color  // perceptual: 0.0-1.0 / 0.0-0.5 / degrees
	fun alpha(self, value: f64): Color   // THIS colour at that alpha
}

impl Gradient {
	fun linear(degrees: f64): Gradient            // 0 up, 90 right, 180 down
	fun radial(extent: RadialExtent): Gradient    // extent keyword; centred
	fun stop(self, color: Color, percent: f64): Gradient
}

alpha renders the relative-colour form, rgb(from <colour> r g b / a), so a ramp step stays a var(--gray-900) and keeps re-theming — which an 8-digit hex could not do. Channels, alphas and the two-stop gradient minimum are checked during const evaluation, so a bad value stops the build naming itself.

oklch is the perceptual literal — hold a hue angle and step the lightness, and the steps look even across hues, which is what deriving a palette wants and what rgba cannot promise. Lightness takes the CSS number form (0.0–1.0, not a percentage), chroma runs 0.0–0.5 (0 is achromatic; sRGB tops out near 0.37), and the hue angle is degrees in its canonical 0–360 turn — angles wrap in CSS, so one colour keeps one spelling and one class. All three ranges are checked during const evaluation, and .alpha() composes over the result like over any other colour.

Color::var is Length::var’s counterpart — the typed end of the dynamic-value channel. It renders var(--name) and declares nothing: the app owns the custom property’s declaration (its emitted theme block, or view.style_var writing it at runtime). .alpha() composes over it through the same relative-colour form, so a variable-backed colour translucifies exactly like a ramp token.

calc wraps and raw does not: Length::calc(e) is Length::raw("calc(" + e + ")"). Write calc for arithmetic, raw for a value that is already whole — clamp(), min(), max(), env(), fit-content(), or one named expression reused across properties. Both refuse an empty value at const time.

A Gradient is a background-image value, not a Color: it reaches a style through background_gradient, which fills a different slot from background. What a Gradient cannot hold — positioned gradients (at 20% 40%), multi-layer lists, repeating-*, and data-URI images — goes to background_image(str), which writes the same slot, so the two override each other instead of racing in the cascade.

Keyword enums: Display (Flex, Block, …), Position, FlexDirection, AlignItems, JustifyContent, TextAlign, Cursor, Overflow, WhiteSpace (Normal, Nowrap, Pre, PreWrap, PreLine), UserSelect (Auto, Text, All, Offnone, named to stay clear of Option::None, like Display::Hidden).

All eleven are backed enums: each variant carries the CSS keyword it stands for, so the enum is that keyword at runtime and .value() hands it back. That is why the names need not match the keywords — AlignItems::Start is "flex-start", Display::Hidden is "none" — and why passing one costs nothing over passing the string.

Display::Hidden.value()          // "none"
AlignItems::Start.value()        // "flex-start"
Display::parse("inline-block")   // Some(Display::InlineBlock)
Display::parse("nope")           // None

Style methods

Every method returns a new Style with one more property slot; each slot is one atomic rule, deduplicated build-wide.

Layout:

MethodValue
displayDisplay (Flex, Grid, Block, Inline, InlineBlock, InlineFlex, InlineGrid, Hidden)
positionPosition
flex_directionFlexDirection
align_itemsAlignItems
justify_contentJustifyContent
flexstr — the shorthand, "1 1 auto"
flex_shrinkf64
grid_template_columnsstr"repeat(3, 1fr)"
gap, padding, padding_x, padding_y, margin, margin_x, margin_yLength
padding_top, padding_right, padding_bottom, padding_leftLength — one edge
margin_top, margin_right, margin_bottom, margin_leftLength — one edge
width, height, min_width, max_width, min_height, max_heightLength
sizeLength — width and height, the square case (size(Length::rem(1.0)) for an icon box); writes the same two slots, so mixing with width/height is last-wins
top, right, bottom, left, insetLength
overflowOverflow

Appearance:

MethodValue
radiusLength
border(width: Length, color: Color) — always solid
border_top, border_right, border_bottom, border_left(width: Length, color: Color)
border_none— fills the border slot, so it removes a border set earlier
border_colorColor — its own slot, so a hover can recolour without restating the width
box_shadowstr
background, colorColor
background_gradientGradient — the background-image slot
background_imagestr — the same slot, for what a Gradient can’t hold
background_sizestr — up to two components, so not a Length
font_familystr
font_sizeLength
font_weighti32
line_heightf64 — unitless, and the one to prefer (inherits as a ratio)
line_height_lengthLength — the same slot, when the leading is absolute
letter_spacingLength — usually Length::em(..)
text_alignTextAlign
text_decorationstr
white_spaceWhiteSpace
user_selectUserSelect
cursorCursor
opacityf64
transitionstr
transformstr

A str-valued method is not a weaker raw: it keeps the property name checked and completable while the value stays a CSS expression the compiler has nothing to validate (a font stack, a transform list, a shadow layer).

The name carries the arity — whole box (padding), axis (padding_x), edge (padding_top) — and there is no multi-value shorthand method, because padding_y(v).padding_x(h) already computes padding: v h.

Arities mix, and resolve in authoring order. A property that covers others forms a family — padding, margin, inset (over top, right, bottom, left), border (over its parts and edges), background, flex — and last-wins applies to the whole family: a longhand written after the shorthand narrows it, a shorthand written after a longhand replaces the family outright. Per condition, so a hover or within variant never disturbs the base, and through raw too, since the family is a fact about the CSS property.

Escape hatches:

fun raw<V: CssValue>(self, property: str, value: V): Style
fun with_length(self, property: str, value: Length): Style
fun with_color(self, property: str, value: Color): Style

CssValue is what a declaration’s value may be: a str (a complete CSS value, verbatim), a Length, or a Color. A Length or Color that is a theme token — space(4), Color::blue(300) — carries its own :root declaration onto the stylesheet, so .raw("padding", space(4)) is never a dangling var(). with_length and with_color are raw at those two value types, kept for the spelling the surface has always had.

Pass the value, not its text: space(4).text is the string var(--space-4) with the declaration left behind, and a str carries no token.

Conditions

Each takes an inner Style and conditions all of its slots:

fun hover(self, inner: Style): Style
fun focus(self, inner: Style): Style
fun active(self, inner: Style): Style
fun disabled(self, inner: Style): Style
fun first(self, inner: Style): Style      // :first-child
fun last(self, inner: Style): Style       // :last-child
fun within(self, name: str, value: str, inner: Style): Style     // [name="value"] .sX — an ancestor guard
fun children(self, inner: Style): Style   // @layer vilan{.sX > *} — every direct child
fun divide(self, inner: Style): Style     // @layer vilan{.sX > :not(:first-child)} — every child but the first
fun attribute(self, name: str, value: str, inner: Style): Style  // .sX[name="value"] — the element itself
fun pseudo(self, name: str, inner: Style): Style

fun sm(self, inner: Style): Style          // breakpoints (min-width):
fun md(self, inner: Style): Style          // 640px, 768px, 1024px, 1280px
fun lg(self, inner: Style): Style
fun xl(self, inner: Style): Style
fun media(self, min_width: str, inner: Style): Style

Stacking

The four condition axes nest outside-in, in the order the selector nests them — media, then the relation (within, children, divide), then the attribute, then the pseudo-class:

style().md(style().within("data-theme", "dark", style().hover(style().opacity(0.8))))
// @media (min-width: 768px){[data-theme="dark"] .sX:hover{opacity:0.8}}

style().md(style().within("data-theme", "dark", style().attribute("data-open", "true", style().hover(style().opacity(0.8)))))
// @media (min-width: 768px){[data-theme="dark"] .sX[data-open="true"]:hover{opacity:0.8}}

Every other order is a compile-time-evaluation panic naming the fix (hover(within(..)) says to write within(.., hover(..))), and no axis can wrap itself — one media, one relation, one attribute, one pseudo-class per slot. Media rules emit in ascending min-width order, so a chain like .sm(x).lg(y) is mobile-first: the widest matching breakpoint wins.

attribute conditions on the element itself.sX[data-open="true"] — where within is the ancestor form. It is the general spelling of state carried in markup: data-state, data-open, aria-expanded — any attribute rides, aria-* included, and the value matches exactly. The app owns setting the attribute on the element; the style only selects on it. Name and value refuse quotes, spaces and : at const time (they delimit the machinery underneath), and a styling hook is a single token in practice — the same fences guard within’s name and value.

within prepends an ancestor selector, so a composed within(.., hover(..)) rule is more specific than either within(..) or hover(..) alone and wins against both — and the same holds along the attribute axis: attribute(.., hover(..)) outranks both of its parts. Between an uncomposed within(.., x) and hover(y) on the same property the guard wins — a theme shouldn’t be undone by a hover — so use within(.., hover(..)) when a dark theme needs its own hover.

Relations

within guards the element’s own slots on an ancestor’s attribute — the theme condition (within("data-theme", "dark", ..), or any theme id under kolt-style n-ary theming) and every other “when an ancestor says so” state. Its rule is unlayered and beats the element’s own base rule exactly when the guard matches. For colours, prefer declaring per-theme custom properties with a declaration block and reading them with Color::var(..); within covers the structural changes a value swap cannot.

children and divide style the element’s children — the parent-owned spacing idioms (Tailwind’s space-*/divide-*). Their rules emit inside @layer vilan, and that is the whole cascade story: a child’s own Style always wins against a rule reaching in from an ancestor, whatever the specificity — they set defaults the child may refuse, never force. (The cost is symmetric: a children/divide rule cannot override any unlayered CSS.) Where both touch one property, divide outranks children on every child but the first, whichever was written first. Both take an unconditioned inner style in this version — a hover or attribute condition belongs on the child’s own style — and a breakpoint wraps either (md(children(..))).

Declaration blocks

Style dresses an element. A declaration block puts a set of declarations under a selector you choose — a theme’s custom properties under [data-theme="…"], a :root token table, a reset’s box-sizing — minting no class, producing no Style and touching no slot key.

fun declarations(): Declarations                 // opens a declaration chain
fun declare(selector: str, body: Declarations)   // puts the block in the stylesheet
fun preflight()                                  // the opt-in base stylesheet (below)

impl Declarations {
    fun raw<V: CssValue>(self, property: str, value: V): Declarations
    fun color(self, property: str, value: Color): Declarations     // `raw` at that value type
    fun length(self, property: str, value: Length): Declarations   // likewise
}

declare is compile-time-only — it reaches std::asset::emit, so it belongs inside a const expression. Building the chain is ordinary code: a Declarations value accumulates the token lines it owes and declare puts them on the sheet with the block.

import std::style::{ Color, declare, declarations, space };

fun theme(id: str) {
    declare(
        i"[data-theme=\"{id}\"]",
        declarations()
            .color("--color-ink", Color::hex("#fafafa"))
            .color("--color-ground", Color::hex("#161616"))
            .length("--gap", space(4)),
    );
}

let _iron = const theme("iron-dark");

fun main() {}

That emits one line into the build’s stylesheet:

@layer vilan{[data-theme="iron-dark"]{--color-ink:#fafafa;--color-ground:#161616;--gap:var(--space-4)}}

A Color or Length spent in a block — through raw, color or length, which are one method — carries its own :root token line onto the sheet exactly as a Style property does, so a ramp or spacing token used here is never a dangling var(). The line lands when declare does: a chain that is never declared reaches the stylesheet with nothing.

Ordering

Every block emits inside one cascade layer, @layer vilan, and that is the whole ordering rule. Unlayered styles beat layered ones whatever their specificity, so a Style always wins against a declaration block: a block cannot reach in and out-specify a view’s own rules, however specific the selector it names, and where its line lands in the stylesheet’s sort decides nothing. Among blocks, ordinary CSS applies — specificity first, then the sheet’s own deterministic line order.

The other face of the same rule: a block cannot override an unlayered declaration either, which includes std’s own token lines (--space-4, --gray-50) and any hand-written CSS the page loads. Declare your own custom properties and read them back with Color::var / Length::var — that composes exactly, and it is what a theme wants anyway.

The base stylesheet: preflight(). std ships no reset by default — UA defaults are fully in force, so body keeps its 8px margin and width(px(200)).padding(space(4)) measures 232px. const preflight() opts in to one, and deleting the line opts back out; there is no build flag and nothing to switch off, because the only door into the stylesheet is a const expression:

import std::style::preflight;

let _reset = const preflight();

fun main() {}

It is Tailwind’s preflight scope, adapted: box-sizing: border-box everywhere, the UA’s margins zeroed, its form-control chrome stripped so a button inherits the page’s font and colour, replaced elements (img, svg, video, …) laid out as blocks that cannot overflow their container, list markers and heading sizes removed — plus one addition that is not in any reset it is adapted from: a, button and select are display: block. That is the most visible rule in it. An anchor in running prose becomes a block, so a page that wants inline links styles them back; this is an opinionated default, not a normalization.

Ordering is the whole design, and it is settled by the layer rather than by where a line lands. Every reset rule emits inside @layer vilan.preflight, a sub-layer of the layer declaration blocks use, and the same cascade sentence applies twice in the same direction: unlayered beats layered, so every Style wins against every reset rule whatever the specificity; and a layer’s own rules beat its sub-layers’, so every declare block wins against every reset rule too. An app tightening one of the reset’s own rules writes declare("a", …) and needs neither !important nor a longer selector. The reset is therefore the weakest author-origin thing in the sheet — which is what a reset should be — and still stronger than every UA default, because cascade layers order author declarations and the UA origin loses to all of them.

Refusals

Checked at const time, each naming its fix:

  • a selector that is blank, carries a newline — the asset channel is line-granular, so a newline does not indent the rule, it splits it into two independently deduplicated and sorted lines — or carries a brace, which is declare’s to write;
  • a selector that is an at-rule: declare puts declarations under a selector, and a group at-rule (@media, @supports) holds rules, not declarations;
  • a block with no declarations;
  • a property carrying : or ;, the two separators it owns, or a blank value. A ; inside a value stays legal, so a data URI rides (url("data:image/svg+xml;base64,…")).

vilan fmt never reorders a declarations() chain. Its links are cascade text joined in authoring order, where a style() chain’s links each own a slot and may be sorted freely.

Construction emits rules and therefore lives in const; these do not emit and work anywhere:

style_a + style_b          // merge: per-property, right side wins (impl Add)
style.class_list(): str    // the space-joined class attribute (what `styled` uses)