Building Design System Primitives: Tokens and Theming
Learn to build a scalable Design System using design tokens and theme-aware components. Eliminate hardcoded values and enforce UI consistency in React apps.
Previously in this course, we explored using Portals for UI overlays to handle complex, detached DOM structures. Now, we shift our focus from functional DOM placement to visual architectural integrity: building a Design System that scales.
In large-scale React applications, "style drift" is the silent killer of productivity. When developers hardcode hex values or spacing units, the UI loses its cohesion. Today, we define the foundation of a robust Design System by implementing design tokens and theme-aware components.
What are Design Tokens?
Design tokens are the atomic building blocks of your UI. Instead of using raw values like #3b82f6 or 16px directly in your components, you define a semantic mapping.
- Global Tokens: Raw values (e.g.,
blue-500: #3b82f6). - Alias Tokens: Purpose-driven values (e.g.,
brand-primary: var(--blue-500)). - Component Tokens: Contextual values (e.g.,
button-bg: var(--brand-primary)).
By decoupling the value from the intent, you enable theming and global style updates without touching a single component file.
Implementing Themed Components
To enforce consistency, we move away from standard CSS and toward a CSS-in-JS or CSS Variable approach. Let's build a theme provider that leverages CSS variables for maximum performance and compatibility.
The Token Foundation
First, define your design tokens in a central configuration object.
JAVASCRIPT// tokens.js export const themeTokens = { light: { CE9178">'--color-bg': CE9178">'#ffffff', CE9178">'--color-text': CE9178">'#1a1a1a', CE9178">'--spacing-md': CE9178">'16px', }, dark: { CE9178">'--color-bg': CE9178">'#1a1a1a', CE9178">'--color-text': CE9178">'#f0f0f0', CE9178">'--spacing-md': CE9178">'16px', } };
The Themed Wrapper
We use a ThemeProvider to inject these tokens into the root of our application. This allows us to switch themes dynamically by simply swapping the CSS variable definitions.
JSX// ThemeProvider.jsx import { createContext, useContext, useEffect } from CE9178">'react'; const ThemeContext = createContext(CE9178">'light'); export const ThemeProvider = ({ theme, children }) => { useEffect(() => { const root = document.documentElement; const tokens = themeTokens[theme]; Object.entries(tokens).forEach(([key, value]) => { root.style.setProperty(key, value); }); }, [theme]); return ( <ThemeContext.Provider value={theme}> {children} </ThemeContext.Provider> ); };
Enforcing Consistency with Primitives
To ensure developers don't bypass the system, we create "Primitive" components. These are low-level building blocks like Box, Text, and Stack that expose our design tokens as a strict API.
JSX// Box.jsx import styled from CE9178">'styled-components'; export const Box = styled.divCE9178">` background-color: var(--color-bg); color: var(--color-text); padding: var(--spacing-md); transition: background-color 0.2s ease; `;
By wrapping our layout logic in these primitives, we ensure that every Box in the application automatically responds to theme changes and adheres to our spacing scale.
Practice Exercise: Implementing a Theme Switcher
Your task is to extend the ThemeProvider to support a user-defined theme toggle.
- Create a
useThemehook that exposes the current theme and atoggleThemefunction. - Build a
ThemeTogglebutton that updates the state in theThemeProvider. - Ensure that switching the theme triggers a smooth transition for all
Boxcomponents using the CSS variable system.
Common Pitfalls
- Over-tokenizing: Don't create a token for every single value. If a value isn't reused or doesn't have semantic meaning (like a specific border-radius for one unique card), keep it as a local style.
- Ignoring Performance: If you use CSS-in-JS libraries that generate classes dynamically for every prop change, you'll bloat your CSSOM. Prefer static CSS variables for theme switching.
- Hardcoding Units: Always use your tokens (e.g.,
var(--spacing-md)) rather than raw numbers. If you find yourself writingpadding: 16px, create a new token.
Recap
Building a Design System requires a shift in mindset: stop thinking about pixels and start thinking about intent. By implementing CSS-in-JS patterns or CSS variables, you enable theming as a first-class citizen. This approach guarantees consistency across your application, making it easier to maintain and faster to scale.
As you advance, remember that your design system should evolve with your application. If you're struggling with data-heavy views, consider how these UI primitives might integrate with API Design Caching Strategies or the API Design Soft Delete Patterns we've discussed for backend stability.
Up next: Managing Large-Scale Data Fetching.
Work with me

Next.js Website & Landing Page Development
A blazing-fast, SEO-optimized website or landing page in Next.js โ the kind that loads instantly and ranks. Design-to-code, done right.

Next.js Full-Stack Web App Development
A fast, SEO-ready full-stack web app built with Next.js 16 โ from idea to deployed product, by an engineer who ships to production.