Skip to content

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:

GeneratorKindOutput
color(seed)primitive11-step OKLCH ramp: .50, .100, ..., .950
number(seed, count, ramp)primitiveN values from a seed using a ramp
number(range(min, max), count, ramp)primitiveN values between bounds
number([stops...])primitiveexplicit list, stops named .1..N
boldness(scale)semantic9 mode-aware stops: hint, faint, subtle, soft, mid, firm, bold, strong, intense
tshirt(scale, min, max)semanticvariable t-shirt stops: xs, sm, md, lg, xl, 2xl, ..., NxL
looseness(scale)semantic6 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 12

Ramps available:

  • linear() (default step equals the seed, so values become seed * 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 1

Explicit 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..9

Semantic: 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, intense

The 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, 9xl

The 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, loose

Used 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.mid

Use 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 ramp

Bareword 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 produced

This 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-immune

The 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:

  1. Bare value on a mode-aware path = error. The parser rejects color.primary: #ff0000 when color.primary is generated by a semantic. Pick a block form or a dotted-path override.

  2. Block at a path = full replace. The block fully replaces the generated semantic's mappings at that path.

  3. 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 branch
  1. Block with only $default = mode-immune lock. A block containing only $default locks that path across all modes:

color.shadow { $default: neutral.950 }  // never flips per mode

Explicit 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.sm

Bareword 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 pickers

hide: 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.styles

root: 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: 4 won't lex as a path because 0.5 reads as a decimal literal. Use a name (radius.hairline: 4) or quote it as a string key inside a number([...]) 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 * 2 arithmetic. 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 .styles file can't import another 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.

Tip: When you're not sure if something is valid, write it and save. Brilliant's resolver runs on every save and surfaces parse + resolve warnings in the log. The grammar is small enough that most things you'd want to write just work.

We use cookies to understand how you use our site. Analytics cookies help us improve. You can accept them, reject them, or manage preferences.

Customize