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)