Pure TailwindMIT licensed

Tailwind Material Design Navigation Bar, Rail and Drawer

The three M3 navigation containers side by side — bottom navigation bar, rail and standard drawer — all driven by radios and peer-checked:, so the active tab, its pill indicator and its filled icon all change with no JavaScript. The port's win is the indicator: an M3 active pill is one peer-checked: background on the icon box, not a ::before rule per item.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tmd-03 w-full min-h-svh bg-tmd03-bg font-tmd03-sans text-tmd03-onsurface px-5 py-9" aria-label="Material Design navigation containers demo">
  <div class="w-[min(1100px,100%)] mx-auto grid gap-6">
    <header class="grid gap-1.5">
      <h2 class="text-[26px] font-medium tracking-[-0.01em]">Navigation bar, rail &amp; drawer</h2>
      <p class="text-sm text-tmd03-onvariant">One item recipe, three containers. Radios carry the state; <code class="font-tmd03-mono text-[12px]">peer-checked:</code> carries the look.</p>
    </header>

    <div class="grid gap-5 grid-cols-[repeat(auto-fit,minmax(min(320px,100%),1fr))]">
      <div class="grid grid-rows-[1fr_auto] min-h-80 rounded-3xl overflow-hidden bg-tmd03-surface shadow-tmd03-1">
        <div class="grid content-start gap-3 p-5">
          <div class="flex items-center gap-3">
            <button type="button" aria-label="Open navigation" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd03-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd03-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd03-icon text-[22px] leading-none" aria-hidden="true">menu</span></button>
            <span class="text-[17px] font-medium">Inbox</span>
            <span class="ml-auto font-tmd03-mono text-[11px] uppercase tracking-[0.08em] text-tmd03-onvariant">Bottom bar</span>
          </div>
          <div class="h-3 w-[78%] rounded-full bg-tmd03-outlinevariant" aria-hidden="true"></div>
          <div class="h-3 w-[62%] rounded-full bg-tmd03-outlinevariant" aria-hidden="true"></div>
          <div class="h-3 w-[70%] rounded-full bg-tmd03-outlinevariant" aria-hidden="true"></div>
        </div>
        <nav aria-label="Primary" class="group/bar grid grid-cols-4 border-t border-tmd03-outlinevariant bg-tmd03-surfacecontainer">
          <input type="radio" name="tmd03-bar" id="tmd03-bar-1" class="peer/b1 sr-only" checked>
          <label for="tmd03-bar-1" class="grid justify-items-center gap-1 pt-3 pb-2.5 cursor-pointer text-tmd03-onvariant text-xs font-medium transition-colors duration-200 group-has-[#tmd03-bar-1:checked]/bar:text-tmd03-onsurface peer-focus-visible/b1:outline-2 peer-focus-visible/b1:-outline-offset-2 peer-focus-visible/b1:outline-tmd03-primary">
            <span class="relative grid place-items-center w-16 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-bar-1:checked]/bar:bg-tmd03-secondarycontainer group-has-[#tmd03-bar-1:checked]/bar:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-bar-1:checked]/bar:[font-variation-settings:'FILL'_1]" aria-hidden="true">inbox</span></span>Inbox
          </label>
          <input type="radio" name="tmd03-bar" id="tmd03-bar-2" class="peer/b2 sr-only">
          <label for="tmd03-bar-2" class="grid justify-items-center gap-1 pt-3 pb-2.5 cursor-pointer text-tmd03-onvariant text-xs font-medium transition-colors duration-200 group-has-[#tmd03-bar-2:checked]/bar:text-tmd03-onsurface peer-focus-visible/b2:outline-2 peer-focus-visible/b2:-outline-offset-2 peer-focus-visible/b2:outline-tmd03-primary">
            <span class="relative grid place-items-center w-16 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-bar-2:checked]/bar:bg-tmd03-secondarycontainer group-has-[#tmd03-bar-2:checked]/bar:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-bar-2:checked]/bar:[font-variation-settings:'FILL'_1]" aria-hidden="true">send</span><b class="absolute top-0.5 right-2.5 min-w-4 h-4 px-1 grid place-items-center rounded-full bg-tmd03-error text-tmd03-onerror font-tmd03-mono text-[10px] tabular-nums">3</b></span>Sent
          </label>
          <input type="radio" name="tmd03-bar" id="tmd03-bar-3" class="peer/b3 sr-only">
          <label for="tmd03-bar-3" class="grid justify-items-center gap-1 pt-3 pb-2.5 cursor-pointer text-tmd03-onvariant text-xs font-medium transition-colors duration-200 group-has-[#tmd03-bar-3:checked]/bar:text-tmd03-onsurface peer-focus-visible/b3:outline-2 peer-focus-visible/b3:-outline-offset-2 peer-focus-visible/b3:outline-tmd03-primary">
            <span class="relative grid place-items-center w-16 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-bar-3:checked]/bar:bg-tmd03-secondarycontainer group-has-[#tmd03-bar-3:checked]/bar:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-bar-3:checked]/bar:[font-variation-settings:'FILL'_1]" aria-hidden="true">drafts</span></span>Drafts
          </label>
          <input type="radio" name="tmd03-bar" id="tmd03-bar-4" class="peer/b4 sr-only">
          <label for="tmd03-bar-4" class="grid justify-items-center gap-1 pt-3 pb-2.5 cursor-pointer text-tmd03-onvariant text-xs font-medium transition-colors duration-200 group-has-[#tmd03-bar-4:checked]/bar:text-tmd03-onsurface peer-focus-visible/b4:outline-2 peer-focus-visible/b4:-outline-offset-2 peer-focus-visible/b4:outline-tmd03-primary">
            <span class="relative grid place-items-center w-16 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-bar-4:checked]/bar:bg-tmd03-secondarycontainer group-has-[#tmd03-bar-4:checked]/bar:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-bar-4:checked]/bar:[font-variation-settings:'FILL'_1]" aria-hidden="true">settings</span></span>Settings
          </label>
        </nav>
      </div>

      <div class="grid grid-cols-[80px_1fr] min-h-80 rounded-3xl overflow-hidden bg-tmd03-surface shadow-tmd03-1">
        <nav aria-label="Rail" class="group/rail grid content-start gap-2 py-4 bg-tmd03-surfacecontainer border-r border-tmd03-outlinevariant">
          <button type="button" aria-label="Open menu" class="relative overflow-hidden grid place-items-center justify-self-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd03-onsurface mb-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd03-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd03-icon text-[22px] leading-none" aria-hidden="true">menu</span></button>
          <input type="radio" name="tmd03-rail" id="tmd03-rail-1" class="peer/r1 sr-only" checked>
          <label for="tmd03-rail-1" class="grid justify-items-center gap-1 py-1.5 cursor-pointer text-tmd03-onvariant text-[11px] font-medium group-has-[#tmd03-rail-1:checked]/rail:text-tmd03-onsurface peer-focus-visible/r1:outline-2 peer-focus-visible/r1:-outline-offset-2 peer-focus-visible/r1:outline-tmd03-primary">
            <span class="grid place-items-center w-14 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-rail-1:checked]/rail:bg-tmd03-secondarycontainer group-has-[#tmd03-rail-1:checked]/rail:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-rail-1:checked]/rail:[font-variation-settings:'FILL'_1]" aria-hidden="true">dashboard</span></span>Home
          </label>
          <input type="radio" name="tmd03-rail" id="tmd03-rail-2" class="peer/r2 sr-only">
          <label for="tmd03-rail-2" class="grid justify-items-center gap-1 py-1.5 cursor-pointer text-tmd03-onvariant text-[11px] font-medium group-has-[#tmd03-rail-2:checked]/rail:text-tmd03-onsurface peer-focus-visible/r2:outline-2 peer-focus-visible/r2:-outline-offset-2 peer-focus-visible/r2:outline-tmd03-primary">
            <span class="grid place-items-center w-14 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-rail-2:checked]/rail:bg-tmd03-secondarycontainer group-has-[#tmd03-rail-2:checked]/rail:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-rail-2:checked]/rail:[font-variation-settings:'FILL'_1]" aria-hidden="true">bar_chart</span></span>Reports
          </label>
          <input type="radio" name="tmd03-rail" id="tmd03-rail-3" class="peer/r3 sr-only">
          <label for="tmd03-rail-3" class="grid justify-items-center gap-1 py-1.5 cursor-pointer text-tmd03-onvariant text-[11px] font-medium group-has-[#tmd03-rail-3:checked]/rail:text-tmd03-onsurface peer-focus-visible/r3:outline-2 peer-focus-visible/r3:-outline-offset-2 peer-focus-visible/r3:outline-tmd03-primary">
            <span class="grid place-items-center w-14 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-rail-3:checked]/rail:bg-tmd03-secondarycontainer group-has-[#tmd03-rail-3:checked]/rail:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-rail-3:checked]/rail:[font-variation-settings:'FILL'_1]" aria-hidden="true">group</span></span>Team
          </label>
        </nav>
        <div class="grid content-start gap-3 p-5">
          <span class="font-tmd03-mono text-[11px] uppercase tracking-[0.08em] text-tmd03-onvariant">Navigation rail &middot; 80px</span>
          <div class="h-28 rounded-2xl bg-linear-[150deg,#c62828,#ef5350_70%,#ffcdd2]" aria-hidden="true"></div>
          <div class="h-3 w-[72%] rounded-full bg-tmd03-outlinevariant" aria-hidden="true"></div>
          <div class="h-3 w-[54%] rounded-full bg-tmd03-outlinevariant" aria-hidden="true"></div>
        </div>
      </div>
    </div>

    <div class="grid grid-cols-[repeat(auto-fit,minmax(min(256px,100%),1fr))] gap-5">
      <nav aria-label="Drawer" class="group/dr has-checked:ring-1 has-checked:ring-tmd03-outlinevariant grid content-start gap-1 p-3 rounded-3xl bg-tmd03-surfacecontainer">
        <span class="px-4 py-2 font-tmd03-mono text-[11px] uppercase tracking-[0.08em] text-tmd03-onvariant">Standard drawer</span>
        <input type="radio" name="tmd03-drawer" id="tmd03-dr-1" class="peer/d1 sr-only" checked>
        <label for="tmd03-dr-1" class="relative overflow-hidden flex items-center gap-3 min-h-14 px-4 rounded-full cursor-pointer text-sm font-medium text-tmd03-onvariant transition-colors duration-200 group-has-[#tmd03-dr-1:checked]/dr:bg-tmd03-secondarycontainer group-has-[#tmd03-dr-1:checked]/dr:text-tmd03-onsecondarycontainer peer-focus-visible/d1:outline-2 peer-focus-visible/d1:-outline-offset-2 peer-focus-visible/d1:outline-tmd03-primary before:absolute before:inset-0 before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-dr-1:checked]/dr:[font-variation-settings:'FILL'_1]" aria-hidden="true">inbox</span>Inbox<span class="ml-auto font-tmd03-mono text-xs tabular-nums">24</span></label>
        <input type="radio" name="tmd03-drawer" id="tmd03-dr-2" class="peer/d2 sr-only">
        <label for="tmd03-dr-2" class="relative overflow-hidden flex items-center gap-3 min-h-14 px-4 rounded-full cursor-pointer text-sm font-medium text-tmd03-onvariant transition-colors duration-200 group-has-[#tmd03-dr-2:checked]/dr:bg-tmd03-secondarycontainer group-has-[#tmd03-dr-2:checked]/dr:text-tmd03-onsecondarycontainer peer-focus-visible/d2:outline-2 peer-focus-visible/d2:-outline-offset-2 peer-focus-visible/d2:outline-tmd03-primary before:absolute before:inset-0 before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-dr-2:checked]/dr:[font-variation-settings:'FILL'_1]" aria-hidden="true">star</span>Starred<span class="ml-auto font-tmd03-mono text-xs tabular-nums">6</span></label>
        <input type="radio" name="tmd03-drawer" id="tmd03-dr-3" class="peer/d3 sr-only">
        <label for="tmd03-dr-3" class="relative overflow-hidden flex items-center gap-3 min-h-14 px-4 rounded-full cursor-pointer text-sm font-medium text-tmd03-onvariant transition-colors duration-200 group-has-[#tmd03-dr-3:checked]/dr:bg-tmd03-secondarycontainer group-has-[#tmd03-dr-3:checked]/dr:text-tmd03-onsecondarycontainer peer-focus-visible/d3:outline-2 peer-focus-visible/d3:-outline-offset-2 peer-focus-visible/d3:outline-tmd03-primary before:absolute before:inset-0 before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-dr-3:checked]/dr:[font-variation-settings:'FILL'_1]" aria-hidden="true">label</span>Labels</label>
        <hr class="my-2 border-0 border-t border-tmd03-outlinevariant">
        <label for="tmd03-dr-1" class="px-4 pb-1 text-xs text-tmd03-onvariant">Selecting any destination adds a hairline ring to this drawer via <span class="font-tmd03-mono text-[11px]">has-checked:</span>.</label>
      </nav>
      <div class="grid content-center gap-2 p-6 rounded-3xl bg-tmd03-surface shadow-tmd03-1">
        <h3 class="text-lg font-medium">Why radios, not divs</h3>
        <p class="text-sm leading-6 text-tmd03-onvariant text-pretty">A native radio group is one tab stop, arrow-key traversable, announced as &ldquo;2 of 4&rdquo;, and restores on back-navigation. Every one of those behaviours would be JavaScript in a <span class="font-tmd03-mono text-[12px]">div role=&quot;tab&quot;</span> rebuild &mdash; and the styling hook, <span class="font-tmd03-mono text-[12px]">peer-checked:</span>, is shorter than the class-toggling it replaces.</p>
      </div>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tmd03-bg: #fafafa;
  --color-tmd03-bg: oklch(0.985 0 89.9);
  --color-tmd03-surface: #fffbfe;
  --color-tmd03-surface: oklch(0.992 0.006 334);
  --color-tmd03-surfacecontainer: #f3eff4;
  --color-tmd03-surfacecontainer: oklch(0.957 0.008 319.4);
  --color-tmd03-primary: #c62828;
  --color-tmd03-primary: oklch(0.539 0.194 26.7);
  --color-tmd03-secondarycontainer: #ffdad6;
  --color-tmd03-secondarycontainer: oklch(0.918 0.042 25.2);
  --color-tmd03-onsecondarycontainer: #410002;
  --color-tmd03-onsecondarycontainer: oklch(0.236 0.096 27.2);
  --color-tmd03-onsurface: #1c1b1f;
  --color-tmd03-onsurface: oklch(0.225 0.008 297.2);
  --color-tmd03-onvariant: #4f4b52;
  --color-tmd03-onvariant: oklch(0.419 0.013 311);
  --color-tmd03-outlinevariant: #ddd5dd;
  --color-tmd03-outlinevariant: oklch(0.882 0.014 325.7);
  --color-tmd03-error: #b3261e;
  --color-tmd03-error: oklch(0.501 0.178 28.7);
  --color-tmd03-onerror: #ffffff;
  --color-tmd03-onerror: oklch(1 0 89.9);
  --font-tmd03-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
  --font-tmd03-mono: 'Roboto Mono', ui-monospace, monospace;
  --font-tmd03-icon: 'Material Symbols Outlined';
  --shadow-tmd03-1: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 1px 3px 1px rgb(0 0 0 / 0.15);
}

.tmd-03 {
  width: 100%;
  min-height: 100svh;
}

.tmd-03 [class*="font-tmd03-icon"] {
  font-feature-settings: 'liga';
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<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.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tmd03-bg: #fafafa;
  --color-tmd03-bg: oklch(0.985 0 89.9);
  --color-tmd03-surface: #fffbfe;
  --color-tmd03-surface: oklch(0.992 0.006 334);
  --color-tmd03-surfacecontainer: #f3eff4;
  --color-tmd03-surfacecontainer: oklch(0.957 0.008 319.4);
  --color-tmd03-primary: #c62828;
  --color-tmd03-primary: oklch(0.539 0.194 26.7);
  --color-tmd03-secondarycontainer: #ffdad6;
  --color-tmd03-secondarycontainer: oklch(0.918 0.042 25.2);
  --color-tmd03-onsecondarycontainer: #410002;
  --color-tmd03-onsecondarycontainer: oklch(0.236 0.096 27.2);
  --color-tmd03-onsurface: #1c1b1f;
  --color-tmd03-onsurface: oklch(0.225 0.008 297.2);
  --color-tmd03-onvariant: #4f4b52;
  --color-tmd03-onvariant: oklch(0.419 0.013 311);
  --color-tmd03-outlinevariant: #ddd5dd;
  --color-tmd03-outlinevariant: oklch(0.882 0.014 325.7);
  --color-tmd03-error: #b3261e;
  --color-tmd03-error: oklch(0.501 0.178 28.7);
  --color-tmd03-onerror: #ffffff;
  --color-tmd03-onerror: oklch(1 0 89.9);
  --font-tmd03-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
  --font-tmd03-mono: 'Roboto Mono', ui-monospace, monospace;
  --font-tmd03-icon: 'Material Symbols Outlined';
  --shadow-tmd03-1: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 1px 3px 1px rgb(0 0 0 / 0.15);
}

.tmd-03 {
  width: 100%;
  min-height: 100svh;
}

.tmd-03 [class*="font-tmd03-icon"] {
  font-feature-settings: 'liga';
}
3Fonts used by this demooptional — skip if you have your own type stack
<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 M3 active indicator stops being a ::before pill and becomes a background on an already-present icon box — fewer nodes, and the pill animates its colour for free.
  • group-has-[#id:checked]/bar:[font-variation-settings:'FILL'_1] replaces the second, filled icon set the CSS version needed. One glyph, one axis, one utility.
  • The direction of the state read is the thing to get right: a sibling label can use peer-checked/b1:, but anything inside that label must use group-has-[#id:checked]/bar: — peer-* matches following siblings only, never their descendants.
  • The three containers now share one item class list and differ only in geometry utilities, which makes the M3 claim that they are the same component visible in the source.
  • has-checked: on the drawer is the ancestor half of the same state read — the utility answer to what used to need a parent class toggled in JS.
  • No JS at all, including focus rings: peer-focus-visible/d1: puts the ring on the label while the real focus sits on the sr-only input.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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 Navigation Bar, Rail and Drawer
Source: https://codefronts.com/design-styles/tailwind-material-design-components/tailwind-material-design-navigation-bar-rail-drawer/

The three M3 navigation containers side by side &mdash; bottom navigation bar, rail and standard drawer &mdash; all driven by radios and peer-checked:, so the active tab, its pill indicator and its filled icon all change with no JavaScript. The port's win is the indicator: an M3 active pill is one peer-checked: background on the icon box, not a ::before rule per item.
## HTML
```html
<section class="tmd-03 w-full min-h-svh bg-tmd03-bg font-tmd03-sans text-tmd03-onsurface px-5 py-9" aria-label="Material Design navigation containers demo">
  <div class="w-[min(1100px,100%)] mx-auto grid gap-6">
    <header class="grid gap-1.5">
      <h2 class="text-[26px] font-medium tracking-[-0.01em]">Navigation bar, rail &amp; drawer</h2>
      <p class="text-sm text-tmd03-onvariant">One item recipe, three containers. Radios carry the state; <code class="font-tmd03-mono text-[12px]">peer-checked:</code> carries the look.</p>
    </header>

    <div class="grid gap-5 grid-cols-[repeat(auto-fit,minmax(min(320px,100%),1fr))]">
      <div class="grid grid-rows-[1fr_auto] min-h-80 rounded-3xl overflow-hidden bg-tmd03-surface shadow-tmd03-1">
        <div class="grid content-start gap-3 p-5">
          <div class="flex items-center gap-3">
            <button type="button" aria-label="Open navigation" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd03-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd03-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd03-icon text-[22px] leading-none" aria-hidden="true">menu</span></button>
            <span class="text-[17px] font-medium">Inbox</span>
            <span class="ml-auto font-tmd03-mono text-[11px] uppercase tracking-[0.08em] text-tmd03-onvariant">Bottom bar</span>
          </div>
          <div class="h-3 w-[78%] rounded-full bg-tmd03-outlinevariant" aria-hidden="true"></div>
          <div class="h-3 w-[62%] rounded-full bg-tmd03-outlinevariant" aria-hidden="true"></div>
          <div class="h-3 w-[70%] rounded-full bg-tmd03-outlinevariant" aria-hidden="true"></div>
        </div>
        <nav aria-label="Primary" class="group/bar grid grid-cols-4 border-t border-tmd03-outlinevariant bg-tmd03-surfacecontainer">
          <input type="radio" name="tmd03-bar" id="tmd03-bar-1" class="peer/b1 sr-only" checked>
          <label for="tmd03-bar-1" class="grid justify-items-center gap-1 pt-3 pb-2.5 cursor-pointer text-tmd03-onvariant text-xs font-medium transition-colors duration-200 group-has-[#tmd03-bar-1:checked]/bar:text-tmd03-onsurface peer-focus-visible/b1:outline-2 peer-focus-visible/b1:-outline-offset-2 peer-focus-visible/b1:outline-tmd03-primary">
            <span class="relative grid place-items-center w-16 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-bar-1:checked]/bar:bg-tmd03-secondarycontainer group-has-[#tmd03-bar-1:checked]/bar:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-bar-1:checked]/bar:[font-variation-settings:'FILL'_1]" aria-hidden="true">inbox</span></span>Inbox
          </label>
          <input type="radio" name="tmd03-bar" id="tmd03-bar-2" class="peer/b2 sr-only">
          <label for="tmd03-bar-2" class="grid justify-items-center gap-1 pt-3 pb-2.5 cursor-pointer text-tmd03-onvariant text-xs font-medium transition-colors duration-200 group-has-[#tmd03-bar-2:checked]/bar:text-tmd03-onsurface peer-focus-visible/b2:outline-2 peer-focus-visible/b2:-outline-offset-2 peer-focus-visible/b2:outline-tmd03-primary">
            <span class="relative grid place-items-center w-16 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-bar-2:checked]/bar:bg-tmd03-secondarycontainer group-has-[#tmd03-bar-2:checked]/bar:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-bar-2:checked]/bar:[font-variation-settings:'FILL'_1]" aria-hidden="true">send</span><b class="absolute top-0.5 right-2.5 min-w-4 h-4 px-1 grid place-items-center rounded-full bg-tmd03-error text-tmd03-onerror font-tmd03-mono text-[10px] tabular-nums">3</b></span>Sent
          </label>
          <input type="radio" name="tmd03-bar" id="tmd03-bar-3" class="peer/b3 sr-only">
          <label for="tmd03-bar-3" class="grid justify-items-center gap-1 pt-3 pb-2.5 cursor-pointer text-tmd03-onvariant text-xs font-medium transition-colors duration-200 group-has-[#tmd03-bar-3:checked]/bar:text-tmd03-onsurface peer-focus-visible/b3:outline-2 peer-focus-visible/b3:-outline-offset-2 peer-focus-visible/b3:outline-tmd03-primary">
            <span class="relative grid place-items-center w-16 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-bar-3:checked]/bar:bg-tmd03-secondarycontainer group-has-[#tmd03-bar-3:checked]/bar:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-bar-3:checked]/bar:[font-variation-settings:'FILL'_1]" aria-hidden="true">drafts</span></span>Drafts
          </label>
          <input type="radio" name="tmd03-bar" id="tmd03-bar-4" class="peer/b4 sr-only">
          <label for="tmd03-bar-4" class="grid justify-items-center gap-1 pt-3 pb-2.5 cursor-pointer text-tmd03-onvariant text-xs font-medium transition-colors duration-200 group-has-[#tmd03-bar-4:checked]/bar:text-tmd03-onsurface peer-focus-visible/b4:outline-2 peer-focus-visible/b4:-outline-offset-2 peer-focus-visible/b4:outline-tmd03-primary">
            <span class="relative grid place-items-center w-16 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-bar-4:checked]/bar:bg-tmd03-secondarycontainer group-has-[#tmd03-bar-4:checked]/bar:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-bar-4:checked]/bar:[font-variation-settings:'FILL'_1]" aria-hidden="true">settings</span></span>Settings
          </label>
        </nav>
      </div>

      <div class="grid grid-cols-[80px_1fr] min-h-80 rounded-3xl overflow-hidden bg-tmd03-surface shadow-tmd03-1">
        <nav aria-label="Rail" class="group/rail grid content-start gap-2 py-4 bg-tmd03-surfacecontainer border-r border-tmd03-outlinevariant">
          <button type="button" aria-label="Open menu" class="relative overflow-hidden grid place-items-center justify-self-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd03-onsurface mb-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd03-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd03-icon text-[22px] leading-none" aria-hidden="true">menu</span></button>
          <input type="radio" name="tmd03-rail" id="tmd03-rail-1" class="peer/r1 sr-only" checked>
          <label for="tmd03-rail-1" class="grid justify-items-center gap-1 py-1.5 cursor-pointer text-tmd03-onvariant text-[11px] font-medium group-has-[#tmd03-rail-1:checked]/rail:text-tmd03-onsurface peer-focus-visible/r1:outline-2 peer-focus-visible/r1:-outline-offset-2 peer-focus-visible/r1:outline-tmd03-primary">
            <span class="grid place-items-center w-14 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-rail-1:checked]/rail:bg-tmd03-secondarycontainer group-has-[#tmd03-rail-1:checked]/rail:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-rail-1:checked]/rail:[font-variation-settings:'FILL'_1]" aria-hidden="true">dashboard</span></span>Home
          </label>
          <input type="radio" name="tmd03-rail" id="tmd03-rail-2" class="peer/r2 sr-only">
          <label for="tmd03-rail-2" class="grid justify-items-center gap-1 py-1.5 cursor-pointer text-tmd03-onvariant text-[11px] font-medium group-has-[#tmd03-rail-2:checked]/rail:text-tmd03-onsurface peer-focus-visible/r2:outline-2 peer-focus-visible/r2:-outline-offset-2 peer-focus-visible/r2:outline-tmd03-primary">
            <span class="grid place-items-center w-14 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-rail-2:checked]/rail:bg-tmd03-secondarycontainer group-has-[#tmd03-rail-2:checked]/rail:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-rail-2:checked]/rail:[font-variation-settings:'FILL'_1]" aria-hidden="true">bar_chart</span></span>Reports
          </label>
          <input type="radio" name="tmd03-rail" id="tmd03-rail-3" class="peer/r3 sr-only">
          <label for="tmd03-rail-3" class="grid justify-items-center gap-1 py-1.5 cursor-pointer text-tmd03-onvariant text-[11px] font-medium group-has-[#tmd03-rail-3:checked]/rail:text-tmd03-onsurface peer-focus-visible/r3:outline-2 peer-focus-visible/r3:-outline-offset-2 peer-focus-visible/r3:outline-tmd03-primary">
            <span class="grid place-items-center w-14 h-8 rounded-full transition-colors duration-200 group-has-[#tmd03-rail-3:checked]/rail:bg-tmd03-secondarycontainer group-has-[#tmd03-rail-3:checked]/rail:text-tmd03-onsecondarycontainer motion-reduce:transition-none"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-rail-3:checked]/rail:[font-variation-settings:'FILL'_1]" aria-hidden="true">group</span></span>Team
          </label>
        </nav>
        <div class="grid content-start gap-3 p-5">
          <span class="font-tmd03-mono text-[11px] uppercase tracking-[0.08em] text-tmd03-onvariant">Navigation rail &middot; 80px</span>
          <div class="h-28 rounded-2xl bg-linear-[150deg,#c62828,#ef5350_70%,#ffcdd2]" aria-hidden="true"></div>
          <div class="h-3 w-[72%] rounded-full bg-tmd03-outlinevariant" aria-hidden="true"></div>
          <div class="h-3 w-[54%] rounded-full bg-tmd03-outlinevariant" aria-hidden="true"></div>
        </div>
      </div>
    </div>

    <div class="grid grid-cols-[repeat(auto-fit,minmax(min(256px,100%),1fr))] gap-5">
      <nav aria-label="Drawer" class="group/dr has-checked:ring-1 has-checked:ring-tmd03-outlinevariant grid content-start gap-1 p-3 rounded-3xl bg-tmd03-surfacecontainer">
        <span class="px-4 py-2 font-tmd03-mono text-[11px] uppercase tracking-[0.08em] text-tmd03-onvariant">Standard drawer</span>
        <input type="radio" name="tmd03-drawer" id="tmd03-dr-1" class="peer/d1 sr-only" checked>
        <label for="tmd03-dr-1" class="relative overflow-hidden flex items-center gap-3 min-h-14 px-4 rounded-full cursor-pointer text-sm font-medium text-tmd03-onvariant transition-colors duration-200 group-has-[#tmd03-dr-1:checked]/dr:bg-tmd03-secondarycontainer group-has-[#tmd03-dr-1:checked]/dr:text-tmd03-onsecondarycontainer peer-focus-visible/d1:outline-2 peer-focus-visible/d1:-outline-offset-2 peer-focus-visible/d1:outline-tmd03-primary before:absolute before:inset-0 before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-dr-1:checked]/dr:[font-variation-settings:'FILL'_1]" aria-hidden="true">inbox</span>Inbox<span class="ml-auto font-tmd03-mono text-xs tabular-nums">24</span></label>
        <input type="radio" name="tmd03-drawer" id="tmd03-dr-2" class="peer/d2 sr-only">
        <label for="tmd03-dr-2" class="relative overflow-hidden flex items-center gap-3 min-h-14 px-4 rounded-full cursor-pointer text-sm font-medium text-tmd03-onvariant transition-colors duration-200 group-has-[#tmd03-dr-2:checked]/dr:bg-tmd03-secondarycontainer group-has-[#tmd03-dr-2:checked]/dr:text-tmd03-onsecondarycontainer peer-focus-visible/d2:outline-2 peer-focus-visible/d2:-outline-offset-2 peer-focus-visible/d2:outline-tmd03-primary before:absolute before:inset-0 before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-dr-2:checked]/dr:[font-variation-settings:'FILL'_1]" aria-hidden="true">star</span>Starred<span class="ml-auto font-tmd03-mono text-xs tabular-nums">6</span></label>
        <input type="radio" name="tmd03-drawer" id="tmd03-dr-3" class="peer/d3 sr-only">
        <label for="tmd03-dr-3" class="relative overflow-hidden flex items-center gap-3 min-h-14 px-4 rounded-full cursor-pointer text-sm font-medium text-tmd03-onvariant transition-colors duration-200 group-has-[#tmd03-dr-3:checked]/dr:bg-tmd03-secondarycontainer group-has-[#tmd03-dr-3:checked]/dr:text-tmd03-onsecondarycontainer peer-focus-visible/d3:outline-2 peer-focus-visible/d3:-outline-offset-2 peer-focus-visible/d3:outline-tmd03-primary before:absolute before:inset-0 before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd03-icon text-[22px] leading-none group-has-[#tmd03-dr-3:checked]/dr:[font-variation-settings:'FILL'_1]" aria-hidden="true">label</span>Labels</label>
        <hr class="my-2 border-0 border-t border-tmd03-outlinevariant">
        <label for="tmd03-dr-1" class="px-4 pb-1 text-xs text-tmd03-onvariant">Selecting any destination adds a hairline ring to this drawer via <span class="font-tmd03-mono text-[11px]">has-checked:</span>.</label>
      </nav>
      <div class="grid content-center gap-2 p-6 rounded-3xl bg-tmd03-surface shadow-tmd03-1">
        <h3 class="text-lg font-medium">Why radios, not divs</h3>
        <p class="text-sm leading-6 text-tmd03-onvariant text-pretty">A native radio group is one tab stop, arrow-key traversable, announced as &ldquo;2 of 4&rdquo;, and restores on back-navigation. Every one of those behaviours would be JavaScript in a <span class="font-tmd03-mono text-[12px]">div role=&quot;tab&quot;</span> rebuild &mdash; and the styling hook, <span class="font-tmd03-mono text-[12px]">peer-checked:</span>, is shorter than the class-toggling it replaces.</p>
      </div>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tmd03-bg: #fafafa;
  --color-tmd03-bg: oklch(0.985 0 89.9);
  --color-tmd03-surface: #fffbfe;
  --color-tmd03-surface: oklch(0.992 0.006 334);
  --color-tmd03-surfacecontainer: #f3eff4;
  --color-tmd03-surfacecontainer: oklch(0.957 0.008 319.4);
  --color-tmd03-primary: #c62828;
  --color-tmd03-primary: oklch(0.539 0.194 26.7);
  --color-tmd03-secondarycontainer: #ffdad6;
  --color-tmd03-secondarycontainer: oklch(0.918 0.042 25.2);
  --color-tmd03-onsecondarycontainer: #410002;
  --color-tmd03-onsecondarycontainer: oklch(0.236 0.096 27.2);
  --color-tmd03-onsurface: #1c1b1f;
  --color-tmd03-onsurface: oklch(0.225 0.008 297.2);
  --color-tmd03-onvariant: #4f4b52;
  --color-tmd03-onvariant: oklch(0.419 0.013 311);
  --color-tmd03-outlinevariant: #ddd5dd;
  --color-tmd03-outlinevariant: oklch(0.882 0.014 325.7);
  --color-tmd03-error: #b3261e;
  --color-tmd03-error: oklch(0.501 0.178 28.7);
  --color-tmd03-onerror: #ffffff;
  --color-tmd03-onerror: oklch(1 0 89.9);
  --font-tmd03-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
  --font-tmd03-mono: 'Roboto Mono', ui-monospace, monospace;
  --font-tmd03-icon: 'Material Symbols Outlined';
  --shadow-tmd03-1: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 1px 3px 1px rgb(0 0 0 / 0.15);
}

.tmd-03 {
  width: 100%;
  min-height: 100svh;
}

.tmd-03 [class*="font-tmd03-icon"] {
  font-feature-settings: 'liga';
}
```

How this works

A tab bar is a radio group, and the nav reads it. Each destination is a visually hidden radio plus a label. The pieces that change — the icon box, the glyph fill — are inside the label, so they are not siblings of the radio: peer-* cannot reach them and the state is read from the nav instead.

<nav class="group/bar grid grid-cols-4">
  <input type="radio" name="tmd03-bar" id="tmd03-bar-1" class="sr-only" checked>
  <label for="tmd03-bar-1">
    <span class="w-16 h-8 rounded-full grid place-items-center
      group-has-[#tmd03-bar-1:checked]/bar:bg-tmd03-secondarycontainer">inbox</span>

Native radios give arrow-key traversal, a single tab stop for the group and correct screen-reader grouping — all the things a div role=tab reimplementation gets wrong.

The filled-icon trick. Material Symbols is a variable font with a FILL axis, so the active icon is the same glyph at FILL 1. As a utility that is peer-checked/a:[font-variation-settings:'FILL'_1] — an arbitrary property, because a font axis is not a design-scale value, and it removes the second icon set entirely.

Three containers, one vocabulary. The bar is a 4-track grid pinned to the bottom of its stage; the rail is an 80px column with the same item recipe stacked; the drawer is a 256px column where the item becomes a full-width pill. Nothing but geometry changes between them, which is exactly what the M3 spec claims and what the CSS version's three unrelated blocks obscured.

Make it yours

  • Add a destination by copying one input/label pair and giving it a fresh peer name — the grid track count is the only other edit.
  • Turn the rail into an extended rail by swapping w-20 for w-56 and the item's grid for flex items-center gap-3.
  • Recolour the active pill through --color-tmd03-secondarycontainer; the indicator, the drawer pill and the badge all read it.
  • Badges are absolutely positioned spans on the icon box; delete them for a quieter bar.
  • For a real app, promote the bar to fixed bottom-0 inset-x-0 and add pb-[env(safe-area-inset-bottom)].

Gotchas — read before shipping

  • sr-only, never hidden, on the radios — a display:none input is unfocusable and the whole group leaves the tab order.
  • peer-* compiles to a sibling selector, so it never reaches a descendant of the label. That mistake fails silently — the active pill simply never paints.
  • The active icon box must be at least 44px tall in its hit target even though the M3 pill is 32px — pad the label, not the pill.
  • aria-current=&quot;page&quot; belongs on drawer links (real navigation); radios express a view switch, not a page change. Do not mix the two idioms in one container.
  • Arbitrary font-variation-settings values need the underscore escape and quoted axis name: [font-variation-settings:'FILL'_1].
  • The rail's labels are optional in M3 but never omit them for the bar; icon-only bottom bars fail comprehension tests at every literacy level.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

has-checked: compiles to :has(), which landed in Firefox 121; the rest runs anywhere Tailwind v4's sheet does. The FILL axis needs the variable Material Symbols font, which the setup block loads.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

HTML & JSno Tailwind involved

Search CodeFronts

Loading…