- TypeScript 76.3%
- JavaScript 19.9%
- SCSS 2.6%
- Dockerfile 0.6%
- Shell 0.6%
|
|
||
|---|---|---|
| .claude | ||
| .github/workflows | ||
| .verdaccio | ||
| examples | ||
| packages | ||
| scripts | ||
| .gitignore | ||
| .npmrc | ||
| .nxignore | ||
| .prettierignore | ||
| .prettierrc | ||
| .stylelintrc.json | ||
| bun.lock | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| eslint.config.js | ||
| LICENSE | ||
| MEMORY.md | ||
| nx.json | ||
| package.json | ||
| PLAN.md | ||
| PRODUCT.md | ||
| project.json | ||
| README.md | ||
| theme-library-spec.md | ||
| TODO.md | ||
| tsconfig.base.json | ||
| vitest.config.ts | ||
| vitest.workspace.ts | ||
themecraft
A generic, framework-agnostic theming library built on CSS custom properties. Define design tokens, generate type-safe SCSS with IDE autocomplete, lazy-load themes with zero FOUC, tree-shake unused tokens at build time, and sync from Figma -- all from a single toolchain.
Why themecraft?
Most theming tools solve one part of the problem. Style Dictionary generates tokens but has no runtime. Vanilla Extract gives you type safety but locks you into a framework. Tailwind has great defaults but is not a token management system.
themecraft handles the entire pipeline:
Figma --> tokens.json --> Generated SCSS --> CSS Custom Properties --> Lazy-Loaded Themes --> * Tree-Shaken Production CSS*
- Zero framework lock-in -- core is pure TypeScript/SCSS, works with Angular, React, Vue, or vanilla JS
- FOUC-free theme switching -- synchronous inline script reads user preference before first paint
- Lazy-loaded themes -- each theme compiles to a separate CSS file, loaded on demand
- IDE autocomplete -- generated SCSS variables give you compile-time validation in WebStorm and VS Code
- Build-time optimization -- PostCSS plugin strips unused token declarations from production CSS
- Figma sync -- pull design tokens directly from Figma Variables API
Packages
| Package | Description | Version |
|---|---|---|
@themecraft/core |
SCSS engine, CLI, runtime, PostCSS plugin, Figma sync | |
@themecraft/angular |
Angular adapter (services, providers, SSR) | |
@themecraft/react |
React adapter (ThemeProvider, useTheme, SSR) | |
@themecraft/vue |
Vue adapter (plugin, useTheme composable) |
Quick Start
1. Install
npm install @themecraft/core
For framework adapters:
# Angular
npm install @themecraft/angular
# React
npm install @themecraft/react
# Vue
npm install @themecraft/vue
2. Initialize
npx themecraft init
This creates two files:
themecraft.config.json -- project configuration:
{
"$schema": "node_modules/@themecraft/core/schema.json",
"outputDir": "src/generated/theme",
"tokens": "tokens.json",
"themes": {
"ocean": "src/themes/ocean-values.scss",
"sunset": "src/themes/sunset-values.scss"
}
}
tokens.json -- your design token schema:
{
"colors": {
"background": [
"body",
"surface",
"divider"
],
"content": [
"primary",
"secondary"
],
"interactive": [
"primary",
"primary-action",
"on-primary"
]
},
"sizes": {
"padding": [
"xs",
"s",
"m",
"l",
"xl"
],
"corner": [
"s",
"m",
"l"
]
},
"typography": [
"display",
"title-primary",
"body-primary",
"body-secondary",
"caption-primary"
],
"breakpoints": [
"small",
"medium",
"large"
]
}
3. Generate SCSS
npx themecraft generate
This reads tokens.json and generates type-safe SCSS files in your outputDir:
src/generated/theme/
variables/
_colors.scss # $background-body: background-body; ...
_sizes.scss # $padding-xs: padding-xs; ...
_typography.scss # $display: display; ...
_breakpoints.scss # $small: small; ...
_colors.scss # $background-body: var(--color-background-body); ...
_sizes.scss # $padding-xs: var(--size-padding-xs); ...
_typography.scss # @mixin display { ... } @mixin body-primary { ... }
_breakpoints.scss # $small: var(--breakpoint-small); ...
_variables.scss # barrel file re-exporting all variable modules
4. Define Theme Values
Create a theme values file for each theme (e.g., src/themes/ocean-values.scss). Export a $theme variable:
@use '@themecraft/core/theming' as theming;
@use 'theme/variables/colors';
@use 'theme/variables/sizes';
@use 'theme/variables/typography';
@use 'theme/variables/breakpoints';
$theme: theming.define-light-theme((
color: theming.define-color-scheme((
colors.$background-body: #f0f4f8,
colors.$background-surface: #ffffff,
colors.$background-divider: #e2e8f0,
colors.$content-primary: #1a202c,
colors.$content-secondary: #4a5568,
colors.$interactive-primary: #3182ce,
colors.$interactive-primary-action: #2b6cb0,
colors.$interactive-on-primary: #ffffff,
)),
size: (
sizes.$padding-xs: 4px,
sizes.$padding-s: 8px,
sizes.$padding-m: theming.size(12px, 16px, 24px),
sizes.$padding-l: 24px,
sizes.$padding-xl: 32px,
sizes.$corner-s: 2px,
sizes.$corner-m: 4px,
sizes.$corner-l: 8px,
),
typography: (
typography.$display: theming.define-typography-level(48px, 1.2, 700, 'Inter'),
typography.$title-primary: theming.define-typography-level(24px, 1.4, 600, 'Inter'),
typography.$body-primary: theming.define-typography-level(16px, 1.5, 400, 'Inter'),
typography.$body-secondary: theming.define-typography-level(14px, 1.5, 400, 'Inter'),
typography.$caption-primary: theming.define-typography-level(12px, 1.4, 400, 'Inter'),
),
breakpoint: (
breakpoints.$small: 0,
breakpoints.$medium: 768px,
breakpoints.$large: 1024px,
),
));
The theming.size() helper creates responsive values with different sizes per breakpoint (small, medium, large).
5. Generate Theme Entry Points
npx themecraft generate --themes ocean,sunset
This creates compilable SCSS entry points (ocean.scss, sunset.scss) that produce standalone CSS files. Each one
contains all the CSS custom property declarations for that theme.
6. Use Tokens in Your Styles
@use 'theme/colors';
@use 'theme/sizes';
@use 'theme/typography';
.card {
background: colors.$background-surface;
padding: sizes.$padding-m;
border-radius: sizes.$corner-m;
@include typography.body-primary;
}
Since the consumer SCSS variables resolve to var() calls, the compiled CSS output is:
.card {
background: var(--color-background-surface);
padding: var(--size-padding-m);
border-radius: var(--size-corner-m);
font-family: var(--typography-level-body-primary-font-family);
font-size: var(--typography-level-body-primary-font-size);
font-weight: var(--typography-level-body-primary-font-weight);
line-height: var(--typography-level-body-primary-line-height);
letter-spacing: var(--typography-level-body-primary-letter-spacing);
}
Runtime Theme Switching
Vanilla JavaScript
import {defineConfig, ThemeManager} from '@themecraft/core';
const config = defineConfig({
defaultTheme: 'ocean',
themes: ['ocean', 'sunset'],
storage: 'localStorage',
basePath: '/themes/',
preferSystemTheme: {
light: 'ocean',
dark: 'sunset',
},
});
const manager = new ThemeManager(config, document);
// Add FOUC-prevention script to <head> (runs before first paint)
manager.addInlineScript();
// Switch theme at runtime
manager.load('sunset');
// Prefetch a theme for instant switching
manager.prefetch('sunset');
// Prefetch all themes
manager.prefetchAll();
// Clean up when done
manager.destroy();
Angular
// app.config.ts
import {provideTheme} from '@themecraft/angular';
export const appConfig = {
providers: [
provideTheme({
defaultTheme: 'ocean',
themes: ['ocean', 'sunset'],
storage: 'localStorage',
basePath: '/themes/',
}),
],
};
// In a component
import {Component, inject} from '@angular/core';
import {ThemeLoader} from '@themecraft/angular';
@Component({ /* ... */})
export class ThemeSwitcher {
private readonly themeLoader = inject(ThemeLoader);
switchTheme(name: string) {
this.themeLoader.load(name);
}
}
For Angular SSR, add provideThemeServer alongside provideTheme in the server config:
// app.config.server.ts
import {provideThemeServer} from '@themecraft/angular';
export const serverConfig = {
providers: [
// provideTheme(...) is already in the base app config
provideThemeServer(() => getUserThemeFromCookie()),
],
};
React
// App.tsx
import {ThemeProvider, useTheme} from '@themecraft/react';
import {defineConfig} from '@themecraft/core';
const config = defineConfig({
defaultTheme: 'ocean',
themes: ['ocean', 'sunset'],
storage: 'localStorage',
basePath: '/themes/',
});
function App() {
return (
<ThemeProvider config={config}>
<ThemeSwitcher/>
</ThemeProvider>
);
}
function ThemeSwitcher() {
const {load, prefetch, prefetchAll} = useTheme<'ocean' | 'sunset'>();
return (
<button onClick={() => load('sunset')}>
Switch to Sunset
</button>
);
}
For Next.js SSR:
// layout.tsx (App Router)
import {ThemeHead} from '@themecraft/react';
export default function RootLayout({children}) {
return (
<html>
<head>
<ThemeHead config={config} userTheme="ocean"/>
</head>
<body>{children}</body>
</html>
);
}
Vue
// main.ts
import {createApp} from 'vue';
import {themecraftPlugin} from '@themecraft/vue';
import {defineConfig} from '@themecraft/core';
import App from './App.vue';
createApp(App)
.use(themecraftPlugin, defineConfig({
defaultTheme: 'ocean',
themes: ['ocean', 'sunset'],
storage: 'localStorage',
basePath: '/themes/',
}))
.mount('#app');
<!-- ThemeSwitcher.vue -->
<script setup>
import {useTheme} from '@themecraft/vue';
const {load, prefetch, prefetchAll} = useTheme < 'ocean' | 'sunset' > ();
</script>
<template>
<button @click="load('sunset')">Switch to Sunset</button>
</template>
Theme Transitions
Enable smooth CSS transitions when switching themes by adding a transition config:
const config = defineConfig({
defaultTheme: 'ocean',
themes: ['ocean', 'sunset'],
storage: 'localStorage',
basePath: '/themes/',
transition: {
duration: '300ms', // default: '200ms'
easing: 'ease-in-out', // default: 'ease-in-out'
properties: ['color', 'background-color', 'border-color'], // default
},
});
When transition is set, ThemeManager.load() adds a temporary themecraft-transitioning CSS class to <html> with
the configured transition styles. The class is removed after the duration elapses.
You can also use the SCSS mixin to add transitions to specific elements:
@use '@themecraft/core/theming' as theming;
.card {
@include theming.theme-transition(300ms, ease-in-out);
}
Transitions are disabled by default and fully opt-in.
Validate Configuration
Validate your themecraft.config.json, tokens.json, and theme value files:
npx themecraft validate
This checks:
- Config file structure and required fields
- Token schema format (correct types for colors, sizes, typography, breakpoints)
- Theme value files against the token schema (detects missing and unknown tokens)
Exits with code 1 if errors are found. Missing tokens are reported as warnings.
Nuxt
For Nuxt SSR support, use the Nuxt plugin utilities:
// plugins/themecraft.ts
import {createNuxtThemePlugin} from '@themecraft/vue/nuxt';
export default defineNuxtPlugin(
createNuxtThemePlugin({
defaultTheme: 'ocean',
themes: ['ocean', 'sunset'],
storage: 'localStorage',
basePath: '/themes/',
cookieName: 'themecraft-theme', // optional, for SSR theme persistence
}),
);
The Nuxt plugin reads the user's theme from cookies during SSR and provides useTheme() on the client.
// To persist theme in cookies (for SSR)
import {setThemeCookie} from '@themecraft/vue/nuxt';
const {load} = useTheme();
load('sunset');
setThemeCookie('sunset');
PostCSS Plugin (Token Tree-Shaking)
The PostCSS plugin analyzes var() references across your CSS and removes unused --color-*, --size-*, and
--typography-level-* declarations from :root and body blocks. This reduces theme CSS file size in production.
// postcss.config.js
import themecraftPostCSS from '@themecraft/core/postcss';
export default {
plugins: [
themecraftPostCSS({
// Scan additional CSS files for var() references
// (files outside the PostCSS pipeline)
additionalSources: ['dist/components/**/*.css'],
}),
],
};
If your theme defines 50 tokens but your app only uses 20, the other 30 declarations are stripped from the output CSS.
Figma Sync
Pull design tokens directly from the Figma Variables API into your tokens.json:
npx themecraft sync --figma-file-key YOUR_FILE_KEY --token YOUR_PERSONAL_ACCESS_TOKEN
Configure collection mapping in themecraft.config.json:
{
"figma": {
"fileKey": "abc123",
"collectionMapping": {
"Colors": "colors",
"Sizes": "sizes",
"Typography": "typography"
}
}
}
Figma variable naming convention: use / as a separator in Figma (e.g., Background / Body), which maps to
background-body in your token schema.
After syncing, run generate to update your SCSS files (sync does this automatically).
Watch Mode
Re-generate SCSS files automatically when tokens.json changes:
npx themecraft generate --watch
CLI Reference
themecraft <command> [options]
Commands:
init Create themecraft.config.json and tokens.json
generate Generate SCSS files from tokens.json
generate --themes ocean,sunset Also generate theme entry points
generate --watch Watch tokens.json for changes
validate Validate config, tokens, and theme files
sync Sync tokens from Figma Variables API
Sync options:
--figma-file-key <key> Figma file key
--token <token> Figma personal access token
--themes <a,b> Also generate theme entry points after sync
SCSS Engine API
The core SCSS engine provides functions for defining themes. In component styles, use the generated SCSS variables (see Step 6 above) rather than calling engine functions directly.
@use '@themecraft/core/theming' as theming;
@use '@themecraft/core/themes' as themes;
Theme Definition Functions
| Function | Description |
|---|---|
define-light-theme($config) |
Wraps a config map as a light theme |
define-dark-theme($config) |
Wraps a config map as a dark theme |
define-color-scheme($scheme) |
Passes through a color scheme map |
size($small, $medium?, $large?) |
Creates a responsive size map with values per breakpoint |
define-typography-level($font-size, $line-height, $font-weight, $font-family, $letter-spacing) |
Creates a typography level map |
Theme Output
| Mixin | Description |
|---|---|
@include themes.variables() |
Generates all CSS custom property declarations in :root / body |
Theme entry points pass theme values via @use ... with (...) -- this is the only way to configure $themes:
// src/themes/ocean.scss (compiles to ocean.css)
@use './ocean-values' as ocean;
@use '@themecraft/core/themes' with ($themes: (ocean: ocean.$theme));
@include themes.variables(':root');
ThemeConfig Interface
interface ThemeConfig {
defaultTheme: string; // Theme to use when no preference is stored
themes: string[]; // List of all available theme names
storage: 'localStorage' | 'sessionStorage'; // Where to persist user preference
basePath: string; // URL path prefix for theme CSS files (e.g., '/themes/')
preferSystemTheme?: { // Map system color scheme to themes
light: string; // Theme for prefers-color-scheme: light
dark: string; // Theme for prefers-color-scheme: dark
};
prefix?: string; // Storage key prefix (default: none, key = 'theme')
transition?: { // Optional theme transition animation
duration?: string; // CSS duration (default: '200ms')
easing?: string; // CSS easing function (default: 'ease-in-out')
properties?: string[]; // CSS properties to transition (default: color, background-color, border-color)
};
}
How It Works
- Define tokens in
tokens.json-- the schema of your design system (token names, not values) - Generate SCSS with
themecraft generate-- produces variables and consumer API files with IDE autocomplete - Define theme values in SCSS files -- one file per theme, mapping tokens to concrete values
- Compile themes -- each theme SCSS entry point compiles to a standalone CSS file with
:rootdeclarations - Load at runtime --
ThemeManagerloads the active theme CSS file via<link>tag - Prevent FOUC -- an inline
<script>reads the stored preference and sets the correct theme class before first paint - Optimize for production -- PostCSS plugin strips unused token declarations
Architecture
@themecraft/core (required)
+-- SCSS Engine (_theming.scss, _themes.scss, _util.scss)
+-- Runtime (ThemeManager)
+-- CLI (init, generate, sync, watch)
+-- PostCSS Plugin (token tree-shaking)
+-- Figma Sync (API client, variable parser)
@themecraft/angular --> @themecraft/core
@themecraft/react --> @themecraft/core
@themecraft/vue --> @themecraft/core
Framework adapters are thin wrappers (under 50 lines each) that delegate all logic to ThemeManager. You can always use
@themecraft/core directly without a framework adapter.
Browser Support
themecraft uses CSS custom properties, which are supported in all modern browsers (97%+ global coverage). No polyfills are needed for:
- Chrome 49+
- Firefox 31+
- Safari 9.1+
- Edge 15+
Development
# Install dependencies
bun install
# Build all packages
bun nx run-many -t build
# Run all tests
bun nx run-many -t test
# Lint all packages
bun nx run-many -t lint
# Build/test only affected packages
bun nx affected -t build
bun nx affected -t test
License
MIT