Theme
Learn about the Dynamic Color system, design tokens, and how to configure MD3ThemeProvider for your application.
Bug On MD3 Expressive provides native support for the Material Design 3 (Material You) Dynamic Color system. This system generates accessible, harmonious color palettes derived from a single Source Color using the HCT (Hue, Chroma, Tone) color space algorithm.
MD3ThemeProvider
MD3ThemeProvider is the single root entry point for theme state and tokens. It manages:
- 🎨 Dynamic Palette Generation — Calculates light and dark mode color schemes from a source color.
- 🌓 Theme Modes — Manages Light, Dark, and System theme state (
window.matchMedia). - 🔤 Typography & Variables — Configures font family and variable font axes (Google Sans Flex defaults).
- ⚡ Framer Motion Context — Provides spring motion tokens required by animated components.
- 🔔 Snackbar Queue (opt-in) — Global notification queue via
enableSnackbar.
Basic Configuration
Wrap your application at the root level (e.g., layout.tsx or App.tsx):
import { MD3ThemeProvider } from "@bug-on/m3-expressive/core";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<MD3ThemeProvider
sourceColor="#6750A4"
defaultMode="system"
variant="expressive"
contrastLevel={0}
persistToLocalStorage
>
{children}
</MD3ThemeProvider>
);
}
With Global Snackbar Queue
Pass enableSnackbar to activate the global notification host. This allows useSnackbar() to be called anywhere in your app without needing an extra provider:
<MD3ThemeProvider
sourceColor="#6750A4"
defaultMode="light"
variant="expressive"
contrastLevel={0}
persistToLocalStorage
enableSnackbar
>
{children}
</MD3ThemeProvider>
With Custom Typography Overrides
Override font family or variable font axes directly on the provider:
<MD3ThemeProvider
sourceColor="#6750A4"
fontFamily="'Inter', sans-serif"
fontVariationAxes={{ ROND: 50 }} // 0 = sharp corners, 100 = full rounded variable font
>
{children}
</MD3ThemeProvider>
Provider Props Reference
Theme Props
| Prop | Type | Default | Description |
|---|---|---|---|
sourceColor | string | #6750A4 | Primary hex color used to derive the entire MD3 palette. |
defaultMode | ThemeMode | "light" | Initial theme mode ("light", "dark", or "system"). |
variant | SchemeVariant | "expressive" | Color scheme variant ("expressive", "tonal_spot", "vibrant", "fidelity", "content", "monochrome", "neutral"). |
contrastLevel | ContrastLevel | 0 | Contrast level for generated scheme (0 standard, 0.5 medium, 1 maximum contrast / WCAG AAA). |
persistToLocalStorage | boolean | false | Saves and restores theme preferences from localStorage. |
Scheme Variants & Contrast Levels
Bug On MD3 Expressive integrates with @material/material-color-utilities ^0.4.0 using MD3 Expressive Spec 2025 (specVersion: '2025').
Scheme Variants (variant)
"expressive"(Default) — MD3 Expressive 2025 palette with vibrant, high-chroma tones for hero moments and expressive UI elements."tonal_spot"— Standard Android 12+ Material 3 palette with balanced chroma."vibrant"— Maximizes colorfulness across primary, secondary, and tertiary tones."fidelity"— Keeps generated colors tightly matched to the original seed color."content"— Tailored for content-centric surfaces (e.g., photo viewers, reading apps)."monochrome"— Grayscale scheme using purely neutral tones."neutral"— Low-chroma scheme with subtle accent tones.
Contrast Levels (contrastLevel)
0(Default) — Standard contrast level.0.5— Medium contrast, providing clearer boundaries between containers and text.1— High contrast, meeting WCAG AAA accessibility requirements.
Theme Hooks
useTheme
Returns full theme state, including current source color, mode, active scheme variant, contrast level, and palette mutators.
import { useTheme } from "@bug-on/m3-expressive/core";
function ColorPicker() {
const { sourceColor, setSourceColor, mode, setMode, effectiveMode, variant, contrastLevel } = useTheme();
return (
<div>
<input
type="color"
value={sourceColor}
onChange={(e) => setSourceColor(e.target.value)}
/>
<button onClick={() => setMode(effectiveMode === "dark" ? "light" : "dark")}>
Mode: {effectiveMode} (Variant: {variant})
</button>
</div>
);
}
useThemeMode
Utility hook for managing Light/Dark mode transitions exclusively.
import { useThemeMode } from "@bug-on/m3-expressive/core";
function ThemeToggle() {
const { mode, setMode, effectiveMode } = useThemeMode();
const isDark = effectiveMode === "dark";
return (
<button onClick={() => setMode(isDark ? "light" : "dark")}>
{isDark ? "Switch to Light Mode" : "Switch to Dark Mode"}
</button>
);
}
How Design Tokens & Tailwind v4 Integration Work
@bug-on/m3-expressive/index.css integrates design tokens at three distinct layers:
┌────────────────────────────────────────────────────────┐
│ 1. Raw Tokens & Variable CSS Defaults │
│ (--md-sys-color-primary, --md-sys-shape-corner-xl) │
└───────────────────────────┬────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 2. Tailwind v4 @theme Block Mapping │
│ (--color-m3-primary: var(--md-sys-color-primary)) │
└───────────────────────────┬────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 3. Tailwind Utility Classes │
│ (bg-m3-primary, text-m3-on-surface, rounded-m3-xl) │
└────────────────────────────────────────────────────────┘
Dynamic Runtime Color Injection
When MD3ThemeProvider mounts, it computes the MD3 palette based on sourceColor and updates CSS variables on :root dynamically. Because Tailwind v4 utility classes reference these CSS variables, all components re-render with the new palette instantly without full page reloads.
Overriding Tokens in CSS
Because token mappings use Tailwind v4 @theme, you can override specific color roles or radii in your application's globals.css:
@import "tailwindcss";
@import "@bug-on/m3-expressive/index.css";
@theme {
/* Override a static color role */
--color-m3-primary: #1e40af;
/* Override a custom shape radius */
--radius-m3-xl: 32px;
}