Generic, framework-agnostic theming library with CSS custom properties, multi-theme lazy loading, Figma token sync, and build-time optimization
  • TypeScript 76.3%
  • JavaScript 19.9%
  • SCSS 2.6%
  • Dockerfile 0.6%
  • Shell 0.6%
Find a file
Ladislav Brodecký a036a7de44
Some checks failed
CI / Build, Lint & Test (push) Has been cancelled
chore(release): v1.7.1
2026-06-01 11:52:48 +02:00
.claude chore(release): v1.4.3 2026-04-03 03:46:54 +02:00
.github/workflows feat: add GitHub Actions CI workflow 2026-03-06 11:02:16 +01:00
.verdaccio refactor: regenerate all packages using @nx/js:library generator 2026-03-06 01:00:42 +01:00
examples docs: update examples and READMEs to use defineConfig() and generic useTheme<T>() 2026-03-06 17:17:03 +01:00
packages chore(release): v1.7.1 2026-06-01 11:52:48 +02:00
scripts fix: copy schema.json files to dist for npm publish 2026-03-26 01:01:17 +01:00
.gitignore docs: improve README SCSS usage examples with generated variables 2026-03-18 21:22:22 +01:00
.npmrc feat: initialize Nx monorepo with bun package manager 2026-03-06 00:18:05 +01:00
.nxignore fix: copy schema.json files to dist for npm publish 2026-03-26 01:01:17 +01:00
.prettierignore feat: configure ESLint and Prettier for all packages 2026-03-06 00:32:09 +01:00
.prettierrc feat: configure ESLint and Prettier for all packages 2026-03-06 00:32:09 +01:00
.stylelintrc.json chore: enforce strict ESLint, Stylelint, and const arrow functions 2026-03-06 10:42:35 +01:00
bun.lock chore(release): v{version} 2026-04-03 16:47:22 +02:00
CHANGELOG.md chore(release): v1.7.1 2026-06-01 11:52:48 +02:00
CLAUDE.md chore: update agent instructions and add Docker support 2026-03-06 23:05:08 +01:00
eslint.config.js chore: enforce strict ESLint, Stylelint, and const arrow functions 2026-03-06 10:42:35 +01:00
LICENSE chore: pre-release preparation for v1.0.0 2026-03-06 11:08:00 +01:00
MEMORY.md fix(core): add link creation fallback in buildInlineScript for PWA cached shells 2026-04-03 00:56:43 +02:00
nx.json fix: move release.git to top-level for nx release command 2026-03-10 16:02:16 +01:00
package.json fix(angular): switch to ng-packagr for AOT-compatible output 2026-03-28 14:18:05 +01:00
PLAN.md chore: update agent toolkit infrastructure 2026-03-10 16:04:55 +01:00
PRODUCT.md feat: initialize project for autonomous development 2026-03-06 00:11:53 +01:00
project.json refactor: regenerate all packages using @nx/js:library generator 2026-03-06 01:00:42 +01:00
README.md docs: improve README SCSS usage examples with generated variables 2026-03-18 21:22:22 +01:00
theme-library-spec.md feat: initialize project for autonomous development 2026-03-06 00:11:53 +01:00
TODO.md test: add integration tests for sync theme value generation 2026-03-09 21:28:27 +01:00
tsconfig.base.json feat: implement @themecraft/angular adapter with ThemeLoader, providers 2026-03-06 01:46:40 +01:00
vitest.config.ts feat: configure Vitest for all packages 2026-03-06 00:28:56 +01:00
vitest.workspace.ts refactor: regenerate all packages using @nx/js:library generator 2026-03-06 01:00:42 +01:00

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 npm
@themecraft/angular Angular adapter (services, providers, SSR) npm
@themecraft/react React adapter (ThemeProvider, useTheme, SSR) npm
@themecraft/vue Vue adapter (plugin, useTheme composable) npm

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

  1. Define tokens in tokens.json -- the schema of your design system (token names, not values)
  2. Generate SCSS with themecraft generate -- produces variables and consumer API files with IDE autocomplete
  3. Define theme values in SCSS files -- one file per theme, mapping tokens to concrete values
  4. Compile themes -- each theme SCSS entry point compiles to a standalone CSS file with :root declarations
  5. Load at runtime -- ThemeManager loads the active theme CSS file via <link> tag
  6. Prevent FOUC -- an inline <script> reads the stored preference and sets the correct theme class before first paint
  7. 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