Skip to content

React Integration

This guide shows you how to integrate the Axiomatic Color into a React application using TypeScript.

First, install the CLI and generate your theme CSS and TypeScript definitions.

Terminal window
npm install -D @axiomatic-design/color
npx axiomatic init
npx axiomatic build --out ./src/theme.css
npx axiomatic export --format typescript --out ./src/theme.ts

Import the CSS in your root entry point (e.g., main.tsx or App.tsx):

import "./theme.css";

The core concept of the system is the Surface. In React, we can create a reusable <Surface> component that handles the nesting logic automatically.

src/components/Surface.tsx
import React from "react";
import { tokens } from "../theme";
type SurfaceProps = {
as?: React.ElementType;
variant: keyof typeof tokens.surface;
children: React.ReactNode;
className?: string;
} & React.HTMLAttributes<HTMLElement>;
export function Surface({
as: Component = "div",
variant,
children,
className = "",
...props
}: SurfaceProps) {
const surfaceClass = tokens.surface[variant];
return (
<Component className={`${surfaceClass} ${className}`} {...props}>
{children}
</Component>
);
}

To handle Dark Mode and System preferences robustly, we recommend creating a Context that wraps the ThemeManager.

src/context/ThemeContext.tsx
import React, { createContext, useContext, useEffect, useState } from "react";
import { ThemeManager, type ThemeMode } from "@axiomatic-design/color/browser";
type ThemeContextType = {
mode: ThemeMode;
setMode: (mode: ThemeMode) => void;
toggle: () => void;
};
const ThemeContext = createContext<ThemeContextType | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
// Initialize the manager once
const [manager] = useState(() => new ThemeManager());
const [mode, setModeState] = useState<ThemeMode>("system");
useEffect(() => {
// Restore saved preference on mount
const saved = localStorage.getItem("theme") as ThemeMode | null;
if (saved) {
manager.setMode(saved);
setModeState(saved);
}
}, [manager]);
const setMode = (newMode: ThemeMode) => {
manager.setMode(newMode);
setModeState(newMode);
localStorage.setItem("theme", newMode);
};
const toggle = () => {
const next = manager.resolvedMode === "light" ? "dark" : "light";
setMode(next);
};
return (
<ThemeContext.Provider value={{ mode, setMode, toggle }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error("useTheme must be used within a ThemeProvider");
return context;
}

Wrap your application in the ThemeProvider, and then use the useTheme hook to control the theme.

src/App.tsx
import { Surface } from "./components/Surface";
import { ThemeProvider, useTheme } from "./context/ThemeContext";
import { tokens } from "./theme";
function Header() {
const { toggle, mode } = useTheme();
return (
<header style={{ display: "flex", justifyContent: "space-between" }}>
<h1 style={{ color: tokens.context.text.high }}>My App</h1>
<button onClick={toggle}>{mode === "light" ? "🌙" : "☀️"}</button>
</header>
);
}
export default function App() {
return (
<ThemeProvider>
<Surface variant="page" style={{ minHeight: "100vh", padding: "2rem" }}>
<Header />
<main>
<Surface
variant="card"
style={{ padding: "2rem", borderRadius: "8px" }}
>
<h2 style={{ color: tokens.context.text.high }}>Hello World</h2>
<p style={{ color: tokens.context.text.subtle }}>
This card is automatically themed based on its context.
</p>
</Surface>
</main>
</Surface>
</ThemeProvider>
);
}