Authoring
The DSL grammar in detail. Every form you'll need to author or modify a Styles/default.styles file.
If you're new to the design system, start with Overview for the conceptual tour. This page is the syntax reference.
Comments
Both // line comments and /* ... */ block comments work:
// This is a line comment.
brand: #FF6600 // trailing comment
/*
This is a block comment.
Useful for hiding multiple lines temporarily.
*/The hash character # is reserved for hex color literals (#FF6600), so don't use it for comments.
Two kinds of declarations
The DSL distinguishes primitives (1:1 mappings, mode-independent) from semantics (1:N mappings, mode-aware). Primitives are the author-layer building blocks; semantics are the agent-facing surface. The split is visible in the syntax, and it's the central thing to know.
Five generators do the work:
| Generator | Kind | Output |
|---|---|---|
color(seed) | primitive | 11-step OKLCH ramp: .50, .100, ..., .950 |
number(seed, count, ramp) | primitive | N values from a seed using a ramp |
number(range(min, max), count, ramp) | primitive | N values between bounds |
number([stops...]) | primitive | explicit list, stops named .1..N |
boldness(scale) | semantic | 9 mode-aware stops: hint, faint, subtle, soft, mid, firm, bold, strong, intense |
tshirt(scale, min, max) | semantic | variable t-shirt stops: xs, sm, md, lg, xl, 2xl, ..., NxL |
looseness(scale) | semantic | 6 mode-aware stops: none, tight, snug, normal, relaxed, loose |
Primitive: color(seed)
A color(...) primitive expands one seed into an 11-step OKLCH lightness ramp. Output stops are named .50, .100, .200, .300, .400, .500, .600, .700, .800, .900, .950. The seed value IS the .500 stop.
primary: color(#1976D2) // sRGB hex
primary: color(oklch(63.7%, 0.237, 25.331)) // OKLCH (L on 0–1 or as %, C, H)Brilliant's seed template uses OKLCH for the 22 Tailwind palettes (because that's Tailwind v4's source of truth) and lets you pick whichever form is convenient for your own palettes.
Custom palettes work the same way. Any name, either form:
acme_blue: color(oklch(60%, 0.21, 230))
acme_orange: color(#FF6A00)Primitive: number(...)
number(...) is polymorphic: it accepts a seed, a range, or an explicit list.
Generated from a seed. number(seed, count, ramp) produces count values starting from seed, stepping according to ramp:
spacing.raw: number(4, 32, linear()) // → spacing.raw.1=4, .2=8, ..., .32=128
spacing.raw: number(4, 32, linear(step: 8)) // → .1=4, .2=12, .3=20, ...
font.size.raw: number(12, 13, geometric(ratio: 1.2))
// → geometric ramp from 12Ramps available:
linear()(default step equals the seed, so values becomeseed * n)linear(step: S)(each stop adds S)geometric()(default ratio 2)geometric(ratio: R)(each stop multiplies by R)
Generated within a range. number(range(min, max), count, ramp) clamps the generated series:
opacity: number(range(0, 1), 21, linear()) // → 21 stops from 0 to 1Explicit list. Pass a literal list and count / ramp are omitted:
weight.raw: number([100, 200, 300, 400, 500, 600, 700, 800, 900])
// → weight.raw.1..9Semantic: boldness(scale)
Wrap a primitive scale in boldness(...) to produce a 9-stop semantic with the boldness vocabulary:
hint, faint, subtle, soft, mid, firm, bold, strong, intenseThe same vocabulary applies across every domain. Use it for color tones, font weights, stroke widths, visibility:
primary: boldness(color(#0080FF))
// → primary.{hint..intense}
font.weight: boldness(number([100, 200, 300, 400, 500, 600, 700, 800, 900]))
// → font.weight.{hint..intense}
stroke.width: boldness(number([0.25, 0.5, 0.75, 1, 1.5, 2, 3, 5, 8]))
// → stroke.width.{hint..intense}
visibility: boldness(number([0.02, 0.05, 0.10, 0.20, 0.40, 0.60, 0.80, 0.95, 1.0]))
// → visibility.{hint..intense}The catalog maps the 9 boldness names to whatever stops make sense per domain. For colors, bold → .700 (matches OKLCH convention). For font weight, bold → 700 (matches CSS canon). The mapping is positional for explicit lists (list[0] → hint, list[8] → intense).
Mode behavior. Every scale generator (boldness, tshirt, looseness) accepts an optional transforms: argument declaring per-mode index ops on the role list: shift(N), mirror, outward(N). The seed binds these so theme.dark mirrors color stops, density.compact shifts spacing tighter, accessibility.large-text shifts font size up, etc. See Modes & brands for the full vocabulary.
Semantic: tshirt(scale, min, max)
tshirt(...) produces variable-count semantic stops with the tshirt vocabulary:
xs, sm, md, lg, xl, 2xl, 3xl, 4xl, 5xl, 6xl, 7xl, 8xl, 9xlThe vocabulary extends as needed; you'll see as many stops as your underlying scale provides. Use it for spacing, radius, font size:
spacing: tshirt(number(4, 32, linear()))
// → spacing.{xs..6xl} (catalog picks 10 stops)
font.size: tshirt(number([12, 14, 16, 20, 24, 32, 36, 40, 48, 64, 80, 96, 128]))
// → font.size.{xs..9xl} (positional)Boundary stops. min and max named-arg records add custom stops at either end:
radius: tshirt(number([4, 8, 16, 24, 32, 48]),
min: { none: 0 },
max: { full: 9999 })
// → radius.{none, xs, sm, md, lg, xl, 2xl, full}Mode behavior. density.compact shifts the role mappings down a notch toward smaller values (md → sm's old value, etc.). accessibility.large-text shifts them up.
Semantic: looseness(scale)
looseness(...) produces 6 semantic stops with the looseness vocabulary:
none, tight, snug, normal, relaxed, looseUsed for line height and letter spacing:
font.lineHeight: looseness(number([1.0, 1.25, 1.375, 1.5, 1.625, 2.0]))
font.letterSpacing: looseness(number([-0.05, -0.025, -0.0125, 0, 0.025, 0.1]))Mode behavior. accessibility.large-text shifts toward looser by one step.
Bare alias convention
For a color seed wrapped in boldness(...), the bare reference resolves to the mid stop (the seed value):
$primary // → $primary.midUse it for "the canonical version of this color scale." Number-backed scales (spacing, radius, font.size, font.weight, stroke.width, and anything else built on number(...)) emit no bare token, so you must name a stop:
$spacing // error: pick a named stop
$spacing.md // ✓Chrome aliases ($color.surface etc.) keep their full name; they are their own names, not shortcuts.
Single-value primitives
Some primitives don't have a scale. They're a single value:
font.family: Manrope
brand: #0080FF // single hex, no ramp
font.size: 16 // single number
brand.300: #B8B9D6 // a specific stop, no rampBareword identifiers (no quotes) are fine when the value is a single word. Quote when there's whitespace or special characters:
font.family: "Fira Code"If you want multiple font families, declare them as separate primitives:
font.family.serif: "Noto Serif"
font.family.mono: "Fira Code"Reference them with the full dotted path ($font.family.mono).
Pinning a single stop
If a generator gives you a stop that doesn't quite fit, override it by declaring the stop as its own primitive. Explicit beats generated:
primary: boldness(color(#1976D2))
primary.300: #B8B9D6 // hand-picked, replaces what the resolver producedThis is a separate declaration, not a nested block. Stop overrides are just primitives with a dotted name.
Semantic alias declarations
Beyond the boldness/tshirt/looseness generators, you can declare alias semantics that point at another resolved value. These are the chrome roles:
color.surface: neutral.faint
color.surface.container: neutral.subtle
color.text.primary: neutral.bold
color.primary: primary.mid
color.shadow: neutral.950 // primitive ref → mode-immuneThe right-hand side can be another semantic (primary.mid), a primitive stop (neutral.950), or a literal value. Refs to semantics inherit the source's mode-awareness; refs to primitive stops or literals are mode-immune.
Override semantics
Four rules govern how mode branches stack:
Bare value on a mode-aware path = error. The parser rejects
color.primary: #ff0000whencolor.primaryis generated by a semantic. Pick a block form or a dotted-path override.Block at a path = full replace. The block fully replaces the generated semantic's mappings at that path.
Dotted-path mode segment = additive override. Use a dotted path to override just one branch without touching the rest:
color.primary.bold.dark: #00CCFF // override just the bold/dark branchBlock with only
$default= mode-immune lock. A block containing only$defaultlocks that path across all modes:
color.shadow { $default: neutral.950 } // never flips per modeExplicit mode block form
When you want to author per-mode branches by hand, use the block form:
color.primary {
$default: primary.mid
dark: primary.firm
}
color.text.primary {
$default: neutral.bold
dark: neutral.faint
density.compact: neutral.firm
theme.dark, density.compact: neutral.subtle
}$default is the fallback when no mode-specific key matches. Mode keys can be a bare axis value (dark), a fully-qualified path (theme.dark), or a comma-separated combo (theme.dark, density.compact). Combo branches apply only when all listed modes are active.
The parser validates that mode keys belong to a declared axis (see modes { ... } below).
Composites
Composites combine atoms into a structured value. Same shape as primitives, but the value is a record or list.
Typography composites
Records with named fields. Field values can be literals or references:
typography.h1: { fontSize: font.size.3xl, fontWeight: font.weight.bold, lineHeight: 1.2 }
typography.body.md: { fontSize: 16, fontWeight: 400, lineHeight: font.lineHeight.normal }
typography.code: {
fontSize: font.size.sm
fontWeight: font.weight.soft
lineHeight: font.lineHeight.normal
fontFamily: font.family.mono
}Apply a typography composite to text via the Apply Typography Token palette command.
A composite that needs to flip per mode can wrap in a semantic block. Records merge per field, so only the overridden fields change:
typography.h1 {
$default: { fontSize: font.size.3xl, fontWeight: font.weight.bold, lineHeight: 1.2 }
density.compact: { fontSize: font.size.2xl } // only fontSize overrides
}Shadow composites
Lists of drop(...) layers. Each layer takes named args:
shadow.sm: [ drop(y: 1, blur: 2, color: rgba(0, 0, 0, 0.05)) ]
shadow.md: [
drop(y: 2, blur: 4, spread: -1, color: rgba(0, 0, 0, 0.06)),
drop(y: 4, blur: 6, spread: -1, color: rgba(0, 0, 0, 0.10)),
]x, y, blur, spread, color are the layer args. color accepts hex literals or rgba(r, g, b, a).
Apply via the Apply Shadow Token palette command. Replaces existing drop shadows on the element; preserves inner shadows, glows, and blurs.
References
Anywhere a value is expected, you can pass a token reference instead of a literal. A reference is a dotted path that the resolver follows to a concrete value:
color.text.primary: neutral.bold
typography.h1: { fontSize: font.size.3xl, lineHeight: font.lineHeight.tight }
shadow.subtle: shadow.smBareword identifiers in value position are interpreted as references when the path matches a known token, or as plain string values for string-typed primitives like font.family. Brilliant figures it out from context.
References can chain: a semantic can point at another semantic. The resolver walks the chain to a final concrete value.
Mode axes
Declare available axes once at the top of the file:
modes {
theme: [light, dark]
density: [comfortable, compact]
accessibility: [standard, high-contrast, large-text]
}Each axis has a list of values. The first value is the default. Semantic blocks below carry per-mode branches keyed by these axis values. The parser validates that any mode key used in a semantic block belongs to a declared axis.
Active brand and active mode
active { ... } sets the default brand and active mode set for descendants of this folder:
active {
brand: acme_corporate
modes: { theme: dark, density: compact }
}The chat picker in the AI panel can override per session. Per-element and per-canvas settings override per folder.
Removing tokens
unset { ... } drops entries from the merged file. Top-level only. Paths that don't match anything emit a warning instead of failing, so it's safe to leave in place.
ds_file("corporate-blue")
unset {
color.primary // drop the entire semantic
primary.300 // drop a specific stop override
color.surface.dark // drop just the dark branch of a semantic
*.dark // drop the dark branch across ALL semantics
}The wildcard form *.<key> strips that branch from every mode-keyed semantic in one go. Built-in axes (theme, density, accessibility) and any custom axes you declare are recognized automatically. Useful for "switch this brand to light-only" sweeps.
Nested unset inside another block isn't supported.
Picker visibility
hide: stone, zinc, gray, slate // remove from inspector pickers (still resolvable)
pin: primary, color.text.*, color.surface
// float favorites to the top of relevant pickershide: and pin: accept comma-separated paths. Globs (color.text.*) work for path prefixes.
Cascade flags
Two flags control how this file participates in the parent-folder cascade. Both follow .editorconfig semantics.
root: true // stop the parent-folder walk at this file
inherits: none // (in a brand file) opt out of merging with sibling default.stylesroot: true truncates the cascade. Files in ancestor folders are not merged. Useful in monorepos where one sub-project shouldn't inherit from a parent design system. Default is false.
inherits: none makes a brand file standalone instead of layering on top of its sibling default.styles. Default is default (merge enabled). See Modes & Brands for when this matters.
Anti-patterns
A few things that look like they should work but don't:
Decimal stop names.
radius.0.5: 4won't lex as a path because0.5reads as a decimal literal. Use a name (radius.hairline: 4) or quote it as a string key inside anumber([...])list.Variables in literals. There's no string templating; you can't write
red.500: "#${something}". References work in value position only.Computed scales. No
spacing.4 * 2arithmetic. If you need a value, write it.Mode keys on primitives. Primitives are mode-independent by design. To pick between two values per mode, declare two primitives and route via a semantic.
Imports / extends across files. A
.stylesfile can'timportanother file. The cascade across folders gives you the equivalent declaratively.
Anything else, the parser will surface a clear error pointing at the line and column.