@themeshift/vite-plugin-themeshift
v0.3.11
Published
Vite plugin that makes using Style Dictionary + SASS easy as pie.
Readme
@themeshift/vite-plugin-themeshift
ThemeShift is a Vite plugin that makes using Style Dictionary easy as pie.
It watches your design tokens, regenerates token outputs automatically, and keeps your app
up to date without extra build scripts. It also injects a global Sass token() function so
you can reference CSS variables ergonomically in SCSS. It can also extend token JSON published
by UI packages and layer local app overrides on top. It also ships a standalone Sass module
for explicit token() imports.
Why this exists
If you’re already using Style Dictionary to manage design tokens, you usually end up writing custom scripts to rebuild tokens and wire up live reload. ThemeShift moves that logic into a Vite plugin so token changes behave like any other frontend change.
Features
- 👀 Watches
tokens/**/*.jsonand rebuilds on change - ⚙️ Runs Style Dictionary programmatically (no extra CLI step)
- 🎨 Outputs CSS variables for multi-mode theming
- 🧵 Optional Sass output for static tokens
- ✨ Injects a global Sass
token()helper - 📦 Ships a standalone Sass
token()module - 📦 Extends tokens from installed UI packages (like
@themeshift/ui) - 🏷️ Supports CSS variable prefixes
- 🔥 Vite HMR for
tokens.css(fallback to full reload)
Basic usage
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { themeShift } from '@themeshift/vite-plugin-themeshift';
export default defineConfig({
plugins: [react(), themeShift()],
});By default, ThemeShift expects a tokens/ directory in your project root containing
Style Dictionary JSON files and outputs:
src/css/tokens.csssrc/sass/_tokens.static.scsssrc/design-tokens/token-paths.{json,ts}src/design-tokens/token-values.{json,ts}
Local tokens continue to work exactly as before. If you do nothing, the plugin only reads
your app's tokens/**/*.json.
Getting started (new project)
If you're wiring this up for the first time, this is a good baseline setup:
- Install packages
npm install --save-dev @themeshift/vite-plugin-themeshift style-dictionary sass- Add the plugin to
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { themeShift } from '@themeshift/vite-plugin-themeshift';
export default defineConfig({
plugins: [react(), themeShift()],
});- Create your first tokens file
Create tokens/theme.json:
{
"theme": {
"text": {
"base": { "$value": "#0f172a" }
}
}
}- Import the generated CSS
Import the CSS file that ThemeShift generates. For example in src/main.tsx:
import './css/tokens.css';- Ignore generated outputs
In most apps, these files should be treated as build artifacts rather than source:
src/css/tokens.css
src/sass/_tokens.static.scss
src/design-tokens/- Optional: import the Sass token helper directly
ThemeShift injects token() automatically by default. If you prefer explicit Sass imports,
you can use the published module instead:
@use '@themeshift/vite-plugin-themeshift/token' as themeShift;
.button {
color: themeShift.token('theme.text.base');
}If your app uses cssVarPrefix, the plugin injects that prefix as the Sass default automatically,
so the same explicit import still works without module configuration:
@use '@themeshift/vite-plugin-themeshift/token' as themeShift;
.button {
color: themeShift.token('components.button.font');
}You can also pass a prefix explicitly per call:
@use '@themeshift/vite-plugin-themeshift/token' as themeShift;
.button {
color: themeShift.token('components.button.font', 'themeshift');
}For shared mixins and partials, prefer the namespaced import form so they do not depend on root-level injection:
@use '@themeshift/vite-plugin-themeshift/token' as themeShift;
@mixin style($path) {
font: themeShift.token('typography.styles.#{$path}.font');
}Root stylesheets can still use the injected global token() helper. The plugin now injects
that helper as a thin wrapper around the canonical token module, so explicit imports and
root-level injection can coexist in the same Sass compile graph without module configuration
conflicts.
- Optional: use the JavaScript token helpers
ThemeShift also ships a JavaScript runtime API on the same ./token subpath:
import { token, tokenValue } from '@themeshift/vite-plugin-themeshift/token';
import { tokenValues } from './design-tokens/token-values';
const currentTextColor = token('theme.text.base', { prefix: 'themeshift' });
const authoredTitleStyle = tokenValue('text.style.title', { values: tokenValues });token() reads the current computed CSS custom property value in the browser.
tokenValue() reads the authored value from the generated token-values manifest.
Dark/light mode setup
To enable theme modes, split your tokens into separate files:
tokens/theme.light.jsontokens/theme.dark.json
Example:
tokens/theme.light.json:
{
"theme": {
"light": {
"text": {
"base": { "$value": "#0f172a" }
},
"surface": {
"base": { "$value": "#ecf0f1" }
}
}
}
}tokens/theme.dark.json:
{
"theme": {
"dark": {
"text": {
"base": { "$value": "#e2e8f0" }
},
"surface": {
"base": { "$value": "#2c3e50" }
}
}
}
}Then set the data-theme attribute on your document root (usually inside of index.html):
<html lang="en" data-theme="dark">
...
</html>You can toggle data-theme between light and dark to switch modes at runtime. To easily toggle this on the fly
you can use a hook like useDarkMode from useHooks (or you can write your own).
Playground
This repo includes a playground project under playground/ to try things locally.
npm install
npm -C playground install
npm run playgroundPlugin options
type ThemeShiftExtendSource =
| string
| {
package: string;
tokensGlob?: string;
contractFile?: string;
};
type ThemeShiftCssGroup = {
label: string;
match: (name: string) => boolean;
};
type themeShiftOptions = {
tokensGlob?: string; // default: "tokens/**/*.json" (watch uses tokensDir)
tokensDir?: string; // default: "tokens"
extends?: ThemeShiftExtendSource[];
cssVarPrefix?: string;
groups?: ThemeShiftCssGroup[];
defaultTheme?: 'light' | 'dark';
outputPrintTheme?: boolean; // default: false
watch?: boolean; // default: true
injectSassTokenFn?: boolean; // default: true
platforms?: Array<'css' | 'scss' | 'meta'>; // default: all three
filters?: Partial<
Record<
'css' | 'scss' | 'meta',
| {
includePrefixes?: string[];
excludePrefixes?: string[];
}
| ((token: any) => boolean)
>
>;
reloadStrategy?: 'hmr' | 'full'; // default: "hmr"
log?: {
warnings?: 'warn' | 'error' | 'disabled';
verbosity?: 'default' | 'silent' | 'verbose';
errors?: { brokenReferences?: 'throw' | 'console' };
};
};extends
Use extends to load token JSON from installed packages before local app tokens are loaded.
Local files always win.
themeShift({
extends: ['@themeshift/ui'],
cssVarPrefix: 'themeshift',
});Resolution order is:
- Extended package tokens
- Local
tokens/**/*.json
If you pass a string entry like @themeshift/ui, ThemeShift resolves that package from the
consuming app root and looks for theme-contract.json in the package root by default.
Example contract:
{
"name": "@themeshift/ui",
"cssVarPrefix": "themeshift",
"tokensGlob": "dist/tokens/**/*.json",
"version": "1.0.0"
}If you do not want to publish a contract file, use the explicit object form:
themeShift({
extends: [
{
package: '@themeshift/ui',
tokensGlob: 'dist/tokens/**/*.json',
},
],
});You can also override the contract filename:
themeShift({
extends: [
{
package: '@themeshift/ui',
contractFile: 'dist/theme-contract.json',
},
],
});If an extended package cannot be resolved, the build fails with a clear error.
cssVarPrefix
Use cssVarPrefix to make generated CSS variable names part of a stable public contract:
themeShift({
cssVarPrefix: 'themeshift',
});This changes output like:
--components-button-font- to
--themeshift-components-button-font
The injected Sass helper uses the same naming, so token('components.button.font')
resolves to var(--themeshift-components-button-font) when a prefix is configured.
The standalone Sass module uses the same naming contract. It checks the prefix argument first,
then the plugin-injected default prefix, and finally falls back to an unprefixed CSS variable.
The plugin configures the shared default prefix before other Sass imports load, then injects a
local token() wrapper for root stylesheets.
groups
Use groups to customize comment sections in the generated
css/variables-modes-grouped output:
themeShift({
cssVarPrefix: 'themeshift',
groups: [
{ label: 'Colors', match: (name) => name.startsWith('color-') },
{
label: 'Typography',
match: (name) =>
name.startsWith('font-') || name.startsWith('typography-'),
},
{
label: 'Accessibility',
match: (name) =>
name.startsWith('accessibility-') || name.startsWith('a11y-'),
},
{ label: 'Theme', match: (name) => name.startsWith('theme-') },
{
label: 'Components',
match: (name) =>
name.startsWith('component-') || name.startsWith('components-'),
},
{ label: 'Other', match: (_name) => true },
],
});Defaults use the same group list shown above.
groups fully replaces the defaults, and match order matters. The first matching
group wins.
Grouping is always based on the raw token name, not the final CSS variable name.
That means cssVarPrefix does not change how tokens are grouped:
groups.match(name)seescomponents-button-textcssVarPrefix: "themeshift"renders--themeshift-components-button-text
defaultTheme
Use defaultTheme to emit either your light or dark themed variables into bare :root
as a fallback when the document does not have a data-theme attribute.
themeShift({
defaultTheme: 'light',
});This is useful for published component libraries like @themeshift/ui, where you want styles
to work immediately with zero consumer setup.
Theme-specific blocks are still emitted, so apps can override the fallback later with:
:root[data-theme='light']:root[data-theme='dark']
outputPrintTheme
By default, ThemeShift does not emit the :root[data-theme='print'] block or the matching
@media print block.
Set outputPrintTheme: true to opt into that output when you have print theme tokens:
themeShift({
outputPrintTheme: true,
});filters
Use filters to customize which tokens are included per output platform.
By default, ThemeShift preserves its existing behavior:
scssincludes non-themedradius-*,spacing-*,font-*,text-*, andlayout-*tokenscssincludes all tokensmetaincludes all tokens
You can override this with declarative include/exclude rules:
themeShift({
filters: {
scss: {
includePrefixes: ['radius-', 'spacing-', 'font-', 'text-', 'layout-'],
excludePrefixes: ['theme-', 'components-'],
},
css: {
includePrefixes: [],
excludePrefixes: [],
},
meta: {
includePrefixes: [],
excludePrefixes: [],
},
},
});Or with a predicate function for advanced cases:
themeShift({
filters: {
scss: (token) => !token.attributes?.theme,
},
});reloadStrategy
When tokens change, ThemeShift will try to HMR-reload the generated tokens.css. If it
can’t find the CSS module in Vite’s module graph, it will fallback to a full reload.
Set reloadStrategy: "full" to always reload.
log
By default, ThemeShift silences Style Dictionary output (verbosity: "silent" and
warnings: "disabled"). Override to opt back into logs.
Forwarded to Style Dictionary's logging config. Use this to reduce or silence output. For example, to hide warnings:
themeShift({
log: { warnings: 'disabled' },
});To fully silence Style Dictionary output:
themeShift({
log: { verbosity: 'silent', warnings: 'disabled' },
});Token workflow notes
- The
token()Sass helper mapstoken("theme.text.base")→var(--theme-text-base). - With
cssVarPrefix: "themeshift", the same token becomesvar(--themeshift-theme-text-base). - The standalone Sass module exposes the same
token()API via@use '@themeshift/vite-plugin-themeshift/token'. - The Sass API is
token($path, $cssVarPrefix: null), so explicit imports do not requirewith (...). - Shared Sass modules should prefer
@use '@themeshift/vite-plugin-themeshift/token' as themeShift;. - Root stylesheets may use the injected global
token()helper or an explicit namespaced import. - The JavaScript
@themeshift/vite-plugin-themeshift/tokenexport providestoken()for computed CSS values andtokenValue()for authored values fromtoken-values. - Pass the token's JSON path to
token(). CamelCase segments likegapWidthare normalized to kebab-case CSS vars like--...-gap-width. groupsmatches raw token names;cssVarPrefixonly changes emitted CSS custom property names.defaultThemeduplicates eitherlightordarktokens into bare:rootas a startup fallback.- Tokens that include
light,dark, orprintin their path are treated as mode-specific. - Print-theme CSS blocks are only emitted when
outputPrintThemeistrue. - The CSS output groups tokens for readability, and those groups are configurable.
- CSS variable names are intended to be a public API for consuming packages and apps.
Development
npm run devBuild:
npm run buildLicense
MIT
