Skip to content

Modes & Brands

Two ways to make your design system flex: modes (light/dark, custom axes like density) and brands (different palette + alias overlays for different audiences). Both are first-class in the DSL.

Modes

A mode is a named variant. The seed template declares three axes:

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; the others are alternatives you can switch into. The parser checks every mode key used elsewhere in the file against these axes.

Mode behavior is baked into each generator

A bare semantic generator already carries sensible mode behavior. Write one line, get a mode-aware scale:

primary: boldness(color(#0080FF))                           // dark mirrors; high-contrast pushes outward
spacing: tshirt(number([4, 8, 12, 16, 24, 32, 48, 64]))     // compact -1, large-text +1
font.lineHeight: looseness(number([1.0, 1.25, 1.5, 2.0]))   // large-text widens

Per-generator defaults:

generatordefault transforms
boldness(color(...))theme.dark: mirror, accessibility.high-contrast: outward(1)
boldness(number(...))accessibility.high-contrast: shift(+1)
tshirt(number(...))density.compact: shift(-1), accessibility.large-text: shift(+1)
looseness(number(...))accessibility.large-text: shift(+1)

Three built-in ops, all work on any generator (they only see indices, not value types):

opsemantics
shift(N)Offset the role's stop index by N. Sum-composes when multiple modes are active.
mirrorReflect around the middle stop (hint ↔ intense, and for the full t-shirt ramp xs ↔ 20xl, etc).
outward(N)Push N steps away from the middle, toward whichever terminal stop is closer.

Override per-token when you want different behavior

transforms: on a generator call replaces the defaults entirely (no merging: we don't want "which fired?" debugging). Two shapes:

// Custom transform list: replaces defaults.
font.size: tshirt(number([12, 14, 16, 20, 24, 32, 36, 40, 48, 64, 80, 96, 128]),
                  transforms: { accessibility.large-text: shift(+1) })   // skip compact (no text shrink)

// Explicit opt-out: no mode behavior at all.
radius: tshirt(number([2, 4, 6, 8, 12, 16, 20, 24]),
               transforms: none)   // corner radius stays stable across modes

Mode keys must be axis-qualified (density.compact, never bare compact) so values reused across custom axes don't collide.

Explicit per-mode branches

When the catalog defaults don't fit, override per mode with an explicit block. Per-mode branches live inline with the role:

color.text.primary {
  $default: neutral.bold
  dark:     neutral.faint
}

color.surface { $default: neutral.faint, dark: neutral.950 }

$default is the fallback when no mode-specific key matches. Mode keys can be the bare axis value (dark), a fully-qualified path (theme.dark), or a comma-separated combo (theme.dark, density.compact).

A few notes:

  • Primitives are mode-independent. $primary.500 is the same hex in every mode. If you want a seed to differ per mode, declare two primitives and route between them with a semantic.

  • Records merge per-field. A semantic-wrapped composite like typography.h1 { $default: { ... }, density.compact: { fontSize: font.size.2xl } } only changes fontSize; the other fields inherit from $default.

  • Lists replace. A mode-specific shadow list fully replaces the base.

Override semantics

Four shapes of override apply when you want to tweak the auto-generated semantics:

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

  2. Block at a path = full replace. A 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
primary.bold.dark:       oklch(45%, 0.22, 245)
  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

Combining modes

A single branch can apply when multiple modes are simultaneously active. Comma-list the mode keys:

color.text.primary {
  $default:                       neutral.bold
  dark:                           neutral.faint
  density.compact:                neutral.firm
  theme.dark, density.compact:    neutral.subtle
}

The combo branch applies only when all listed modes are active. Order doesn't matter; the resolver normalizes.

Switching modes

The right toolbar's Design system section (top) has a dropdown per mode axis. Hover to preview; click to commit. With nothing selected, switching sets the canvas-level override (CanvasState.designSystem), which persists in the .design file and applies only to that canvas. With a selection, it sets Element.designSystem on each selected element instead.

There's no default keyboard shortcut for mode switching, but you can bind one through the keybindings file if you do it often.

Custom axes

modes { ... } accepts any axes you want:

modes {
  theme:    [light, dark]
  contrast: [normal, high]
  motion:   [auto, reduced]
}

Then key branches on any of them:

color.text.primary {
  $default:      neutral.bold
  contrast.high: neutral.950
}

color.outline {
  $default:      neutral.soft
  contrast.high: neutral.bold
}

Each axis is independent. You can be in theme.dark + contrast.high + motion.reduced simultaneously, with each axis contributing its overrides through whatever branches match.

Brands

A brand is a sibling .styles file living next to default.styles in the same Styles/ folder:

my-project/
└── Styles/
    ├── default.styles               ← shared base
    ├── acme.styles                  ← brand A overlay (sparse)
    └── globex.styles                ← brand B overlay

Brand files are sparse. They declare only the deltas vs. the base: typically a brand seed, maybe a font family, and a few semantic overrides where the brand wants different mode behavior. Everything else cascades.

A typical brand file:

// Styles/acme.styles
primary:     boldness(color(#FF6A00))
font.family: Inter

color.primary { $default: primary.firm, dark: primary.subtle }    // override a single alias

That's it. Tailwind palettes, typography composites, the other chrome aliases, all inherit from default.styles.

Activating a brand

Three ways to make a brand the active one:

  1. In default.styles for the folder. Declare an active block:

active {
  brand: acme
}

This makes Acme the default brand for every canvas in this folder and below.

  1. Per-element through the inspector. The right toolbar has a brand picker for each element's design system reference.

  1. Per-canvas. With nothing selected, the same Design system section in the right toolbar sets the canvas-level override (CanvasState.designSystem), which persists in the .design file and applies only to that canvas.

The per-element setting wins, then per-canvas, then folder. Standard cascade.

Authoring a brand inline from a blueprint

The ds_file("name") directive in a blueprint creates or updates a brand file alongside any element rows in the same block:

ds_file("corporate-blue")
  primary:     boldness(color(#1976D2))
  neutral:     boldness(color(#5F6368))
  font.family: Roboto

  color.primary { $default: primary.bold, dark: primary.subtle }
  color.surface { $default: neutral.faint, dark: neutral.950 }

The directive runs before any element rows in the block, so subsequent rows can stamp ds(corporate-blue) to use the freshly-written brand. Multiple ds_file(...) blocks for the same name in one call concatenate. Output lands at <canvas-folder>/Styles/corporate-blue.styles; sibling canvases pick it up automatically.

Removing branches in a brand

A brand can strip branches from the merged result with unset { ... }. The wildcard form is useful for "this brand is light-only":

ds_file("acme-light-only")
  primary: boldness(color(#FF6A00))
  unset {
    *.dark             // drop every dark branch from every semantic
  }

You can also unset a single branch (color.surface.dark) or a whole semantic (color.primary). Idempotent: paths that don't match anything emit a warning instead of failing.

Brand .gen.yaml

Each brand gets its own resolved artifact: Styles/.gen/<brand>.gen.yaml. The generated file includes the merged base and brand state. Every alias, primitive, and composite from default.styles shows up, with the brand's deltas applied on top. External tools that consume the brand's .gen.yaml see the full effective design system.

Standalone brands

If you want a brand that doesn't inherit from default.styles (say, a completely different design system that happens to live in the same project), opt out of base inheritance:

// Styles/standalone.styles
inherits: none

primary: boldness(color(#000000))
neutral: boldness(color(#888888))
// ... full standalone declarations

inherits: none declares that this brand stands alone instead of layering on top of base. Default is default (inherit on). The flag is honored when the brand's .gen.yaml artifact is written and in the ds_file pre-write gate, so external tools and the generated file see the isolated system. At paint time the live render cascade currently still merges the base underneath, so treat inherits: none as the authoring contract rather than a hard runtime wall for now.

Cascade across folders

Brilliant's design system cascades through the folder tree, like .editorconfig. If a sub-folder has its own Styles/default.styles, the resolver walks parent folders root → leaf, with later layers winning per-field.

my-project/
├── Styles/
│   └── default.styles               ← project-wide design system
└── Marketing/
    ├── Styles/
    │   └── default.styles           ← Marketing-specific overrides
    └── Hero.design                  ← uses project-wide + Marketing overrides

For canvases under Marketing/, the resolver merges my-project/Styles/default.styles then Marketing/Styles/default.styles (sub-folder wins).

Stopping the cascade

Truncate the parent walk with a root: true declaration in any .styles file:

// Marketing/Styles/default.styles
root: true

primary: boldness(color(#FF6A00))
// ... fully self-contained design system

Files in ancestor folders are not merged. Useful when a sub-project should be a self-contained design system rather than an overlay.

This mirrors .editorconfig's root = true behavior exactly.

Putting it together

A reasonable multi-brand, multi-mode setup might look like:

my-project/
├── Styles/
│   ├── default.styles               // base; semantics + chrome aliases; active.brand=acme
│   ├── acme.styles                  // Acme brand deltas
│   └── globex.styles                // Globex brand deltas
└── Internal/
    ├── Styles/
    │   ├── default.styles           // root: true; standalone internal-tools system
    │   └── alpha.styles             // an internal alpha-build brand
    └── Dashboard.design

Switching theme to dark walks every semantic's branches and picks the dark variant where one exists (and the boldness/tshirt/looseness generators do their auto-flip). Setting active.brand to globex applies the Globex overlay across the whole design (or per element). Canvases under Internal/ use a separate, isolated design system.

Tip: If you're not sure which design system applies to an element, the right toolbar's Design system section shows the active brand and modes. Hover a dropdown to see where each value is inherited from (e.g. "Theme · Inherited from canvas", "Inherited from folder default").

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