MD3
Expressive
MATERIAL DESIGN 3 EXPRESSIVE

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):

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:

tsx
<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.

tsx
<MD3ThemeProvider motion="auto">
  {children}
</MD3ThemeProvider>
  • "auto" (default) follows the device's prefers-reduced-motion setting.
  • "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:

tsx
<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

PropTypeDefaultDescription
sourceColorstring#6750A4Primary hex color used to derive the entire MD3 palette.
defaultModeThemeMode"light"Initial theme mode ("light", "dark", or "system").
variantSchemeVariant"expressive"Color scheme variant ("expressive", "tonal_spot", "vibrant", "fidelity", "content", "monochrome", "neutral").
contrastLevelContrastLevel0Contrast level for generated scheme (0 standard, 0.5 medium, 1 maximum contrast / WCAG AAA).
persistToLocalStoragebooleanfalseSaves 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.

tsx
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.

tsx
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:

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

LevelDepth / PurposeCSS Variable (@bug-on/m3-tokens)Tailwind ClassUtility Class
Level 0Flat surface (0dp), no shadow--md-sys-elevation-level0shadow-m3-elevation-0elevation-0
Level 1Standard cards, bottom sheets (1dp)--md-sys-elevation-level1shadow-m3-elevation-1elevation-1
Level 2Menus, popovers, bottom app bar (3dp)--md-sys-elevation-level2shadow-m3-elevation-2elevation-2
Level 3Search bars, dialogs, pickers (6dp)--md-sys-elevation-level3shadow-m3-elevation-3elevation-3
Level 4Hovered cards, floating toolbars (8dp)--md-sys-elevation-level4shadow-m3-elevation-4elevation-4
Level 5Modal dialogs, FABs in flight (12dp)--md-sys-elevation-level5shadow-m3-elevation-5elevation-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:

css
@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 (applies elevation-0).
  • elevation: Adjusts the level dynamically (0 | 1 | 2 | 3 | 4 | 5).
tsx
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:

tsx
// cn() automatically resolves the conflict and drops elevation-3
<Search className="shadow-none border border-m3-outline" />