Skip to 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.

Create a new file alongside your ocean theme:

sunset.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)

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.

Build both themes in one command:

Terminal window
npx @gesslar/sassy build ocean.yaml sunset.yaml

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

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

shared/tokens.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:

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.

During development, watch all your themes at once:

Terminal window
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.

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.