DEV Community

Cover image for Generating a Custom Chakra UI v3 Theme from Design Tokens: A Complete Guide
Kiran Mantha
Kiran Mantha

Posted on

Generating a Custom Chakra UI v3 Theme from Design Tokens: A Complete Guide

A practical, framework-agnostic guide to generating a **Chakra UI v3* theme from Figma design tokens — adaptable to any token format.*


Chakra UI v3 introduced a new theming system based on tokens, semantic tokens, conditions, and recipes. While the official docs are great, many teams struggle with automating theme generation from Figma in a scalable, maintainable way.

This guide walks you through:

  • The exact structure Chakra UI v3 expects in themeConfig.json
  • What goes into tokens vs semanticTokens
  • How to support responsive values, multiple themes, and text styles
  • How to integrate the generated theme into your React app

No Figma plugin or proprietary format required — just JSON design tokens and a Node.js script.


Let's start by creating a json called themeConfig.json which defines the final chakra theme.

Important: Chakra UI v3 Uses Panda CSS Under the Hood

Chakra UI v3 is built on top of Panda CSS — a CSS-in-JS engine that powers its styling, theming, and component system.

This means:

  • Your themeConfig.json is not just Chakra-specific — it's a Panda CSS config.
  • The tokens, semanticTokens, conditions, and recipes you define are fully compatible with Panda CSS.
  • You can reuse the same theme in non-React projects (Vue, Svelte, etc.) via Panda.

Official Docs to Bookmark

Resource Link
Chakra UI v3 Docs https://chakra-ui.com
Panda CSS Docs https://panda-css.com
Panda Theme Tokens https://panda-css.com/docs/concepts/theme-tokens
Chakra + Panda Integration https://chakra-ui.com/docs/styled-system/theming

Pro Tip: Read both docs — Chakra handles the React layer, Panda handles the CSS engine.


Critical Rule: No Token References in Final Output

themeConfig.json must contain only **fully resolved values — never {primitives.colors.blue.500}**

Wrong (Do Not Do This)

{
  "colors": {
    "brand": {
      "500": { "value": "{primitives.colors.blue.500}" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Correct (Required)

{
  "colors": {
    "brand": {
      "500": { "value": "#0ea5e9" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why?

  • Panda CSS does not resolve references at runtime
  • Your build script must resolve all {...} references
  • Final output must be static, predictable, and framework-ready

Your transformation script is responsible for:

  1. Detecting {path.to.token}
  2. Traversing the token tree
  3. Recursively resolving nested references
  4. Replacing with final value (#0ea5e9, 1.5rem, etc.)
function resolveReference(ref: string, tokens: any): any {
  if (!ref.startsWith('{') || !ref.endsWith('}')) return ref;
  const path = ref.slice(1, -1).split('.');
  let value = tokens;
  for (const key of path) {
    value = value?.[key];
    if (!value) throw new Error(`Unresolved token: ${ref}`);
  }
  return typeof value === 'object' && 'value' in value
    ? resolveReference(value.value, tokens)
    : value;
}
Enter fullscreen mode Exit fullscreen mode

This is a sample function for token value resolution. The logic will vary on your figma design tokens structure.
Always resolve before writing to themeConfig.json


Prerequisites

  • Node.js ≥ 18
  • Chakra UI v3 (@chakra-ui/react@3.3.1 or later)
  • Design tokens exported as JSON (from Figma, Tokens Studio, etc.)

Step 1: Understand Chakra UI v3 Theme Structure

Chakra v3 uses a token-based system powered by Panda CSS. Your theme config must follow this shape:

interface ChakraThemeConfig {
  breakpoints: Record<string, string>;
  tokens: {
    colors: Record<string, any>;
    fonts: Record<string, string>;
    fontWeights: Record<string, any>;
    spacing: Record<string, any>;
    borders: Record<string, any>;
    radii: Record<string, any>;
    shadows: Record<string, any>;
    gradients: Record<string, any>;
    letterSpacings: Record<string, any>;
  };
  semanticTokens: {
    colors: Record<string, any>;
    spacing: Record<string, any>;
    fontSizes: Record<string, any>;
    lineHeights: Record<string, any>;
    grid: Record<string, any>;
  };
  textStyles: Record<string, { value: any }>;
  themes: Record<string, string>; // for CSS variable scoping
}
Enter fullscreen mode Exit fullscreen mode

Let’s break down each section.


breakpoints

Define your responsive breakpoints in rem.

"breakpoints": {
  "base": "0rem",
  "sm": "40rem",
  "md": "48rem",
  "lg": "64rem",
  "xl": "80rem"
}
Enter fullscreen mode Exit fullscreen mode

Use min-width values from your design system.


tokens — Primitive Design Tokens

These are raw, reusable values. Think of them as CSS variables.

colors

Primitive color palette (not semantic):

"colors": {
  "brand": {
    "50": { "value": "#f0f9ff" },
    "500": { "value": "#0ea5e9" },
    "900": { "value": "#0c4a6e" }
  },
  "gray": { ... },
  "transparent": { "value": "transparent" }
}
Enter fullscreen mode Exit fullscreen mode

Use { value: ... } wrapper — required by Panda CSS.

fonts

Font families:

"fonts": {
  "heading": { "value": "'Inter', sans-serif" },
  "body": { "value": "'Inter', sans-serif" },
  "mono": { "value": "'Fira Code', monospace" }
}
Enter fullscreen mode Exit fullscreen mode

Always wrap in { value: ... }

fontWeights, letterSpacings, spacing, borders, radii, shadows, gradients

All follow the same pattern:

"spacing": {
  "4": { "value": "1rem" },
  "8": { "value": "2rem" }
},
"radii": {
  "md": { "value": "0.375rem" }
},
"shadows": {
  "sm": { "value": "0 1px 3px rgba(0,0,0,0.1)" }
}
Enter fullscreen mode Exit fullscreen mode

Convert px to rem using base 16: px / 16 = rem


semanticTokens — Meaningful, Contextual Tokens

These are design decisions, not raw values.

Use when you want:

  • Responsive values
  • Theme-aware values (light/dark)
  • Semantic meaning (text.primary, bg.card)

Responsive Values (via object)

"fontSizes": {
  "display.xl": {
    "value": {
      "base": "2.5rem",
      "sm": "3rem",
      "md": "3.75rem",
      "lg": "4.5rem"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

base and sm = mobile

md = tablet (optional)

lg+ = desktop

Single Value (non-responsive)

"fontSizes": {
  "body": { "value": "1rem" }
}
Enter fullscreen mode Exit fullscreen mode

grid — Responsive Layout Tokens

"grid": {
  "gutterSize": {
    "value": { "base": "1rem", "lg": "2rem" }
  },
  "count": {
    "value": { "base": 4, "lg": 12 }
  }
}
Enter fullscreen mode Exit fullscreen mode

textStyles — Reusable Typography

Define complete text styles:

"textStyles": {
  "heading.h1": {
    "value": {
      "fontSize": "{fontSizes.display.xl}",
      "lineHeight": "{lineHeights.tight}",
      "fontWeight": "{fontWeights.bold}",
      "fontFamily": "{fonts.heading}",
      "letterSpacing": "{letterSpacings.tight}",
      "textTransform": "uppercase"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Use token references with {path.to.token}

Use textCaseToTransform() to map Figma UPPERCASE to uppercase


themes — Multi-Theme Support (Light/Dark/Minimal/Contrast)

Chakra v3 supports CSS-scoped themes via [data-theme] or css classes. Below is an example with data-theme.

"themes": {
  "lightTheme": "[data-theme=light] &",
  "darkTheme": "[data-theme=dark] &",
  "minimalTheme": "[data-theme=minimal] &",
  "contrastTheme": "[data-theme=contrast] &"
}
Enter fullscreen mode Exit fullscreen mode

Same can be replicated using css classes:

"themes": {
  "lightTheme": ".light &",
  "darkTheme": ".dark &",
  "minimalTheme": ".minimal &",
  "contrastTheme": ".contrast &"
}
Enter fullscreen mode Exit fullscreen mode

then set the defined data-theme or classname to the body tag for different views.

Use in semanticTokens.colors like:

"colors": {
  "bg.page": {
    "value": {
      "_light": "#ffffff",
      "_darkTheme": "#0f172a",
      "_minimalTheme": "#f8f9fa",
      "_contrastTheme": "#000000"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

_light is default

Use _themeNameTheme for custom themes


Final themeConfig.json Example

{
  "breakpoints": { ... },
  "tokens": {
    "colors": {
      "brand": {
        "500": { "value": "#0ea5e9" }
      }
    },
    "fonts": { ... },
    "spacing": { ... }
  },
  "semanticTokens": {
    "colors": {
      "text.primary": {
        "value": { "_light": "#1f2937", "_darkTheme": "#f1f5f9" }
      }
    },
    "fontSizes": {
      "display.xl": { "value": { "base": "2.5rem", "lg": "4.5rem" } }
    }
  },
  "textStyles": {
    "heading.h1": { "value": { ... } }
  },
  "themes": {
    "lightTheme": "[data-theme=light] &",
    "darkTheme": "[data-theme=dark] &",
    "minimalTheme": "[data-theme=minimal] &",
    "contrastTheme": "[data-theme=contrast] &"
  }
}
Enter fullscreen mode Exit fullscreen mode

No {...} references — all values fully resolved


Step 2: Generate themeConfig.json (Any Format to Chakra)

Write a Node.js script that:

  1. Reads your Figma JSON
  2. Resolves all {references}
  3. Converts px to rem
  4. Maps to Chakra/Panda structure
  5. Outputs dist/themeConfig.json

Your input format doesn’t matter — only the output does.

Example Script Skeleton

// generate-theme.ts
import fs from 'fs/promises';
import path from 'path';

const pxToRem = (px: number) => `${px / 16}rem`;

function resolveReference(ref: string, tokens: any): any {
  if (!ref.startsWith('{') || !ref.endsWith('}')) return ref;
  const path = ref.slice(1, -1).split('.');
  let value = tokens;
  for (const key of path) {
    value = value?.[key];
    if (!value) throw new Error(`Unresolved token: ${ref}`);
  }
  return typeof value === 'object' && 'value' in value
    ? resolveReference(value.value, tokens)
    : value;
}

async function generateTheme() {
  const input = JSON.parse(await fs.readFile('tokens.json', 'utf-8'));

  const theme: any = {
    breakpoints: { base: '0rem', sm: '40rem', md: '48rem', lg: '64rem', xl: '80rem' },
    tokens: { colors: {}, fonts: {}, spacing: {} },
    semanticTokens: { colors: {}, fontSizes: {} },
    textStyles: {},
    themes: {
      lightTheme: '[data-theme=light] &',
      darkTheme: '[data-theme=dark] &',
      minimalTheme: '[data-theme=minimal] &',
      contrastTheme: '[data-theme=contrast] &'
    }
  };

  // Map your tokens to Chakra/Panda
  // Use resolveReference() everywhere
  // ...

  await fs.mkdir('dist', { recursive: true });
  await fs.writeFile('dist/themeConfig.json', JSON.stringify(theme, null, 2));
}

generateTheme();
Enter fullscreen mode Exit fullscreen mode

Step 3: Use in React App

// theme.ts
import * as themeConfig from './dist/themeConfig.json';
import { createSystem, defaultConfig, defineConfig } from '@chakra-ui/react';

const customConfig = defineConfig({
  theme: {
    ...(themeConfig as any),
    recipes: { /* your component recipes */ },
    slotRecipes: { /* your slot recipes */ },
  },
  conditions: themeConfig.themes as any,
});

export const system = createSystem(defaultConfig, customConfig);
export const { ThemeProvider, theme } = system;
Enter fullscreen mode Exit fullscreen mode

App Entry

// App.tsx
import { ThemeProvider, theme } from './theme';
import { Theme } from '@chakra-ui/react';

export default function App() {
  return (
    <ThemeProvider>
      <Theme accentColor="brand.500">
        <YourApp />
      </Theme>
    </ThemeProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Switching Themes at Runtime

<button onClick={() => document.documentElement.dataset.theme = 'minimal'}>
  Minimal Mode
</button>
Enter fullscreen mode Exit fullscreen mode

Chakra automatically picks up [data-theme=minimal]


Best Practices

Do Don't
Resolve all {...} references Leave references in output
Use { value: ... } everywhere Use raw strings in tokens
Convert px to rem Keep px values
Use semanticTokens for responsive values Put responsive values in tokens
Use themes object for multi-theme Hardcode light/dark in tokens
Reference tokens with {path} in textStyles Duplicate values

Tools & Resources


Conclusion

You now have a complete, scalable pipeline:

Figma to JSON Tokens => generate-theme.ts => themeConfig.json (resolved) to Chakra UI v3
Enter fullscreen mode Exit fullscreen mode

This system:

  • Works with any token format
  • Supports responsive, multi-theme, semantic design
  • Is fully automated and type-safe
  • Scales to large design systems
  • Powered by Panda CSS — future-proof and framework-agnostic
  • Zero runtime token resolution

Share your ideas in the comments.

Happy theming!

Kiran 👋 👋

Top comments (0)