Skip to main content

Multiple Themes, One System

This is the power move. You have a design system with palette colours, semantic tokens, and scope mappings. Now you'll create a second theme that reuses the entire system -- just with different colours.

The Sunset Theme

Create a new file alongside your ocean theme:

sunset.yaml:

yaml
config:
name: "Sunset"
type: dark
import:
- "./shared/palette.yaml"
- "./shared/variables.yaml"

# Override the palette
palette:
blue: "#8b4513"
cyan: "#ff8c42"
gray: "#4a3728"
white: "#f5e6d3"
red: "#e74c3c"
green: "#27ae60"
yellow: "#f39c12"

vars:
# Override the semantic anchors
accent: $$cyan
main: $$white
std:
bg: "#1a1008"

theme:
colors:
editor.background: $(std.bg)
editor.foreground: $(std.fg)
editorCursor.foreground: $(std.fg.accent)
sideBar.background: $(std.bg.panel)
sideBarSectionHeader.background: $(std.bg.panel.inner)
activityBar.background: $(std.bg.panel)
focusBorder: $(std.outline)
widget.shadow: $(std.shadow)

editorError.foreground: $(status.error)
editorWarning.foreground: $(status.warning)
editorInfo.foreground: $(status.info)

tokenColors:
- name: Comments
scope: comment
settings:
foreground: $(scope.comment)
- name: Keywords
scope: keyword
settings:
foreground: $(scope.keyword)
- name: Strings
scope: string
settings:
foreground: $(scope.string)
- name: Numbers
scope: constant.numeric
settings:
foreground: $(scope.number)
- name: Functions
scope: entity.name.function
settings:
foreground: $(scope.function)
- name: Types
scope: entity.name.type
settings:
foreground: $(scope.type)

How Override Cascading Works

The imports load shared/palette.yaml and shared/variables.yaml first, establishing the full design system. Then the main file's palette and vars sections merge on top. Because objects deep-merge:

  • palette.blue changes from #2d5a87 to #8b4513
  • palette.cyan changes from #4a9eff to #ff8c42 — and since accent is $$cyan, it picks up the new value automatically
  • std.bg changes to #1a1008
  • Everything else -- std.fg, std.bg.panel, scope.keyword, all of it -- recalculates automatically from the new values

You only override what changes. The derived values cascade.

Building Both

Build both themes in one command:

TERMINAL
npx @gesslar/sassy build ocean.yaml sunset.yaml

Each gets its own .color-theme.json output. Same structure, completely different feel.

Sharing tokenColors Too

If your themes use identical tokenColors, extract them into a shared file:

shared/tokens.yaml:

yaml
theme:
tokenColors:
- name: Comments
scope: comment
settings:
foreground: $(scope.comment)
- name: Keywords
scope: keyword
settings:
foreground: $(scope.keyword)
- name: Strings
scope: string
settings:
foreground: $(scope.string)
- name: Numbers
scope: constant.numeric
settings:
foreground: $(scope.number)
- name: Functions
scope: entity.name.function
settings:
foreground: $(scope.function)
- name: Types
scope: entity.name.type
settings:
foreground: $(scope.type)

Then both themes import it:

yaml
config:
import:
- "./shared/palette.yaml"
- "./shared/variables.yaml"
- "./shared/tokens.yaml"

Remember: tokenColors from imports appear first in the output, and the main file's tokenColors come last. Since VS Code uses first-match semantics, the imported rules take precedence and the main file's entries act as fallbacks for scopes not already covered.

Watch Mode with Multiple Themes

During development, watch all your themes at once:

TERMINAL
npx @gesslar/sassy build --watch ocean.yaml sunset.yaml

Edit shared/variables.yaml and both themes rebuild. Edit a palette override in sunset.yaml and only that theme rebuilds. Sassy tracks dependencies and rebuilds only what changed.

Next Steps

So far every colour has been a hex code. But Sassy supports far more expressive colour formats -- and some of them are genuinely better for theme design.