13 / 14
All 14 in 14 Material Design Components in Tailwind CSS
- 01Tailwind Material Design 3 Buttons
- 02Tailwind Material Design 3 Cards
- 03Tailwind Material Design Navigation Bar, Rail and Drawer
- 04Tailwind Material Design Text Fields and Form Controls
- 05Tailwind Material Design Dialog, Bottom Sheet and Snackbar
- 06Tailwind Material Design Data Table
- 07Tailwind Material Design Tabs with Sliding Indicator
- 08Tailwind Material Design Chips with Live Filtering
- 09Tailwind Material Design Stepper and Checkout Flow
- 10Tailwind Material Design Tooltips, Progress and Badges
- 11Tailwind Material Design Expansion Panels and Accordion
- 12Tailwind Material Design Lists and Swipeable Rows
- 13Tailwind Material Design Color Palette and Theme Switcher
- 14Tailwind Material Design Responsive App Shell Layout
Tailwind Material Design Color Palette and Theme Switcher
The M3 colour system laid out as role pairs — primary / on-primary, container / on-container, surface tiers, error — with four themes switchable by radio and no JavaScript. It is also the collection's one honest lesson about v4: @theme tokens are build-time, so a runtime theme swap uses plain custom properties consumed as bg-[var(--p)], and knowing which to reach for is the whole skill.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tmd-13 w-full min-h-svh bg-tmd13-bg font-tmd13-sans text-tmd13-onsurface px-5 py-9" aria-label="Material Design colour roles and theme switcher demo">
<div class="w-[min(960px,100%)] mx-auto grid gap-6 [--p:#6750a4] [--on-p:#ffffff] [--pc:#eaddff] [--on-pc:#21005d] [--s:#625b71] [--sc:#e8def8] [--t:#7d5260] [--tc:#ffd8e4] has-[#tmd13-teal:checked]:[--p:#006874] has-[#tmd13-teal:checked]:[--pc:#a2d0d8] has-[#tmd13-teal:checked]:[--on-pc:#001f25] has-[#tmd13-teal:checked]:[--s:#4a6267] has-[#tmd13-teal:checked]:[--sc:#cce8ed] has-[#tmd13-teal:checked]:[--t:#525e7d] has-[#tmd13-teal:checked]:[--tc:#dae2ff] has-[#tmd13-rose:checked]:[--p:#b5271f] has-[#tmd13-rose:checked]:[--pc:#ffdad6] has-[#tmd13-rose:checked]:[--on-pc:#410002] has-[#tmd13-rose:checked]:[--s:#775652] has-[#tmd13-rose:checked]:[--sc:#ffdad6] has-[#tmd13-rose:checked]:[--t:#6c5d2f] has-[#tmd13-rose:checked]:[--tc:#f6e1a6] has-[#tmd13-forest:checked]:[--p:#1f6b34] has-[#tmd13-forest:checked]:[--pc:#b8f2be] has-[#tmd13-forest:checked]:[--on-pc:#00210c] has-[#tmd13-forest:checked]:[--s:#51634f] has-[#tmd13-forest:checked]:[--sc:#d4e8cf] has-[#tmd13-forest:checked]:[--t:#39656b] has-[#tmd13-forest:checked]:[--tc:#bcebf2] transition-[background-color] duration-300">
<header class="grid gap-1.5">
<h2 class="text-[26px] font-medium tracking-[-0.01em]">Colour roles & runtime themes</h2>
<p class="text-sm text-tmd13-onvariant text-pretty"><code class="font-tmd13-mono text-[12px]">@theme</code> tokens are build-time; a switcher needs plain custom properties read as <code class="font-tmd13-mono text-[12px]">bg-[var(--p)]</code>. Both are here, doing the jobs they are good at.</p>
</header>
<fieldset class="border-0 p-0 m-0">
<legend class="mb-2 font-tmd13-mono text-[11px] uppercase tracking-[0.1em] text-tmd13-onvariant">Theme</legend>
<div class="flex flex-wrap gap-2">
<input type="radio" name="tmd13-theme" id="tmd13-purple" class="peer/pu sr-only" checked>
<label for="tmd13-purple" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/pu:bg-[var(--p)] peer-checked/pu:text-[var(--on-p)] peer-checked/pu:border-transparent peer-focus-visible/pu:outline-2 peer-focus-visible/pu:outline-offset-2 peer-focus-visible/pu:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#6750a4]"></span>Purple</label>
<input type="radio" name="tmd13-theme" id="tmd13-teal" class="peer/te sr-only">
<label for="tmd13-teal" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/te:bg-[var(--p)] peer-checked/te:text-[var(--on-p)] peer-checked/te:border-transparent peer-focus-visible/te:outline-2 peer-focus-visible/te:outline-offset-2 peer-focus-visible/te:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#006874]"></span>Teal</label>
<input type="radio" name="tmd13-theme" id="tmd13-rose" class="peer/ro sr-only">
<label for="tmd13-rose" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/ro:bg-[var(--p)] peer-checked/ro:text-[var(--on-p)] peer-checked/ro:border-transparent peer-focus-visible/ro:outline-2 peer-focus-visible/ro:outline-offset-2 peer-focus-visible/ro:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#b5271f]"></span>Rose</label>
<input type="radio" name="tmd13-theme" id="tmd13-forest" class="peer/fo sr-only">
<label for="tmd13-forest" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/fo:bg-[var(--p)] peer-checked/fo:text-[var(--on-p)] peer-checked/fo:border-transparent peer-focus-visible/fo:outline-2 peer-focus-visible/fo:outline-offset-2 peer-focus-visible/fo:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#1f6b34]"></span>Forest</label>
</div>
</fieldset>
<div class="grid gap-3 grid-cols-[repeat(auto-fit,minmax(min(150px,100%),1fr))]">
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--p)] text-[var(--on-p)] transition-[background-color,color] duration-300"><b class="text-[13px] font-medium">Primary</b><span class="font-tmd13-mono text-[11px] opacity-80">on-primary</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--pc)] text-[var(--on-pc)] transition-[background-color,color] duration-300"><b class="text-[13px] font-medium">Primary container</b><span class="font-tmd13-mono text-[11px] opacity-80">on-primary-container</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--s)] text-white transition-[background-color] duration-300"><b class="text-[13px] font-medium">Secondary</b><span class="font-tmd13-mono text-[11px] opacity-80">on-secondary</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--sc)] text-[#1d192b] transition-[background-color] duration-300"><b class="text-[13px] font-medium">Secondary container</b><span class="font-tmd13-mono text-[11px] opacity-70">on-secondary-container</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--t)] text-white transition-[background-color] duration-300"><b class="text-[13px] font-medium">Tertiary</b><span class="font-tmd13-mono text-[11px] opacity-80">on-tertiary</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--tc)] text-[#31111d] transition-[background-color] duration-300"><b class="text-[13px] font-medium">Tertiary container</b><span class="font-tmd13-mono text-[11px] opacity-70">on-tertiary-container</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-tmd13-error text-white"><b class="text-[13px] font-medium">Error</b><span class="font-tmd13-mono text-[11px] opacity-80">on-error</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-tmd13-surface text-tmd13-onsurface border border-tmd13-outlinevariant"><b class="text-[13px] font-medium">Surface</b><span class="font-tmd13-mono text-[11px] text-tmd13-onvariant">on-surface</span></div>
</div>
<div class="grid gap-4 p-5.5 rounded-3xl bg-tmd13-surface border border-tmd13-outlinevariant">
<span class="font-tmd13-mono text-[11px] uppercase tracking-[0.1em] text-tmd13-onvariant">Tonal ramp from the live seed — color-mix()</span>
<div class="grid grid-cols-[repeat(auto-fit,minmax(64px,1fr))] gap-1 overflow-hidden rounded-xl">
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_12%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_28%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_46%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_68%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[var(--p)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_78%,black)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_58%,black)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_34%,black)] transition-[background-color] duration-300"></div>
</div>
<p class="text-sm leading-6 text-tmd13-onvariant text-pretty">Eight tone stops from one seed. M3 derives its whole palette this way, which is why a theme switcher only needs to move one value.</p>
</div>
<div class="grid gap-3 p-5.5 rounded-3xl bg-tmd13-surface border border-tmd13-outlinevariant">
<span class="font-tmd13-mono text-[11px] uppercase tracking-[0.1em] text-tmd13-onvariant">Components under the live theme</span>
<div class="flex flex-wrap items-center gap-3">
<button type="button" class="relative overflow-hidden inline-flex items-center gap-2 min-h-10 px-6 rounded-full border-0 bg-[var(--p)] text-[var(--on-p)] cursor-pointer text-sm font-medium transition-[background-color] duration-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--p)] before:absolute before:inset-0 before:bg-[color-mix(in_oklab,var(--on-p)_8%,transparent)] before:opacity-0 before:transition-opacity hover:before:opacity-100"><span class="font-tmd13-icon text-[18px] leading-none" aria-hidden="true">palette</span>Filled</button>
<button type="button" class="relative overflow-hidden inline-flex items-center min-h-10 px-6 rounded-full bg-[var(--pc)] text-[var(--on-pc)] border-0 cursor-pointer text-sm font-medium transition-[background-color] duration-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--p)] before:absolute before:inset-0 before:bg-[color-mix(in_oklab,var(--on-pc)_8%,transparent)] before:opacity-0 before:transition-opacity hover:before:opacity-100">Tonal</button>
<span class="inline-flex items-center gap-2 h-8 px-3 rounded-lg bg-[var(--tc)] text-[#31111d] text-xs font-medium transition-[background-color] duration-300">Tertiary chip</span>
<span class="inline-flex items-center h-8 px-3 rounded-lg bg-[color-mix(in_oklab,var(--p)_12%,transparent)] text-[var(--p)] text-xs font-medium">12% state layer</span>
</div>
</div>
</div>
</section><section class="tmd-13 w-full min-h-svh bg-tmd13-bg font-tmd13-sans text-tmd13-onsurface px-5 py-9" aria-label="Material Design colour roles and theme switcher demo">
<div class="w-[min(960px,100%)] mx-auto grid gap-6 [--p:#6750a4] [--on-p:#ffffff] [--pc:#eaddff] [--on-pc:#21005d] [--s:#625b71] [--sc:#e8def8] [--t:#7d5260] [--tc:#ffd8e4] has-[#tmd13-teal:checked]:[--p:#006874] has-[#tmd13-teal:checked]:[--pc:#a2d0d8] has-[#tmd13-teal:checked]:[--on-pc:#001f25] has-[#tmd13-teal:checked]:[--s:#4a6267] has-[#tmd13-teal:checked]:[--sc:#cce8ed] has-[#tmd13-teal:checked]:[--t:#525e7d] has-[#tmd13-teal:checked]:[--tc:#dae2ff] has-[#tmd13-rose:checked]:[--p:#b5271f] has-[#tmd13-rose:checked]:[--pc:#ffdad6] has-[#tmd13-rose:checked]:[--on-pc:#410002] has-[#tmd13-rose:checked]:[--s:#775652] has-[#tmd13-rose:checked]:[--sc:#ffdad6] has-[#tmd13-rose:checked]:[--t:#6c5d2f] has-[#tmd13-rose:checked]:[--tc:#f6e1a6] has-[#tmd13-forest:checked]:[--p:#1f6b34] has-[#tmd13-forest:checked]:[--pc:#b8f2be] has-[#tmd13-forest:checked]:[--on-pc:#00210c] has-[#tmd13-forest:checked]:[--s:#51634f] has-[#tmd13-forest:checked]:[--sc:#d4e8cf] has-[#tmd13-forest:checked]:[--t:#39656b] has-[#tmd13-forest:checked]:[--tc:#bcebf2] transition-[background-color] duration-300">
<header class="grid gap-1.5">
<h2 class="text-[26px] font-medium tracking-[-0.01em]">Colour roles & runtime themes</h2>
<p class="text-sm text-tmd13-onvariant text-pretty"><code class="font-tmd13-mono text-[12px]">@theme</code> tokens are build-time; a switcher needs plain custom properties read as <code class="font-tmd13-mono text-[12px]">bg-[var(--p)]</code>. Both are here, doing the jobs they are good at.</p>
</header>
<fieldset class="border-0 p-0 m-0">
<legend class="mb-2 font-tmd13-mono text-[11px] uppercase tracking-[0.1em] text-tmd13-onvariant">Theme</legend>
<div class="flex flex-wrap gap-2">
<input type="radio" name="tmd13-theme" id="tmd13-purple" class="peer/pu sr-only" checked>
<label for="tmd13-purple" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/pu:bg-[var(--p)] peer-checked/pu:text-[var(--on-p)] peer-checked/pu:border-transparent peer-focus-visible/pu:outline-2 peer-focus-visible/pu:outline-offset-2 peer-focus-visible/pu:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#6750a4]"></span>Purple</label>
<input type="radio" name="tmd13-theme" id="tmd13-teal" class="peer/te sr-only">
<label for="tmd13-teal" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/te:bg-[var(--p)] peer-checked/te:text-[var(--on-p)] peer-checked/te:border-transparent peer-focus-visible/te:outline-2 peer-focus-visible/te:outline-offset-2 peer-focus-visible/te:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#006874]"></span>Teal</label>
<input type="radio" name="tmd13-theme" id="tmd13-rose" class="peer/ro sr-only">
<label for="tmd13-rose" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/ro:bg-[var(--p)] peer-checked/ro:text-[var(--on-p)] peer-checked/ro:border-transparent peer-focus-visible/ro:outline-2 peer-focus-visible/ro:outline-offset-2 peer-focus-visible/ro:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#b5271f]"></span>Rose</label>
<input type="radio" name="tmd13-theme" id="tmd13-forest" class="peer/fo sr-only">
<label for="tmd13-forest" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/fo:bg-[var(--p)] peer-checked/fo:text-[var(--on-p)] peer-checked/fo:border-transparent peer-focus-visible/fo:outline-2 peer-focus-visible/fo:outline-offset-2 peer-focus-visible/fo:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#1f6b34]"></span>Forest</label>
</div>
</fieldset>
<div class="grid gap-3 grid-cols-[repeat(auto-fit,minmax(min(150px,100%),1fr))]">
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--p)] text-[var(--on-p)] transition-[background-color,color] duration-300"><b class="text-[13px] font-medium">Primary</b><span class="font-tmd13-mono text-[11px] opacity-80">on-primary</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--pc)] text-[var(--on-pc)] transition-[background-color,color] duration-300"><b class="text-[13px] font-medium">Primary container</b><span class="font-tmd13-mono text-[11px] opacity-80">on-primary-container</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--s)] text-white transition-[background-color] duration-300"><b class="text-[13px] font-medium">Secondary</b><span class="font-tmd13-mono text-[11px] opacity-80">on-secondary</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--sc)] text-[#1d192b] transition-[background-color] duration-300"><b class="text-[13px] font-medium">Secondary container</b><span class="font-tmd13-mono text-[11px] opacity-70">on-secondary-container</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--t)] text-white transition-[background-color] duration-300"><b class="text-[13px] font-medium">Tertiary</b><span class="font-tmd13-mono text-[11px] opacity-80">on-tertiary</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--tc)] text-[#31111d] transition-[background-color] duration-300"><b class="text-[13px] font-medium">Tertiary container</b><span class="font-tmd13-mono text-[11px] opacity-70">on-tertiary-container</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-tmd13-error text-white"><b class="text-[13px] font-medium">Error</b><span class="font-tmd13-mono text-[11px] opacity-80">on-error</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-tmd13-surface text-tmd13-onsurface border border-tmd13-outlinevariant"><b class="text-[13px] font-medium">Surface</b><span class="font-tmd13-mono text-[11px] text-tmd13-onvariant">on-surface</span></div>
</div>
<div class="grid gap-4 p-5.5 rounded-3xl bg-tmd13-surface border border-tmd13-outlinevariant">
<span class="font-tmd13-mono text-[11px] uppercase tracking-[0.1em] text-tmd13-onvariant">Tonal ramp from the live seed — color-mix()</span>
<div class="grid grid-cols-[repeat(auto-fit,minmax(64px,1fr))] gap-1 overflow-hidden rounded-xl">
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_12%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_28%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_46%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_68%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[var(--p)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_78%,black)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_58%,black)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_34%,black)] transition-[background-color] duration-300"></div>
</div>
<p class="text-sm leading-6 text-tmd13-onvariant text-pretty">Eight tone stops from one seed. M3 derives its whole palette this way, which is why a theme switcher only needs to move one value.</p>
</div>
<div class="grid gap-3 p-5.5 rounded-3xl bg-tmd13-surface border border-tmd13-outlinevariant">
<span class="font-tmd13-mono text-[11px] uppercase tracking-[0.1em] text-tmd13-onvariant">Components under the live theme</span>
<div class="flex flex-wrap items-center gap-3">
<button type="button" class="relative overflow-hidden inline-flex items-center gap-2 min-h-10 px-6 rounded-full border-0 bg-[var(--p)] text-[var(--on-p)] cursor-pointer text-sm font-medium transition-[background-color] duration-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--p)] before:absolute before:inset-0 before:bg-[color-mix(in_oklab,var(--on-p)_8%,transparent)] before:opacity-0 before:transition-opacity hover:before:opacity-100"><span class="font-tmd13-icon text-[18px] leading-none" aria-hidden="true">palette</span>Filled</button>
<button type="button" class="relative overflow-hidden inline-flex items-center min-h-10 px-6 rounded-full bg-[var(--pc)] text-[var(--on-pc)] border-0 cursor-pointer text-sm font-medium transition-[background-color] duration-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--p)] before:absolute before:inset-0 before:bg-[color-mix(in_oklab,var(--on-pc)_8%,transparent)] before:opacity-0 before:transition-opacity hover:before:opacity-100">Tonal</button>
<span class="inline-flex items-center gap-2 h-8 px-3 rounded-lg bg-[var(--tc)] text-[#31111d] text-xs font-medium transition-[background-color] duration-300">Tertiary chip</span>
<span class="inline-flex items-center h-8 px-3 rounded-lg bg-[color-mix(in_oklab,var(--p)_12%,transparent)] text-[var(--p)] text-xs font-medium">12% state layer</span>
</div>
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-tmd13-bg: #f7f4fb;
--color-tmd13-bg: oklch(0.971 0.01 305.4);
--color-tmd13-surface: #fffbfe;
--color-tmd13-surface: oklch(0.992 0.006 334);
--color-tmd13-onsurface: #1c1b1f;
--color-tmd13-onsurface: oklch(0.225 0.008 297.2);
--color-tmd13-onvariant: #4a4458;
--color-tmd13-onvariant: oklch(0.401 0.034 298.6);
--color-tmd13-outlinevariant: #d8d2e0;
--color-tmd13-outlinevariant: oklch(0.873 0.02 305.2);
--color-tmd13-error: #b3261e;
--color-tmd13-error: oklch(0.501 0.178 28.7);
--font-tmd13-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
--font-tmd13-mono: 'Roboto Mono', ui-monospace, monospace;
--font-tmd13-icon: 'Material Symbols Outlined';
}
.tmd-13 {
width: 100%;
min-height: 100svh;
}
.tmd-13 .font-tmd13-icon {
font-feature-settings: 'liga';
}@import "tailwindcss";
@theme {
--color-tmd13-bg: #f7f4fb;
--color-tmd13-bg: oklch(0.971 0.01 305.4);
--color-tmd13-surface: #fffbfe;
--color-tmd13-surface: oklch(0.992 0.006 334);
--color-tmd13-onsurface: #1c1b1f;
--color-tmd13-onsurface: oklch(0.225 0.008 297.2);
--color-tmd13-onvariant: #4a4458;
--color-tmd13-onvariant: oklch(0.401 0.034 298.6);
--color-tmd13-outlinevariant: #d8d2e0;
--color-tmd13-outlinevariant: oklch(0.873 0.02 305.2);
--color-tmd13-error: #b3261e;
--color-tmd13-error: oklch(0.501 0.178 28.7);
--font-tmd13-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
--font-tmd13-mono: 'Roboto Mono', ui-monospace, monospace;
--font-tmd13-icon: 'Material Symbols Outlined';
}
.tmd-13 {
width: 100%;
min-height: 100svh;
}
.tmd-13 .font-tmd13-icon {
font-feature-settings: 'liga';
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tmd13-bg: #f7f4fb;
--color-tmd13-bg: oklch(0.971 0.01 305.4);
--color-tmd13-surface: #fffbfe;
--color-tmd13-surface: oklch(0.992 0.006 334);
--color-tmd13-onsurface: #1c1b1f;
--color-tmd13-onsurface: oklch(0.225 0.008 297.2);
--color-tmd13-onvariant: #4a4458;
--color-tmd13-onvariant: oklch(0.401 0.034 298.6);
--color-tmd13-outlinevariant: #d8d2e0;
--color-tmd13-outlinevariant: oklch(0.873 0.02 305.2);
--color-tmd13-error: #b3261e;
--color-tmd13-error: oklch(0.501 0.178 28.7);
--font-tmd13-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
--font-tmd13-mono: 'Roboto Mono', ui-monospace, monospace;
--font-tmd13-icon: 'Material Symbols Outlined';
}
.tmd-13 {
width: 100%;
min-height: 100svh;
}
.tmd-13 .font-tmd13-icon {
font-feature-settings: 'liga';
}@import "tailwindcss";
@theme {
--color-tmd13-bg: #f7f4fb;
--color-tmd13-bg: oklch(0.971 0.01 305.4);
--color-tmd13-surface: #fffbfe;
--color-tmd13-surface: oklch(0.992 0.006 334);
--color-tmd13-onsurface: #1c1b1f;
--color-tmd13-onsurface: oklch(0.225 0.008 297.2);
--color-tmd13-onvariant: #4a4458;
--color-tmd13-onvariant: oklch(0.401 0.034 298.6);
--color-tmd13-outlinevariant: #d8d2e0;
--color-tmd13-outlinevariant: oklch(0.873 0.02 305.2);
--color-tmd13-error: #b3261e;
--color-tmd13-error: oklch(0.501 0.178 28.7);
--font-tmd13-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
--font-tmd13-mono: 'Roboto Mono', ui-monospace, monospace;
--font-tmd13-icon: 'Material Symbols Outlined';
}
.tmd-13 {
width: 100%;
min-height: 100svh;
}
.tmd-13 .font-tmd13-icon {
font-feature-settings: 'liga';
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,300..700&family=Roboto+Mono:wght@400;500&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0..1,0&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,300..700&family=Roboto+Mono:wght@400;500&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0..1,0&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The demo's real subject is the boundary between
@themeand runtime variables. Fixed scale values belong in@theme; anything the user switches while the page is open must be a plain custom property read throughvar(). - Setting
--color-tmd13-primaryon an element does NOT rethemebg-tmd13-primaryin v4. That misconception is worth a gotcha of its own. - State layers survive an unknown theme because they are
color-mix()of the liveon-colour rather than a fixed tint per variant. - The tonal ramp is eight
color-mix()utilities on one seed, which is how M3 actually generates a palette — the CSS version hard-coded four full palettes instead. - Every swatch prints its
on-pair, because a colour role without its contrast partner is not a usable token. - Arbitrary
var()colours lose Tailwind's opacity modifier syntax, so alpha must be written as an explicit mix. That trade is the price of runtime theming.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Material Design Color Palette and Theme Switcher
Source: https://codefronts.com/design-styles/tailwind-material-design-components/tailwind-material-design-color-palette-theme-switcher/
The M3 colour system laid out as role pairs — primary / on-primary, container / on-container, surface tiers, error — with four themes switchable by radio and no JavaScript. It is also the collection's one honest lesson about v4: @theme tokens are build-time, so a runtime theme swap uses plain custom properties consumed as bg-[var(--p)], and knowing which to reach for is the whole skill.
## HTML
```html
<section class="tmd-13 w-full min-h-svh bg-tmd13-bg font-tmd13-sans text-tmd13-onsurface px-5 py-9" aria-label="Material Design colour roles and theme switcher demo">
<div class="w-[min(960px,100%)] mx-auto grid gap-6 [--p:#6750a4] [--on-p:#ffffff] [--pc:#eaddff] [--on-pc:#21005d] [--s:#625b71] [--sc:#e8def8] [--t:#7d5260] [--tc:#ffd8e4] has-[#tmd13-teal:checked]:[--p:#006874] has-[#tmd13-teal:checked]:[--pc:#a2d0d8] has-[#tmd13-teal:checked]:[--on-pc:#001f25] has-[#tmd13-teal:checked]:[--s:#4a6267] has-[#tmd13-teal:checked]:[--sc:#cce8ed] has-[#tmd13-teal:checked]:[--t:#525e7d] has-[#tmd13-teal:checked]:[--tc:#dae2ff] has-[#tmd13-rose:checked]:[--p:#b5271f] has-[#tmd13-rose:checked]:[--pc:#ffdad6] has-[#tmd13-rose:checked]:[--on-pc:#410002] has-[#tmd13-rose:checked]:[--s:#775652] has-[#tmd13-rose:checked]:[--sc:#ffdad6] has-[#tmd13-rose:checked]:[--t:#6c5d2f] has-[#tmd13-rose:checked]:[--tc:#f6e1a6] has-[#tmd13-forest:checked]:[--p:#1f6b34] has-[#tmd13-forest:checked]:[--pc:#b8f2be] has-[#tmd13-forest:checked]:[--on-pc:#00210c] has-[#tmd13-forest:checked]:[--s:#51634f] has-[#tmd13-forest:checked]:[--sc:#d4e8cf] has-[#tmd13-forest:checked]:[--t:#39656b] has-[#tmd13-forest:checked]:[--tc:#bcebf2] transition-[background-color] duration-300">
<header class="grid gap-1.5">
<h2 class="text-[26px] font-medium tracking-[-0.01em]">Colour roles & runtime themes</h2>
<p class="text-sm text-tmd13-onvariant text-pretty"><code class="font-tmd13-mono text-[12px]">@theme</code> tokens are build-time; a switcher needs plain custom properties read as <code class="font-tmd13-mono text-[12px]">bg-[var(--p)]</code>. Both are here, doing the jobs they are good at.</p>
</header>
<fieldset class="border-0 p-0 m-0">
<legend class="mb-2 font-tmd13-mono text-[11px] uppercase tracking-[0.1em] text-tmd13-onvariant">Theme</legend>
<div class="flex flex-wrap gap-2">
<input type="radio" name="tmd13-theme" id="tmd13-purple" class="peer/pu sr-only" checked>
<label for="tmd13-purple" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/pu:bg-[var(--p)] peer-checked/pu:text-[var(--on-p)] peer-checked/pu:border-transparent peer-focus-visible/pu:outline-2 peer-focus-visible/pu:outline-offset-2 peer-focus-visible/pu:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#6750a4]"></span>Purple</label>
<input type="radio" name="tmd13-theme" id="tmd13-teal" class="peer/te sr-only">
<label for="tmd13-teal" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/te:bg-[var(--p)] peer-checked/te:text-[var(--on-p)] peer-checked/te:border-transparent peer-focus-visible/te:outline-2 peer-focus-visible/te:outline-offset-2 peer-focus-visible/te:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#006874]"></span>Teal</label>
<input type="radio" name="tmd13-theme" id="tmd13-rose" class="peer/ro sr-only">
<label for="tmd13-rose" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/ro:bg-[var(--p)] peer-checked/ro:text-[var(--on-p)] peer-checked/ro:border-transparent peer-focus-visible/ro:outline-2 peer-focus-visible/ro:outline-offset-2 peer-focus-visible/ro:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#b5271f]"></span>Rose</label>
<input type="radio" name="tmd13-theme" id="tmd13-forest" class="peer/fo sr-only">
<label for="tmd13-forest" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/fo:bg-[var(--p)] peer-checked/fo:text-[var(--on-p)] peer-checked/fo:border-transparent peer-focus-visible/fo:outline-2 peer-focus-visible/fo:outline-offset-2 peer-focus-visible/fo:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#1f6b34]"></span>Forest</label>
</div>
</fieldset>
<div class="grid gap-3 grid-cols-[repeat(auto-fit,minmax(min(150px,100%),1fr))]">
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--p)] text-[var(--on-p)] transition-[background-color,color] duration-300"><b class="text-[13px] font-medium">Primary</b><span class="font-tmd13-mono text-[11px] opacity-80">on-primary</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--pc)] text-[var(--on-pc)] transition-[background-color,color] duration-300"><b class="text-[13px] font-medium">Primary container</b><span class="font-tmd13-mono text-[11px] opacity-80">on-primary-container</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--s)] text-white transition-[background-color] duration-300"><b class="text-[13px] font-medium">Secondary</b><span class="font-tmd13-mono text-[11px] opacity-80">on-secondary</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--sc)] text-[#1d192b] transition-[background-color] duration-300"><b class="text-[13px] font-medium">Secondary container</b><span class="font-tmd13-mono text-[11px] opacity-70">on-secondary-container</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--t)] text-white transition-[background-color] duration-300"><b class="text-[13px] font-medium">Tertiary</b><span class="font-tmd13-mono text-[11px] opacity-80">on-tertiary</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--tc)] text-[#31111d] transition-[background-color] duration-300"><b class="text-[13px] font-medium">Tertiary container</b><span class="font-tmd13-mono text-[11px] opacity-70">on-tertiary-container</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-tmd13-error text-white"><b class="text-[13px] font-medium">Error</b><span class="font-tmd13-mono text-[11px] opacity-80">on-error</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-tmd13-surface text-tmd13-onsurface border border-tmd13-outlinevariant"><b class="text-[13px] font-medium">Surface</b><span class="font-tmd13-mono text-[11px] text-tmd13-onvariant">on-surface</span></div>
</div>
<div class="grid gap-4 p-5.5 rounded-3xl bg-tmd13-surface border border-tmd13-outlinevariant">
<span class="font-tmd13-mono text-[11px] uppercase tracking-[0.1em] text-tmd13-onvariant">Tonal ramp from the live seed — color-mix()</span>
<div class="grid grid-cols-[repeat(auto-fit,minmax(64px,1fr))] gap-1 overflow-hidden rounded-xl">
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_12%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_28%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_46%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_68%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[var(--p)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_78%,black)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_58%,black)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_34%,black)] transition-[background-color] duration-300"></div>
</div>
<p class="text-sm leading-6 text-tmd13-onvariant text-pretty">Eight tone stops from one seed. M3 derives its whole palette this way, which is why a theme switcher only needs to move one value.</p>
</div>
<div class="grid gap-3 p-5.5 rounded-3xl bg-tmd13-surface border border-tmd13-outlinevariant">
<span class="font-tmd13-mono text-[11px] uppercase tracking-[0.1em] text-tmd13-onvariant">Components under the live theme</span>
<div class="flex flex-wrap items-center gap-3">
<button type="button" class="relative overflow-hidden inline-flex items-center gap-2 min-h-10 px-6 rounded-full border-0 bg-[var(--p)] text-[var(--on-p)] cursor-pointer text-sm font-medium transition-[background-color] duration-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--p)] before:absolute before:inset-0 before:bg-[color-mix(in_oklab,var(--on-p)_8%,transparent)] before:opacity-0 before:transition-opacity hover:before:opacity-100"><span class="font-tmd13-icon text-[18px] leading-none" aria-hidden="true">palette</span>Filled</button>
<button type="button" class="relative overflow-hidden inline-flex items-center min-h-10 px-6 rounded-full bg-[var(--pc)] text-[var(--on-pc)] border-0 cursor-pointer text-sm font-medium transition-[background-color] duration-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--p)] before:absolute before:inset-0 before:bg-[color-mix(in_oklab,var(--on-pc)_8%,transparent)] before:opacity-0 before:transition-opacity hover:before:opacity-100">Tonal</button>
<span class="inline-flex items-center gap-2 h-8 px-3 rounded-lg bg-[var(--tc)] text-[#31111d] text-xs font-medium transition-[background-color] duration-300">Tertiary chip</span>
<span class="inline-flex items-center h-8 px-3 rounded-lg bg-[color-mix(in_oklab,var(--p)_12%,transparent)] text-[var(--p)] text-xs font-medium">12% state layer</span>
</div>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tmd13-bg: #f7f4fb;
--color-tmd13-bg: oklch(0.971 0.01 305.4);
--color-tmd13-surface: #fffbfe;
--color-tmd13-surface: oklch(0.992 0.006 334);
--color-tmd13-onsurface: #1c1b1f;
--color-tmd13-onsurface: oklch(0.225 0.008 297.2);
--color-tmd13-onvariant: #4a4458;
--color-tmd13-onvariant: oklch(0.401 0.034 298.6);
--color-tmd13-outlinevariant: #d8d2e0;
--color-tmd13-outlinevariant: oklch(0.873 0.02 305.2);
--color-tmd13-error: #b3261e;
--color-tmd13-error: oklch(0.501 0.178 28.7);
--font-tmd13-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
--font-tmd13-mono: 'Roboto Mono', ui-monospace, monospace;
--font-tmd13-icon: 'Material Symbols Outlined';
}
.tmd-13 {
width: 100%;
min-height: 100svh;
}
.tmd-13 .font-tmd13-icon {
font-feature-settings: 'liga';
}
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Material Design Color Palette and Theme Switcher
Source: https://codefronts.com/design-styles/tailwind-material-design-components/tailwind-material-design-color-palette-theme-switcher/
The M3 colour system laid out as role pairs — primary / on-primary, container / on-container, surface tiers, error — with four themes switchable by radio and no JavaScript. It is also the collection's one honest lesson about v4: @theme tokens are build-time, so a runtime theme swap uses plain custom properties consumed as bg-[var(--p)], and knowing which to reach for is the whole skill.
## HTML
```html
<section class="tmd-13 w-full min-h-svh bg-tmd13-bg font-tmd13-sans text-tmd13-onsurface px-5 py-9" aria-label="Material Design colour roles and theme switcher demo">
<div class="w-[min(960px,100%)] mx-auto grid gap-6 [--p:#6750a4] [--on-p:#ffffff] [--pc:#eaddff] [--on-pc:#21005d] [--s:#625b71] [--sc:#e8def8] [--t:#7d5260] [--tc:#ffd8e4] has-[#tmd13-teal:checked]:[--p:#006874] has-[#tmd13-teal:checked]:[--pc:#a2d0d8] has-[#tmd13-teal:checked]:[--on-pc:#001f25] has-[#tmd13-teal:checked]:[--s:#4a6267] has-[#tmd13-teal:checked]:[--sc:#cce8ed] has-[#tmd13-teal:checked]:[--t:#525e7d] has-[#tmd13-teal:checked]:[--tc:#dae2ff] has-[#tmd13-rose:checked]:[--p:#b5271f] has-[#tmd13-rose:checked]:[--pc:#ffdad6] has-[#tmd13-rose:checked]:[--on-pc:#410002] has-[#tmd13-rose:checked]:[--s:#775652] has-[#tmd13-rose:checked]:[--sc:#ffdad6] has-[#tmd13-rose:checked]:[--t:#6c5d2f] has-[#tmd13-rose:checked]:[--tc:#f6e1a6] has-[#tmd13-forest:checked]:[--p:#1f6b34] has-[#tmd13-forest:checked]:[--pc:#b8f2be] has-[#tmd13-forest:checked]:[--on-pc:#00210c] has-[#tmd13-forest:checked]:[--s:#51634f] has-[#tmd13-forest:checked]:[--sc:#d4e8cf] has-[#tmd13-forest:checked]:[--t:#39656b] has-[#tmd13-forest:checked]:[--tc:#bcebf2] transition-[background-color] duration-300">
<header class="grid gap-1.5">
<h2 class="text-[26px] font-medium tracking-[-0.01em]">Colour roles & runtime themes</h2>
<p class="text-sm text-tmd13-onvariant text-pretty"><code class="font-tmd13-mono text-[12px]">@theme</code> tokens are build-time; a switcher needs plain custom properties read as <code class="font-tmd13-mono text-[12px]">bg-[var(--p)]</code>. Both are here, doing the jobs they are good at.</p>
</header>
<fieldset class="border-0 p-0 m-0">
<legend class="mb-2 font-tmd13-mono text-[11px] uppercase tracking-[0.1em] text-tmd13-onvariant">Theme</legend>
<div class="flex flex-wrap gap-2">
<input type="radio" name="tmd13-theme" id="tmd13-purple" class="peer/pu sr-only" checked>
<label for="tmd13-purple" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/pu:bg-[var(--p)] peer-checked/pu:text-[var(--on-p)] peer-checked/pu:border-transparent peer-focus-visible/pu:outline-2 peer-focus-visible/pu:outline-offset-2 peer-focus-visible/pu:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#6750a4]"></span>Purple</label>
<input type="radio" name="tmd13-theme" id="tmd13-teal" class="peer/te sr-only">
<label for="tmd13-teal" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/te:bg-[var(--p)] peer-checked/te:text-[var(--on-p)] peer-checked/te:border-transparent peer-focus-visible/te:outline-2 peer-focus-visible/te:outline-offset-2 peer-focus-visible/te:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#006874]"></span>Teal</label>
<input type="radio" name="tmd13-theme" id="tmd13-rose" class="peer/ro sr-only">
<label for="tmd13-rose" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/ro:bg-[var(--p)] peer-checked/ro:text-[var(--on-p)] peer-checked/ro:border-transparent peer-focus-visible/ro:outline-2 peer-focus-visible/ro:outline-offset-2 peer-focus-visible/ro:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#b5271f]"></span>Rose</label>
<input type="radio" name="tmd13-theme" id="tmd13-forest" class="peer/fo sr-only">
<label for="tmd13-forest" class="inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd13-outlinevariant cursor-pointer text-sm font-medium peer-checked/fo:bg-[var(--p)] peer-checked/fo:text-[var(--on-p)] peer-checked/fo:border-transparent peer-focus-visible/fo:outline-2 peer-focus-visible/fo:outline-offset-2 peer-focus-visible/fo:outline-[var(--p)] transition-colors duration-200"><span class="w-4 h-4 rounded-full bg-[#1f6b34]"></span>Forest</label>
</div>
</fieldset>
<div class="grid gap-3 grid-cols-[repeat(auto-fit,minmax(min(150px,100%),1fr))]">
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--p)] text-[var(--on-p)] transition-[background-color,color] duration-300"><b class="text-[13px] font-medium">Primary</b><span class="font-tmd13-mono text-[11px] opacity-80">on-primary</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--pc)] text-[var(--on-pc)] transition-[background-color,color] duration-300"><b class="text-[13px] font-medium">Primary container</b><span class="font-tmd13-mono text-[11px] opacity-80">on-primary-container</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--s)] text-white transition-[background-color] duration-300"><b class="text-[13px] font-medium">Secondary</b><span class="font-tmd13-mono text-[11px] opacity-80">on-secondary</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--sc)] text-[#1d192b] transition-[background-color] duration-300"><b class="text-[13px] font-medium">Secondary container</b><span class="font-tmd13-mono text-[11px] opacity-70">on-secondary-container</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--t)] text-white transition-[background-color] duration-300"><b class="text-[13px] font-medium">Tertiary</b><span class="font-tmd13-mono text-[11px] opacity-80">on-tertiary</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-[var(--tc)] text-[#31111d] transition-[background-color] duration-300"><b class="text-[13px] font-medium">Tertiary container</b><span class="font-tmd13-mono text-[11px] opacity-70">on-tertiary-container</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-tmd13-error text-white"><b class="text-[13px] font-medium">Error</b><span class="font-tmd13-mono text-[11px] opacity-80">on-error</span></div>
<div class="grid content-between gap-6 min-h-28 p-4 rounded-2xl bg-tmd13-surface text-tmd13-onsurface border border-tmd13-outlinevariant"><b class="text-[13px] font-medium">Surface</b><span class="font-tmd13-mono text-[11px] text-tmd13-onvariant">on-surface</span></div>
</div>
<div class="grid gap-4 p-5.5 rounded-3xl bg-tmd13-surface border border-tmd13-outlinevariant">
<span class="font-tmd13-mono text-[11px] uppercase tracking-[0.1em] text-tmd13-onvariant">Tonal ramp from the live seed — color-mix()</span>
<div class="grid grid-cols-[repeat(auto-fit,minmax(64px,1fr))] gap-1 overflow-hidden rounded-xl">
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_12%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_28%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_46%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_68%,white)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[var(--p)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_78%,black)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_58%,black)] transition-[background-color] duration-300"></div>
<div class="h-16 bg-[color-mix(in_oklab,var(--p)_34%,black)] transition-[background-color] duration-300"></div>
</div>
<p class="text-sm leading-6 text-tmd13-onvariant text-pretty">Eight tone stops from one seed. M3 derives its whole palette this way, which is why a theme switcher only needs to move one value.</p>
</div>
<div class="grid gap-3 p-5.5 rounded-3xl bg-tmd13-surface border border-tmd13-outlinevariant">
<span class="font-tmd13-mono text-[11px] uppercase tracking-[0.1em] text-tmd13-onvariant">Components under the live theme</span>
<div class="flex flex-wrap items-center gap-3">
<button type="button" class="relative overflow-hidden inline-flex items-center gap-2 min-h-10 px-6 rounded-full border-0 bg-[var(--p)] text-[var(--on-p)] cursor-pointer text-sm font-medium transition-[background-color] duration-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--p)] before:absolute before:inset-0 before:bg-[color-mix(in_oklab,var(--on-p)_8%,transparent)] before:opacity-0 before:transition-opacity hover:before:opacity-100"><span class="font-tmd13-icon text-[18px] leading-none" aria-hidden="true">palette</span>Filled</button>
<button type="button" class="relative overflow-hidden inline-flex items-center min-h-10 px-6 rounded-full bg-[var(--pc)] text-[var(--on-pc)] border-0 cursor-pointer text-sm font-medium transition-[background-color] duration-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--p)] before:absolute before:inset-0 before:bg-[color-mix(in_oklab,var(--on-pc)_8%,transparent)] before:opacity-0 before:transition-opacity hover:before:opacity-100">Tonal</button>
<span class="inline-flex items-center gap-2 h-8 px-3 rounded-lg bg-[var(--tc)] text-[#31111d] text-xs font-medium transition-[background-color] duration-300">Tertiary chip</span>
<span class="inline-flex items-center h-8 px-3 rounded-lg bg-[color-mix(in_oklab,var(--p)_12%,transparent)] text-[var(--p)] text-xs font-medium">12% state layer</span>
</div>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tmd13-bg: #f7f4fb;
--color-tmd13-bg: oklch(0.971 0.01 305.4);
--color-tmd13-surface: #fffbfe;
--color-tmd13-surface: oklch(0.992 0.006 334);
--color-tmd13-onsurface: #1c1b1f;
--color-tmd13-onsurface: oklch(0.225 0.008 297.2);
--color-tmd13-onvariant: #4a4458;
--color-tmd13-onvariant: oklch(0.401 0.034 298.6);
--color-tmd13-outlinevariant: #d8d2e0;
--color-tmd13-outlinevariant: oklch(0.873 0.02 305.2);
--color-tmd13-error: #b3261e;
--color-tmd13-error: oklch(0.501 0.178 28.7);
--font-tmd13-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
--font-tmd13-mono: 'Roboto Mono', ui-monospace, monospace;
--font-tmd13-icon: 'Material Symbols Outlined';
}
.tmd-13 {
width: 100%;
min-height: 100svh;
}
.tmd-13 .font-tmd13-icon {
font-feature-settings: 'liga';
}
```How this works
Two kinds of token, and they are not interchangeable. A @theme entry is compiled into utilities: it gives you bg-tmd13-primary, but the value is fixed at build time. A runtime switch therefore needs ordinary custom properties:
<div class="[--p:#6750a4] [--on-p:#fff]
has-[#tmd13-teal:checked]:[--p:#006874]
has-[#tmd13-rose:checked]:[--p:#b5271f]">
<div class="bg-[var(--p)] text-[var(--on-p)]">Primary</div>That is the right trade: @theme for the design system's fixed scale, arbitrary var() values for anything a user can change while the page is open.
The state layer still works over an unknown colour. Because the tint is color-mix(in oklab, var(--on-p) 8%, transparent), it composites correctly whether the live theme is purple, teal, rose or green — you never need a per-theme hover value.
Role pairs, not colours. Every swatch shows its on- colour as text sitting on it, because that is the only property of a colour role that matters: the pair is what guarantees contrast, and a palette that shows fills alone hides the one thing you must check.
Make it yours
- Add a theme by copying one radio and one
has-[#id:checked]:[--*]block; nothing else changes. - Seed your own palette: set
--pand derive the container and on-colours withcolor-mix()rather than hand-picking five hexes. - Increase the transition to 600ms for a demo-friendly theme fade, or remove it for instant switching.
- Extend the tonal ramp to M3's full 0–100 tone stops; the mix percentages are the tone numbers.
- For production, generate the
@themeentries from your seed and keep runtimevar()only for the user-switchable parts.
Gotchas — read before shipping
@themecannot be changed at runtime. Setting--color-tmd13-primaryon an element does not rethemebg-tmd13-primaryin v4 — this is the most common v4 misconception.- Arbitrary
var()values are invisible to the compiler's colour handling, so opacity modifiers (bg-[var(--p)]/50) will not work — mix explicitly instead. - Always ship the
on-colour with the fill. A theme switcher that changes only backgrounds will eventually produce unreadable text. color-mix(in oklab, …)andin srgbgive visibly different tints; pick one perceptual space and use it everywhere or the tints will not match.- Check every pair for 4.5:1 — the four themes here are all AA on their body text, and that is a property you verify, not assume.
- Transitioning
background-coloron hundreds of elements at once is expensive; keep the theme transition to the board, not to every child.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
color-mix() is Baseline 2023 and is part of Tailwind v4's own floor. light-dark() needs Chrome 123, Safari 17.5 and Firefox 120; where it is missing the explicit dark: utilities still apply.