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.
- ♿ Motion Policy — Applies one accessible motion policy to all library-authored effects.
- 🔔 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>
Motion Policy
Set motion once on the provider to control library-authored springs, morphs,
ripples, progress loops, blur, and programmatic scrolling. It does not modify
animations supplied by your own className, children, or Motion props.
<MD3ThemeProvider motion="auto">
{children}
</MD3ThemeProvider>
"auto"(default) follows the device'sprefers-reduced-motionsetting."always"enables library motion even when the device requests reduced motion."off"immediately resolves library effects to their static end state.
For a local override, use a component's motion prop where available or wrap a
subtree in MotionProvider from @bug-on/m3-expressive/core.
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. |
motion | "auto" | "always" | "off" | "auto" | Global policy for motion authored by this library. |
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 the --md-sys-color-* variables on :root. These are the dynamic source of truth. The bundled Tailwind v4 theme defines --color-m3-* once as CSS aliases to those system tokens, so the provider does not duplicate every alias in the inline style attribute.
Changing a source color from a picker updates React state immediately. Consecutive palette writes are coalesced to the latest value in the next animation frame, avoiding repeated color-scheme generation and DOM writes while a user drags. The first palette is applied immediately so the page does not briefly show the fallback scheme.
The public applyTheme() helper remains backwards compatible for standalone use: it writes both namespaces for callers that do not import @bug-on/m3-expressive/index.css.
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;
}
Elevation & Shadow System
Material Design 3 Expressive defines 6 distinct elevation levels (Level 0 through Level 5) to convey hierarchy, focus, and depth between interactive surfaces and their background.
Elevation Levels Reference
| Level | Depth / Purpose | CSS Variable (@bug-on/m3-tokens) | Tailwind Class | Utility Class |
|---|---|---|---|---|
| Level 0 | Flat surface (0dp), no shadow | --md-sys-elevation-level0 | shadow-m3-elevation-0 | elevation-0 |
| Level 1 | Standard cards, bottom sheets (1dp) | --md-sys-elevation-level1 | shadow-m3-elevation-1 | elevation-1 |
| Level 2 | Menus, popovers, bottom app bar (3dp) | --md-sys-elevation-level2 | shadow-m3-elevation-2 | elevation-2 |
| Level 3 | Search bars, dialogs, pickers (6dp) | --md-sys-elevation-level3 | shadow-m3-elevation-3 | elevation-3 |
| Level 4 | Hovered cards, floating toolbars (8dp) | --md-sys-elevation-level4 | shadow-m3-elevation-4 | elevation-4 |
| Level 5 | Modal dialogs, FABs in flight (12dp) | --md-sys-elevation-level5 | shadow-m3-elevation-5 | elevation-5 |
Monorepo Architecture: How Elevation is Distributed
Elevation tokens flow seamlessly from raw design tokens to final React components through three coordinated packages:
┌───────────────────────────────────────────────────────────────┐
│ 1. @bug-on/m3-tokens │
│ • CSS variables: --md-sys-elevation-level0..5 │
│ • JS exports: elevationTokens, ElevationLevel, ElevationKey │
└───────────────────────────────┬───────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 2. @bug-on/m3-tailwind │
│ • Tailwind CSS v4 @theme plugin injection: │
│ --shadow-m3-elevation-0..5: var(--md-sys-elevation-...) │
│ • Generates utility classes: shadow-m3-elevation-[0-5] │
└───────────────────────────────┬───────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 3. @bug-on/m3-expressive (Self-Contained) │
│ • Bundles elevation.css & @theme shadow into dist/index.css│
│ • Provides backward-compatible @utility elevation-[0-5] │
│ • Extended twMerge in cn() for seamless shadow override │
└───────────────────────────────────────────────────────────────┘
1. @bug-on/m3-tokens
The design tokens package defines the official MD3 multi-layered shadow values. Both camelCase and kebab-case CSS variables are provided for maximum compatibility:
- CSS file:
@bug-on/m3-tokens/elevation.css - TypeScript exports:
ts
import { elevationTokens, type ElevationLevel } from "@bug-on/m3-tokens"; console.log(elevationTokens.level3); // => "0px 1px 3px 0px rgba(0, 0, 0, 0.3), 0px 4px 8px 3px rgba(0, 0, 0, 0.15)"
2. @bug-on/m3-tailwind
Configures Tailwind CSS v4 @theme mappings, transforming token variables into standard Tailwind shadow utilities (shadow-m3-elevation-1 through shadow-m3-elevation-5).
3. @bug-on/m3-expressive (Self-Contained Bundle)
You do not need to manually install or import @bug-on/m3-tailwind for shadows to render properly. When you import the standard stylesheet:
@import "tailwindcss";
@import "@bug-on/m3-expressive/index.css";
All elevation variables and @utility elevation-[0-5] classes are bundled automatically.
Controlling & Disabling Elevation on Components
Components with default elevation (such as Search, Menu, BottomAppBar, DatePickerDialog, TimePickerDialog, and BottomSheetScaffold) provide first-class props to control or remove shadow elevation:
1. Using Component Props (disableElevation & elevation)
disableElevation: Completely removes the shadow drop (applieselevation-0).elevation: Adjusts the level dynamically (0 | 1 | 2 | 3 | 4 | 5).
import { Search } from "@bug-on/m3-expressive/navigation";
import { Menu, MenuTrigger, MenuContent, MenuItem } from "@bug-on/m3-expressive/navigation";
import { Button } from "@bug-on/m3-expressive/buttons";
// 1. Flat SearchBar with no shadow (ideal for embedded headers or bordered designs)
<Search disableElevation placeholder="Search..." />
// 2. Custom elevation level
<Search elevation={1} placeholder="Subtle elevation..." />
// 3. Bordered flat Menu
<Menu disableElevation>
<MenuTrigger>
<Button variant="outlined">Options</Button>
</MenuTrigger>
<MenuContent className="border border-m3-outline-variant">
<MenuItem>Settings</MenuItem>
<MenuItem>Profile</MenuItem>
</MenuContent>
</Menu>
2. Overriding via Tailwind Class (className="shadow-none")
The library's internal cn() utility is configured with custom twMerge conflict resolution. Passing Tailwind's shadow-none or any custom shadow class will cleanly override the component's default elevation-* class without CSS specificity conflicts:
// cn() automatically resolves the conflict and drops elevation-3
<Search className="shadow-none border border-m3-outline" />