Style Dictionary v4 × GitHub Actions: Deploy Dark Mode & Multi-Brand Design Tokens Without File Duplication
When operating a design system, you eventually realize something: the moment a second brand is added and dark mode requirements emerge, nearly identical token files start multiplying into four copies. Change one value, and you have to update all four files — miss one, and a color mismatch appears in production. This is not a file management problem; it is an architecture problem.
Style Dictionary v4 is a complete ESM-based rewrite that fully removes the CTI (Category/Type/Item) structural dependency from v3, and independently supports a powerful feature called $extends group inheritance. Using this feature, an environment with 5 brands and 3 modes goes from 15 files that must be edited per theme down to 1. Combine this with a GitHub Actions pipeline, and the moment a designer modifies a token file, the build and npm deployment complete automatically.
This article walks through how to connect $extends-based hierarchy design, a multi-brand build matrix, and a GitHub Actions automated deployment pipeline into a single production-ready architecture. It also covers the complete pipeline code and a ready-to-copy directory structure.
Core Concepts
Style Dictionary v4 and the DTCG Spec
Style Dictionary is a design token build system created by Amazon. It accepts token files in JSON or JS format and transforms them into outputs for various platforms such as CSS variables, iOS Swift, and Android XML. Released officially in Q2 2024, v4 is a complete ESM-based rewrite that shifts to determining token types via token.$type, completing its alignment with the DTCG spec.
DTCG (Design Tokens Community Group): A W3C community group that standardizes the exchange format for design tokens. It published its first stable version (Format Module 1.0) in October 2025, with Style Dictionary, Tokens Studio, and Terrazzo listed as reference implementations.
How $extends Group Inheritance Works
One important fact to establish upfront: $extends is not part of the official DTCG spec — it is an extension feature supported independently by Style Dictionary v4. The DTCG standard handles inheritance between token files via source array composition; the $extends keyword is syntax unique to Style Dictionary v4. This distinction should be clearly understood within your team.
$extends allows one token group to inherit from another, reusing tokens and properties while overriding only what needs to change. It operates via deep merge, where local properties at the same path overwrite inherited ones.
// tokens/themes/dark.json
{
"semantic": {
"$extends": "./light.json",
"$type": "color",
"background": { "$value": "{color.gray-900}" },
"text-primary": { "$value": "{color.gray-50}" }
}
}Path resolution note: In
"$extends": "./light.json", the relative path is resolved relative to the directory where the file is located, not the CWD (current working directory) from which the build command is run. When moving files, update paths accordingly.
Warning:
$extendsdoes not permit circular references. A structure where A inherits B and B references A in return will cause a build error. Inheritance relationships must always be designed as unidirectional.
The 3-Layer Token Architecture
Effective token design is generally organized into three layers. References between layers are only allowed in the Global → Semantic → Component direction. Reverse references cause Style Dictionary to throw a circular reference error at build time.
| Layer | Name | Role | Example |
|---|---|---|---|
| 1 | Global (Primitive) | Raw value definitions | blue-500: #3B82F6 |
| 2 | Semantic (Alias) | Meaning-based references | color-background-primary: {color.blue-500} |
| 3 | Component | UI component-specific | button-bg: {semantic.color-background-primary} |
$extends is most commonly used at layer 2 (semantic) to handle light/dark branching. Layer 1 is shared by all themes; layer 2 replaces only the reference target per theme.
$extendsvssourcedeep merge: Both approaches coexist in Style Dictionary v4.$extendsis group-level inheritance within a token file, whilesourcearray deep merge is file-level composition. It is recommended to establish a clear convention within your team about which approach to use at which layer.
Practical Application
Now that the concepts are clear, let's design the actual file structure. Below is the complete directory structure covered in this article.
design-tokens/
├── tokens/
│ ├── base/
│ │ └── colors.json # Global (Primitive) raw tokens
│ ├── themes/
│ │ ├── light.json # Light semantic tokens
│ │ └── dark.json # Dark semantic tokens (uses $extends)
│ └── brands/
│ ├── brand-a.json # Brand A overrides
│ └── brand-b.json # Brand B overrides
├── dist/ # Build output (add to .gitignore)
│ ├── brand-a/light/variables.css
│ ├── brand-a/dark/variables.css
│ └── ...
├── style-dictionary.config.mjs # Build configuration
├── .github/workflows/tokens.yml # CI/CD pipeline
└── package.jsonExample 1: Designing the Light/Dark Mode Token Structure
Start by defining the shared primitive token file.
// tokens/base/colors.json
{
"color": {
"$type": "color",
"blue-500": { "$value": "#3B82F6" },
"gray-50": { "$value": "#F9FAFB" },
"gray-900": { "$value": "#111827" }
}
}The light theme references primitive tokens from the semantic layer.
// tokens/themes/light.json
{
"semantic": {
"$type": "color",
"background": { "$value": "{color.gray-50}" },
"text-primary": { "$value": "{color.gray-900}" },
"brand": { "$value": "{color.blue-500}" }
}
}The dark theme inherits from light via $extends and only declares what changes.
// tokens/themes/dark.json
{
"semantic": {
"$extends": "./light.json",
"$type": "color",
"background": { "$value": "{color.gray-900}" },
"text-primary": { "$value": "{color.gray-50}" }
}
}| Token | Light | Dark | Management |
|---|---|---|---|
background |
gray-50 |
gray-900 |
Overridden in dark.json |
text-primary |
gray-900 |
gray-50 |
Overridden in dark.json |
brand |
blue-500 |
blue-500 |
Auto-shared via $extends inheritance |
Example 2: Configuring a Multi-Brand Build Matrix
Brand-specific files override only a subset of global tokens.
// tokens/brands/brand-a.json
{
"color": {
"$type": "color",
"blue-500": { "$value": "#6366F1" }
}
}The ESM config file for Style Dictionary v4 generates all outputs by combining brands and modes.
// style-dictionary.config.mjs
import StyleDictionary from 'style-dictionary';
const brands = ['brand-a', 'brand-b'];
const modes = ['light', 'dark'];
// Parallel builds: use Promise.all() to reduce build time as brand/mode counts grow
const buildTasks = brands.flatMap(brand =>
modes.map(async mode => {
const sd = new StyleDictionary({
source: [
'tokens/base/**/*.json',
`tokens/brands/${brand}.json`,
`tokens/themes/${mode}.json`,
],
platforms: {
css: {
transformGroup: 'css',
buildPath: `dist/${brand}/${mode}/`,
files: [{
destination: 'variables.css',
format: 'css/variables',
}],
},
},
});
await sd.buildAllPlatforms();
})
);
await Promise.all(buildTasks);Four files (base, brand-*, light, dark) produce 2 brands × 2 modes = 4 independent CSS outputs. Even if brands grow to five, only one new brand file needs to be added.
Example 3: GitHub Actions Automated Deployment Pipeline
Apply a paths filter so the pipeline only runs when token files change, and handle PRs and the main branch differently.
# .github/workflows/tokens.yml
name: Build & Publish Design Tokens
on:
push:
branches: [main]
paths: ['tokens/**']
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- run: pnpm install --frozen-lockfile
# Validates JSON structure and token reference integrity
# tokens:lint can be configured with token-validator or a team-custom script
- name: Lint tokens
run: pnpm run tokens:lint
- name: Build tokens
run: pnpm run tokens:build
# On PRs, upload output as an artifact for review
- name: Upload artifacts
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: token-dist
path: dist/
# Only publish to npm on main branch merge
# Consider adopting Changesets or semantic-release for version management
- name: Publish to npm
if: github.ref == 'refs/heads/main'
run: pnpm publish --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}| Step | Trigger | Role |
|---|---|---|
| Lint tokens | PR + Push | Validates JSON structure and token references |
| Build tokens | PR + Push | Full brand × mode build |
| Upload artifacts | PR only | Lets reviewers inspect dist/ diff |
| Publish to npm | main push only | Publishes npm package |
Version management tip:
pnpm publish --no-git-checksdeploys without a version tag. For real pipelines, it is recommended to also adopt Changesets or semantic-release to automate version bumping and CHANGELOG generation.
Pros and Cons Analysis
Advantages
| Item | Description |
|---|---|
| Single source | Primitive token files stay as one, regardless of how many brand/mode combinations exist |
| Type safety | $type inheritance catches token type mismatches at build time |
| Platform independence | CSS, Swift, XML, and JS can all be generated simultaneously, eliminating manual work per platform team |
| Full automation | Token change → build → npm publish completes without human intervention |
| DTCG compatibility | Based on the spec stabilized in October 2025, ensuring interoperability across tools like Figma and Tokens Studio |
Disadvantages and Caveats
| Item | Description | Mitigation |
|---|---|---|
| Build matrix explosion | 5 brands × 3 modes × 4 platforms = 60 builds | Promise.all() parallelism + Actions cache |
| npm token security | Classic tokens were deactivated as of December 2025 — existing pipelines need immediate migration | Switch to Granular Access Token or Trusted Publishing |
| Figma sync timing | Figma changes alone cannot trigger the pipeline | Use Tokens Studio bidirectional sync plugin alongside |
| dist/ git noise | Build output changes on every PR, degrading review quality | Add dist/ to .gitignore and manage via CI artifacts only |
| No circular references | $extends mutual references are not allowed |
Design inheritance relationships as strictly unidirectional |
| Version tracking gap | Using --no-git-checks alone makes version tracking impossible |
Adopt Changesets or semantic-release |
The Most Common Mistakes in Practice
- Applying
$extendsdirectly to primitive tokens: The Global (Primitive) layer contains immutable values shared by all themes. Inheritance should only be used at the semantic layer. - Committing
dist/to git: Including build output in the repository makes PR reviews harder and increases merge conflicts. Distribute exclusively via CI artifacts and npm packages. - Continuing to use npm classic tokens: Classic tokens have already been deactivated after December 2025. If you have an existing pipeline, switching to a Granular Access Token is required.
Closing Thoughts
By combining $extends group inheritance with a GitHub Actions pipeline, you can build a token architecture that scales brand and mode combinations infinitely without file duplication, automatically deploying outputs for all platforms from a single source.
Three steps you can take right now:
- Start by establishing the 3-layer directory structure. Create
tokens/base/,tokens/themes/, andtokens/brands/, install v4 withpnpm add -D style-dictionary@^4, and begin by separating your existing token files according to the layers. - Declare one dark theme file using
$extends. Add"$extends": "./light.json"totokens/themes/dark.json, override 2–3 values that change, then callbuildAllPlatforms()to confirm that separate light and dark CSS files are generated indist/. - Add
.github/workflows/tokens.ymlto connect the pipeline. Configure thepaths: ['tokens/**']filter so builds only run on token changes — then the full flow is complete: review diffs as artifacts in PRs, and publish to npm only on main branch merges.
References
- Style Dictionary v4 Official Docs | styledictionary.com
- Style Dictionary v4 Migration Guide | styledictionary.com
- DTCG Utilities — Style Dictionary v4 | v4.styledictionary.com
- Design Tokens Format Module 2025.10 | W3C DTCG
- W3C DTCG Spec Stabilization Announcement | w3.org
- How to run Style Dictionary with a GitHub Action | Specify
- Implementing Light and Dark Mode with Style Dictionary | Always Twisted
- Implementing Multi-Brand Theming with Style Dictionary | Always Twisted
- Dark Mode with Style Dictionary | dbanks.design
- Creating a design tokens automation pipeline with Figma and Style Dictionary | Medium
- Syncing Figma Variables and Style Dictionary with GitHub Actions | James Ives
- Style Dictionary v4 Release Plans | Tokens Studio
- Tokens Studio sd-transforms | GitHub
- Style Dictionary | GitHub
Next article: How to build an end-to-end sync where a designer's Figma changes automatically create a Git PR and trigger the token pipeline, by connecting the Figma Variables REST API with Tokens Studio
sd-transforms