Tailwind Navbar: 16 Responsive Examples, From Simple to Mega

Tailwind has no navbar class. You build the bar from flexbox, spacing, and breakpoint utilities, and hidden md:flex swaps the links between the mobile menu and the desktop row. For dropdowns, the W3C ARIA Authoring Practices Guide recommends disclosure buttons over the ARIA menu role.

Below are 16 Tailwind navbar examples to copy, followed by a guide to toggles, sticky bars, dropdowns, and accessibility.

By Bogdan Sandu ยท Updated October 29, 2026

Every example is built with Tailwind CSS v4.3 utility classes. To try one quickly, add the Play CDN tags below to your page and paste the example's HTML, CSS and JS. The dark variant line makes dark: classes follow a .dark class instead of the system setting. For production, install Tailwind with Vite or the CLI.

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
  @custom-variant dark (&:where(.dark, .dark *));
</style>

Responsive basics

Five everyday bars built from flex, grid and breakpoint utilities: a collapsing product bar, a centered logo, a docs bar with search, an underline active state and a navbar that responds to its container instead of the screen.

Responsive navbar with a disclosure toggle

  • hidden md:flex
  • group-aria-expanded:
  • aria-[current=page]:
  • flex-wrap

On a phone the links collapse behind the menu button. Tap it to open the panel.

One link list serves both layouts: hidden w-full md:flex keeps it collapsed on phones and inline from 768px, and flex-wrap drops it onto its own row when it opens. The menu button carries aria-expanded and aria-controls, and group-aria-expanded: swaps the hamburger for a close icon without any extra JavaScript. The script toggles one class, updates aria-expanded, closes the panel on Escape and after a link click.

View code
<nav class="w-full border-b border-slate-200 bg-white" aria-label="Main">
  <div class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-y-3 px-4 py-3 md:px-6">
    <a href="#" class="flex items-center gap-2.5 rounded-lg text-lg font-semibold tracking-tight text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-teal-600">
      <svg class="size-8" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="9" fill="#0f766e"/><path d="M9 21l5-9 4 6 2.5-3.5L23 21" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
      Tallyhaus
    </a>
    <button type="button" class="tnv-toggle group inline-flex size-11 items-center justify-center rounded-lg text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-teal-600 md:hidden" aria-controls="tnv-main-menu" aria-expanded="false">
      <span class="sr-only">Main menu</span>
      <svg class="size-6 group-aria-expanded:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg>
      <svg class="hidden size-6 group-aria-expanded:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
    </button>
    <div id="tnv-main-menu" class="hidden w-full md:flex md:w-auto md:items-center md:gap-8">
      <ul class="flex flex-col gap-1 border-t border-slate-200 pt-3 md:flex-row md:gap-6 md:border-0 md:pt-0">
        <li><a href="#" aria-current="page" class="block rounded-lg px-3 py-2.5 text-[15px] font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:bg-teal-50 aria-[current=page]:text-teal-800 md:px-1 md:py-1 md:hover:bg-transparent md:aria-[current=page]:bg-transparent">Product</a></li>
        <li><a href="#" class="block rounded-lg px-3 py-2.5 text-[15px] font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:bg-teal-50 aria-[current=page]:text-teal-800 md:px-1 md:py-1 md:hover:bg-transparent md:aria-[current=page]:bg-transparent">Pricing</a></li>
        <li><a href="#" class="block rounded-lg px-3 py-2.5 text-[15px] font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:bg-teal-50 aria-[current=page]:text-teal-800 md:px-1 md:py-1 md:hover:bg-transparent md:aria-[current=page]:bg-transparent">Customers</a></li>
        <li><a href="#" class="block rounded-lg px-3 py-2.5 text-[15px] font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:bg-teal-50 aria-[current=page]:text-teal-800 md:px-1 md:py-1 md:hover:bg-transparent md:aria-[current=page]:bg-transparent">Changelog</a></li>
      </ul>
      <div class="mt-3 flex flex-col gap-2 border-t border-slate-200 pt-3 pb-1 md:mt-0 md:flex-row md:items-center md:gap-2 md:border-0 md:p-0">
        <a href="#" class="rounded-lg px-3 py-2.5 text-[15px] font-medium text-slate-700 hover:bg-slate-50 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600">Log in</a>
        <a href="#" class="rounded-lg bg-teal-700 px-4 py-2.5 text-center text-[15px] font-semibold text-white shadow-xs hover:bg-teal-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">Start free trial</a>
      </div>
    </div>
  </div>
</nav>

Docs navbar with a search field

  • role="search"
  • placeholder:
  • focus:outline-violet-600/30
  • ml-auto

On a phone the links collapse behind the menu button. Tap it to open the panel.

The search form carries role="search" and a real label hidden with sr-only, and ml-auto pushes it to the right edge from md up. On phones the same form is repeated at the top of the collapsible panel, because the narrow bar has no room for it. The script handles submit with preventDefault, writes the query into a status line and focuses the field when you press the slash key.

View code
<header class="w-full border-b border-zinc-200 bg-white">
  <div class="mx-auto flex max-w-6xl flex-wrap items-center gap-x-6 gap-y-3 px-4 py-3 md:flex-nowrap md:px-6">
    <a href="#" class="flex items-center gap-2 rounded-md font-semibold text-zinc-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-violet-600">
      <svg class="size-7" viewBox="0 0 28 28" aria-hidden="true"><path d="M14 2 25 8.5v11L14 26 3 19.5v-11Z" fill="#6d28d9"/><path d="M14 9l6 3.5v6L14 22l-6-3.5v-6Z" fill="#ddd6fe"/></svg>
      Quarry
      <span class="rounded-full bg-violet-50 px-2 py-0.5 text-xs font-medium text-violet-800 ring-1 ring-violet-200 ring-inset">v2.8</span>
    </a>
    <nav aria-label="Main" class="hidden md:block">
      <ul class="flex gap-5">
        <li><a href="#" aria-current="page" class="block rounded-md px-3 py-2.5 text-sm font-medium text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-violet-600 aria-[current=page]:text-violet-700 md:px-1 md:py-1 md:hover:bg-transparent">Docs</a></li>
        <li><a href="#" class="block rounded-md px-3 py-2.5 text-sm font-medium text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-violet-600 aria-[current=page]:text-violet-700 md:px-1 md:py-1 md:hover:bg-transparent">Components</a></li>
        <li><a href="#" class="block rounded-md px-3 py-2.5 text-sm font-medium text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-violet-600 aria-[current=page]:text-violet-700 md:px-1 md:py-1 md:hover:bg-transparent">Blog</a></li>
      </ul>
    </nav>
    <form role="search" class="tnv-search ml-auto hidden w-full max-w-xs md:block">
      <label for="tnv-q-desk" class="sr-only">Search the docs</label>
      <div class="relative">
        <svg class="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-zinc-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/></svg>
        <input id="tnv-q-desk" name="q" type="search" placeholder="Search the docs" autocomplete="off" class="h-11 w-full rounded-lg border border-zinc-300 bg-zinc-50 pr-3 pl-9 text-sm text-zinc-900 placeholder:text-zinc-500 focus:border-violet-600 focus:bg-white focus:outline-2 focus:outline-violet-600/30 md:h-10 md:pr-9">
        <kbd class="pointer-events-none absolute top-1/2 right-2.5 -translate-y-1/2 rounded border border-zinc-300 bg-white px-1.5 font-sans text-xs text-zinc-500">/</kbd>
      </div>
    </form>
    <button type="button" class="tnv-toggle group inline-flex size-11 items-center justify-center rounded-lg ml-auto text-zinc-700 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-violet-600 md:hidden" aria-controls="tnv-docs-menu" aria-expanded="false">
      <span class="sr-only">Main menu</span>
      <svg class="size-6 group-aria-expanded:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg>
      <svg class="hidden size-6 group-aria-expanded:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
    </button>
    <div id="tnv-docs-menu" class="hidden w-full md:hidden">
      <form role="search" class="tnv-search">
        <label for="tnv-q-mob" class="sr-only">Search the docs</label>
        <div class="relative">
          <svg class="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-zinc-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/></svg>
          <input id="tnv-q-mob" name="q" type="search" placeholder="Search the docs" autocomplete="off" class="h-11 w-full rounded-lg border border-zinc-300 bg-zinc-50 pr-3 pl-9 text-sm text-zinc-900 placeholder:text-zinc-500 focus:border-violet-600 focus:bg-white focus:outline-2 focus:outline-violet-600/30 md:h-10 md:pr-9">
        </div>
      </form>
      <nav aria-label="Main" class="mt-2">
        <ul class="flex flex-col">
          <li><a href="#" aria-current="page" class="block rounded-md px-3 py-2.5 text-sm font-medium text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-violet-600 aria-[current=page]:text-violet-700 md:px-1 md:py-1 md:hover:bg-transparent">Docs</a></li>
          <li><a href="#" class="block rounded-md px-3 py-2.5 text-sm font-medium text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-violet-600 aria-[current=page]:text-violet-700 md:px-1 md:py-1 md:hover:bg-transparent">Components</a></li>
          <li><a href="#" class="block rounded-md px-3 py-2.5 text-sm font-medium text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-violet-600 aria-[current=page]:text-violet-700 md:px-1 md:py-1 md:hover:bg-transparent">Blog</a></li>
        </ul>
      </nav>
    </div>
  </div>
  <p id="tnv-search-status" role="status" class="hidden border-t border-zinc-200 bg-violet-50 px-4 py-2 text-sm text-violet-900 md:px-6"></p>
</header>

Underline active indicator

  • aria-[current=page]:
  • after:scale-x-*
  • motion-safe:
  • overflow-x-auto

Hover a tab, then click it to move the indicator. On a phone the row scrolls sideways.

The current page is marked with aria-current="page", and aria-[current=page]: styles it, so the visual state and the screen reader announcement come from one attribute. The underline is an after: pseudo-element scaled from 0 to 1, grown on hover and kept at full width for the current link, with the transition inside motion-safe:. On phones the row scrolls sideways inside overflow-x-auto instead of wrapping. The script only moves aria-current for the demo; on a real site the server sets it.

View code
<div class="w-full bg-white">
  <div class="border-b border-gray-200">
    <div class="mx-auto max-w-6xl px-4 pt-6 md:px-6">
      <div class="flex flex-wrap items-center gap-3">
        <h2 class="text-xl font-semibold text-gray-950">Q3 retention study</h2>
        <span class="rounded-full bg-emerald-50 px-2.5 py-1 text-xs font-medium text-emerald-800 ring-1 ring-emerald-600/20 ring-inset">Collecting</span>
      </div>
      <nav aria-label="Project" class="-mx-4 mt-3 overflow-x-auto px-4 [scrollbar-width:none]">
        <ul class="flex gap-7 whitespace-nowrap">
          <li><a href="#" aria-current="page" class="tnv-tab relative block py-3.5 text-sm font-medium text-gray-600 hover:text-gray-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600 aria-[current=page]:text-sky-700 after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:origin-left after:scale-x-0 after:rounded-full after:bg-current hover:after:scale-x-100 aria-[current=page]:after:scale-x-100 motion-safe:after:transition-transform motion-safe:after:duration-300">Overview</a></li>
          <li><a href="#" class="tnv-tab relative block py-3.5 text-sm font-medium text-gray-600 hover:text-gray-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600 aria-[current=page]:text-sky-700 after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:origin-left after:scale-x-0 after:rounded-full after:bg-current hover:after:scale-x-100 aria-[current=page]:after:scale-x-100 motion-safe:after:transition-transform motion-safe:after:duration-300">Responses <span class="ml-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-700">1,284</span></a></li>
          <li><a href="#" class="tnv-tab relative block py-3.5 text-sm font-medium text-gray-600 hover:text-gray-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600 aria-[current=page]:text-sky-700 after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:origin-left after:scale-x-0 after:rounded-full after:bg-current hover:after:scale-x-100 aria-[current=page]:after:scale-x-100 motion-safe:after:transition-transform motion-safe:after:duration-300">Segments</a></li>
          <li><a href="#" class="tnv-tab relative block py-3.5 text-sm font-medium text-gray-600 hover:text-gray-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600 aria-[current=page]:text-sky-700 after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:origin-left after:scale-x-0 after:rounded-full after:bg-current hover:after:scale-x-100 aria-[current=page]:after:scale-x-100 motion-safe:after:transition-transform motion-safe:after:duration-300">Reports</a></li>
          <li><a href="#" class="tnv-tab relative block py-3.5 text-sm font-medium text-gray-600 hover:text-gray-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600 aria-[current=page]:text-sky-700 after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:origin-left after:scale-x-0 after:rounded-full after:bg-current hover:after:scale-x-100 aria-[current=page]:after:scale-x-100 motion-safe:after:transition-transform motion-safe:after:duration-300">Settings</a></li>
        </ul>
      </nav>
    </div>
  </div>
  <div class="bg-gray-50">
    <dl class="mx-auto grid max-w-6xl gap-3 px-4 py-6 sm:grid-cols-3 md:px-6">
      <div class="rounded-xl bg-white p-4 ring-1 ring-gray-200"><dt class="text-sm text-gray-600">Responses</dt><dd class="mt-1 text-2xl font-semibold text-gray-950">1,284</dd></div>
      <div class="rounded-xl bg-white p-4 ring-1 ring-gray-200"><dt class="text-sm text-gray-600">Completion rate</dt><dd class="mt-1 text-2xl font-semibold text-gray-950">71%</dd></div>
      <div class="rounded-xl bg-white p-4 ring-1 ring-gray-200"><dt class="text-sm text-gray-600">Median time</dt><dd class="mt-1 text-2xl font-semibold text-gray-950">4m 12s</dd></div>
    </dl>
  </div>
</div>

Navbar driven by a container query

  • @container
  • @2xl:flex
  • @2xl:hidden
  • resize-x

Drag the bottom-right corner of the card to make it narrower.

The wrapper is a @container, so @2xl: (42rem) switches the bar to its desktop row based on the width of the card, not the screen. That makes the same component work in a narrow sidebar, a split view or a full-width header. resize-x lets you drag the corner to watch it collapse; the script only prints the measured width with a ResizeObserver and runs the usual toggle.

View code
<div class="w-full bg-slate-100 px-4 py-8 md:px-8">
  <div id="tnv-cq-box" class="@container mx-auto w-full max-w-full min-w-64 resize-x overflow-hidden rounded-xl border border-slate-300 bg-white shadow-sm">
    <nav aria-label="Main" class="flex flex-wrap items-center justify-between gap-y-2 px-4 py-3">
      <a href="#" class="flex items-center gap-2 rounded-md text-lg font-bold text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-rose-700">
        <svg class="size-7" viewBox="0 0 28 28" aria-hidden="true"><circle cx="14" cy="14" r="14" fill="#be123c"/><path d="M14 6l6 11H8Z" fill="#fff1f2"/><path d="M14 12l4 7h-8Z" fill="#be123c"/></svg>
        Pinecrest
      </a>
      <button type="button" class="tnv-toggle group inline-flex size-11 items-center justify-center rounded-lg text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-rose-700 @2xl:hidden" aria-controls="tnv-cq-menu" aria-expanded="false">
      <span class="sr-only">Main menu</span>
      <svg class="size-6 group-aria-expanded:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg>
      <svg class="hidden size-6 group-aria-expanded:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
    </button>
      <div id="tnv-cq-menu" class="hidden w-full @2xl:flex @2xl:w-auto @2xl:items-center @2xl:gap-5">
        <ul class="flex flex-col gap-1 border-t border-slate-200 pt-2 @2xl:flex-row @2xl:gap-2 @2xl:border-0 @2xl:pt-0">
          <li><a href="#" aria-current="page" class="block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 hover:bg-rose-50 hover:text-rose-800 focus-visible:outline-2 focus-visible:outline-rose-700 aria-[current=page]:text-rose-800 @2xl:px-2 @2xl:py-1.5">Menu</a></li>
          <li><a href="#" class="block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 hover:bg-rose-50 hover:text-rose-800 focus-visible:outline-2 focus-visible:outline-rose-700 aria-[current=page]:text-rose-800 @2xl:px-2 @2xl:py-1.5">Order online</a></li>
          <li><a href="#" class="block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 hover:bg-rose-50 hover:text-rose-800 focus-visible:outline-2 focus-visible:outline-rose-700 aria-[current=page]:text-rose-800 @2xl:px-2 @2xl:py-1.5">Locations</a></li>
          <li><a href="#" class="block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 hover:bg-rose-50 hover:text-rose-800 focus-visible:outline-2 focus-visible:outline-rose-700 aria-[current=page]:text-rose-800 @2xl:px-2 @2xl:py-1.5">Catering</a></li>
        </ul>
        <a href="#" class="mt-2 mb-1 block rounded-full bg-rose-700 px-5 py-2.5 text-center text-sm font-semibold text-white hover:bg-rose-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-700 @2xl:my-0">Reserve a table</a>
      </div>
    </nav>
    <p class="border-t border-slate-200 bg-slate-50 px-4 py-2 text-xs text-slate-600">Container width <output id="tnv-cq-width" class="font-semibold text-slate-900">0</output>px. The links go inline at 672px (@2xl).</p>
  </div>
</div>

Dark, transparent and sticky bars

Color schemes and positioning: a dark navbar with its own theme switch, a bar laid over a photo and a sticky header that changes once its scroll container moves.

Dark navbar with a theme switch

  • dark:
  • @custom-variant dark
  • aria-pressed
  • dark:hidden

Every light class has a dark: partner on the same element, and the page compiles dark: against a .dark class with the @custom-variant line from the setup box. The demo starts with dark on its wrapper; the sun and moon icons swap with dark:hidden and hidden dark:block, and the switch is a toggle button with aria-pressed. On a real site you would put the class on the html element and remember the choice.

View code
<div id="tnv-theme" class="dark w-full">
  <header class="border-b border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950">
    <nav aria-label="Main" class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-y-3 px-4 py-3 md:px-6">
      <a href="#" class="flex items-center gap-2 rounded-md text-lg font-semibold text-zinc-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600 dark:text-white dark:focus-visible:outline-amber-300">
        <svg class="size-7" viewBox="0 0 28 28" aria-hidden="true"><circle cx="14" cy="14" r="14" fill="#f59e0b"/><circle cx="18.5" cy="10.5" r="9" fill="#18181b"/></svg>
        Nightjar Records
      </a>
      <div class="flex items-center gap-1 md:order-last">
        <button type="button" class="tnv-theme-btn inline-flex size-11 items-center justify-center rounded-lg text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-amber-600 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-white dark:focus-visible:outline-amber-300" aria-pressed="true">
          <span class="sr-only">Dark theme</span>
          <svg class="size-5 dark:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"/></svg>
          <svg class="hidden size-5 dark:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"/></svg>
        </button>
        <button type="button" class="tnv-toggle group inline-flex size-11 items-center justify-center rounded-lg text-zinc-600 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-amber-600 md:hidden dark:text-zinc-300 dark:hover:bg-zinc-800 dark:focus-visible:outline-amber-300" aria-controls="tnv-dark-menu" aria-expanded="false">
      <span class="sr-only">Main menu</span>
      <svg class="size-6 group-aria-expanded:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg>
      <svg class="hidden size-6 group-aria-expanded:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
    </button>
      </div>
      <div id="tnv-dark-menu" class="hidden w-full md:flex md:w-auto md:flex-1 md:justify-center">
        <ul class="flex flex-col gap-1 md:flex-row">
          <li><a href="#" aria-current="page" class="block rounded-md px-3 py-2.5 text-sm font-medium text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-amber-600 aria-[current=page]:bg-zinc-100 aria-[current=page]:text-amber-800 md:py-2 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-white dark:focus-visible:outline-amber-300 dark:aria-[current=page]:bg-zinc-800 dark:aria-[current=page]:text-amber-300">Releases</a></li>
          <li><a href="#" class="block rounded-md px-3 py-2.5 text-sm font-medium text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-amber-600 aria-[current=page]:bg-zinc-100 aria-[current=page]:text-amber-800 md:py-2 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-white dark:focus-visible:outline-amber-300 dark:aria-[current=page]:bg-zinc-800 dark:aria-[current=page]:text-amber-300">Artists</a></li>
          <li><a href="#" class="block rounded-md px-3 py-2.5 text-sm font-medium text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-amber-600 aria-[current=page]:bg-zinc-100 aria-[current=page]:text-amber-800 md:py-2 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-white dark:focus-visible:outline-amber-300 dark:aria-[current=page]:bg-zinc-800 dark:aria-[current=page]:text-amber-300">Tour dates</a></li>
          <li><a href="#" class="block rounded-md px-3 py-2.5 text-sm font-medium text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-amber-600 aria-[current=page]:bg-zinc-100 aria-[current=page]:text-amber-800 md:py-2 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-white dark:focus-visible:outline-amber-300 dark:aria-[current=page]:bg-zinc-800 dark:aria-[current=page]:text-amber-300">Shop</a></li>
        </ul>
      </div>
    </nav>
  </header>
  <div class="bg-zinc-50 px-4 py-10 md:px-6 dark:bg-zinc-900">
    <div class="mx-auto max-w-6xl">
      <h2 class="text-2xl font-semibold text-zinc-900 dark:text-white">New pressing out Friday</h2>
      <p class="mt-2 max-w-md text-zinc-600 dark:text-zinc-400">Twelve tracks on 180 gram vinyl, numbered sleeves and a lyric sheet. Pre-orders ship the same week.</p>
    </div>
  </div>
</div>

Transparent navbar over a photo

  • bg-slate-950/60
  • isolate
  • backdrop-blur-md
  • decoration-amber-300

The bar has no background of its own: it sits on a hero whose photo is darkened by a solid bg-slate-950/60 layer, which keeps white text readable without a gradient. isolate on the hero lets the overlay use -z-10 without slipping behind the page. On phones the open panel gets its own translucent fill with backdrop-blur-md so the links stay legible over the busiest part of the image.

View code
<div class="relative isolate w-full overflow-hidden bg-slate-900 bg-cover bg-center" style="background-image:url(coast-road.webp)">
  <div class="absolute inset-0 -z-10 bg-slate-950/60" aria-hidden="true"></div>
  <header>
    <nav aria-label="Main" class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-y-3 border-b border-white/20 px-4 py-4 md:px-6">
      <a href="#" class="flex items-center gap-2 rounded-md text-lg font-semibold text-white focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white">
        <svg class="size-7" viewBox="0 0 28 28" aria-hidden="true"><path d="M2 22 10 9l5 7 3-4 8 10Z" fill="#fcd34d"/><circle cx="20" cy="7" r="3" fill="#fff"/></svg>
        Saltline Trails
      </a>
      <button type="button" class="tnv-toggle group inline-flex size-11 items-center justify-center rounded-lg text-white hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-white md:hidden" aria-controls="tnv-photo-menu" aria-expanded="false">
      <span class="sr-only">Main menu</span>
      <svg class="size-6 group-aria-expanded:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg>
      <svg class="hidden size-6 group-aria-expanded:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
    </button>
      <div id="tnv-photo-menu" class="hidden w-full rounded-xl bg-slate-950/80 p-2 backdrop-blur-md md:flex md:w-auto md:items-center md:gap-8 md:rounded-none md:bg-transparent md:p-0 md:backdrop-blur-none">
        <ul class="flex flex-col md:flex-row md:gap-6">
          <li><a href="#" aria-current="page" class="block rounded-lg px-3 py-2.5 text-[15px] font-medium text-white/90 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white aria-[current=page]:text-white aria-[current=page]:underline aria-[current=page]:decoration-amber-300 aria-[current=page]:decoration-2 aria-[current=page]:underline-offset-8 md:px-1 md:py-1 md:hover:bg-transparent">Routes</a></li>
          <li><a href="#" class="block rounded-lg px-3 py-2.5 text-[15px] font-medium text-white/90 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white aria-[current=page]:text-white aria-[current=page]:underline aria-[current=page]:decoration-amber-300 aria-[current=page]:decoration-2 aria-[current=page]:underline-offset-8 md:px-1 md:py-1 md:hover:bg-transparent">Stays</a></li>
          <li><a href="#" class="block rounded-lg px-3 py-2.5 text-[15px] font-medium text-white/90 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white aria-[current=page]:text-white aria-[current=page]:underline aria-[current=page]:decoration-amber-300 aria-[current=page]:decoration-2 aria-[current=page]:underline-offset-8 md:px-1 md:py-1 md:hover:bg-transparent">Guides</a></li>
          <li><a href="#" class="block rounded-lg px-3 py-2.5 text-[15px] font-medium text-white/90 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white aria-[current=page]:text-white aria-[current=page]:underline aria-[current=page]:decoration-amber-300 aria-[current=page]:decoration-2 aria-[current=page]:underline-offset-8 md:px-1 md:py-1 md:hover:bg-transparent">About</a></li>
        </ul>
        <a href="#" class="mt-2 block rounded-full bg-white px-5 py-2.5 text-center text-sm font-semibold text-slate-900 hover:bg-amber-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white md:mt-0">Plan a trip</a>
      </div>
    </nav>
  </header>
  <div class="mx-auto max-w-6xl px-4 pt-14 pb-20 md:px-6 md:pt-24 md:pb-28">
    <h2 class="max-w-xl text-4xl font-semibold tracking-tight text-balance text-white md:text-5xl">Coastal routes, mapped one switchback at a time</h2>
    <p class="mt-4 max-w-md text-lg text-white/90">Self-guided walks with luggage transfer, hand-picked guesthouses and offline maps for every stage.</p>
    <div class="mt-8 flex flex-wrap gap-3">
      <a href="#" class="rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-950 hover:bg-amber-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white">Browse routes</a>
      <a href="#" class="rounded-full px-6 py-3 text-sm font-semibold text-white ring-1 ring-white/60 hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white">How it works</a>
    </div>
  </div>
</div>

Sticky navbar that changes on scroll

  • sticky top-0
  • data-scrolled:
  • group-data-scrolled:
  • scroll-pt-20

Scroll inside the article.

The header is sticky top-0 inside a scrolling box, so it stays in the flow and nothing slides under it. A passive scroll listener sets a data-scrolled attribute, and data-scrolled: and group-data-scrolled: shrink the padding, add a border, a shadow and a translucent background. scroll-pt-20 on the scroller keeps focused links clear of the bar, and the thin orange line is a reading progress bar scaled with transform.

View code
<div class="w-full bg-stone-100 p-4 md:p-8">
  <div id="tnv-scroller" role="region" aria-label="Article preview" tabindex="0" class="mx-auto h-[26rem] max-w-3xl scroll-pt-20 overflow-y-auto rounded-2xl border border-stone-300 bg-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900">
    <header id="tnv-sticky" class="group sticky top-0 z-10 border-b border-transparent bg-white data-scrolled:border-stone-200 data-scrolled:bg-white/85 data-scrolled:shadow-sm data-scrolled:backdrop-blur-sm motion-safe:transition-[background-color,box-shadow,border-color] motion-safe:duration-300">
      <nav aria-label="Magazine" class="flex items-center justify-between gap-4 px-5 py-5 group-data-scrolled:py-2.5 motion-safe:transition-[padding] motion-safe:duration-300 md:px-8">
        <a href="#" class="rounded-sm font-serif text-xl font-bold text-stone-900 group-data-scrolled:text-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-orange-700">The Long Field</a>
        <ul class="hidden gap-6 text-sm font-medium text-stone-600 md:flex">
          <li><a href="#" aria-current="page" class="rounded-sm hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-orange-700 aria-[current=page]:text-orange-800">Essays</a></li>
          <li><a href="#" class="rounded-sm hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-orange-700">Fieldwork</a></li>
          <li><a href="#" class="rounded-sm hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-orange-700">Letters</a></li>
        </ul>
        <a href="#" class="rounded-full bg-stone-900 px-4 py-2 text-sm font-semibold text-white hover:bg-stone-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900">Subscribe</a>
      </nav>
      <div class="h-0.5 bg-stone-100"><div id="tnv-progress" class="h-full origin-left scale-x-0 bg-orange-700"></div></div>
    </header>
    <article class="px-5 pt-6 pb-10 text-stone-700 md:px-8">
      <h2 class="font-serif text-3xl font-bold text-balance text-stone-900">What a hedgerow remembers</h2>
      <p class="mt-1 text-sm text-stone-600">By June Okafor, 7 min read</p>
      <p class="mt-5 leading-relaxed">The oldest hedge on the farm runs along the lane, a tangle of hawthorn, field maple and hazel that nobody planted on purpose. Count the woody species in a thirty-yard stretch and you get a rough idea of its age.</p>
      <p class="mt-4 leading-relaxed">Ours has seven. That puts it somewhere before the enclosure maps, which is roughly what the parish records suggest. The bank under it is older still, a low ridge of stones cleared from the field one spring at a time.</p>
      <p class="mt-4 leading-relaxed">In winter you can read the hedge like a ledger. Here a gap where a gate once stood, there a stump laid flat and woven back in by someone who knew the craft, and further on a run of blackthorn that took over after the elms died.</p>
      <p class="mt-4 leading-relaxed">We cut it every third year now, in rotation, so one stretch is always thick with berries. The thrushes noticed before we did. By November the lane is loud with them.</p>
      <p class="mt-4 leading-relaxed">Next month: laying the hedge by the old orchard, and why a billhook still beats a flail on anything worth keeping.</p>
    </article>
  </div>
</div>

Dropdowns, mega menus and drawers

Four ways to open a menu from a navbar, from disclosure buttons that follow the W3C pattern to native details, the Popover API and a modal dialog drawer.

Dropdowns with disclosure buttons

  • peer-aria-expanded:block
  • aria-expanded:
  • group-aria-expanded:rotate-180
  • md:absolute

Click a menu. On a phone, open the menu button first where there is one.

This follows the W3C disclosure navigation pattern: plain buttons with aria-expanded and aria-controls, plain links inside, no menu role. The button is the peer and the panel uses hidden peer-aria-expanded:block, so aria-expanded is the only state the script touches. The panels are absolute from md up and stack inline in the mobile panel, and the script closes them on Escape (focus goes back to the button), on outside clicks and when focus leaves the navbar.

View code
<header class="w-full border-b border-gray-200 bg-white">
  <nav id="tnv-dd-nav" aria-label="Main" class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-y-3 px-4 py-3 md:px-6">
    <a href="#" class="flex items-center gap-2 rounded-md text-lg font-semibold text-gray-950 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-orange-700">
      <svg class="size-7" viewBox="0 0 28 28" aria-hidden="true"><rect width="28" height="28" rx="8" fill="#c2410c"/><path d="M8 18c0-6 5-10 12-10-1 7-5 12-12 10Zm0 0 6-6" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>
      Copperleaf
    </a>
    <button type="button" class="tnv-toggle group inline-flex size-11 items-center justify-center rounded-lg text-gray-700 hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-orange-700 md:hidden" aria-controls="tnv-dd-menu" aria-expanded="false">
      <span class="sr-only">Main menu</span>
      <svg class="size-6 group-aria-expanded:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg>
      <svg class="hidden size-6 group-aria-expanded:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
    </button>
    <div id="tnv-dd-menu" class="hidden w-full md:flex md:w-auto md:items-center md:gap-4">
      <ul class="flex flex-col gap-1 md:flex-row md:items-center">
        <li class="tnv-dd-item relative">
          <button type="button" class="tnv-dd group peer flex w-full items-center justify-between gap-1 rounded-lg px-3 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-orange-700 aria-expanded:bg-orange-50 aria-expanded:text-orange-900 md:w-auto md:py-2" aria-expanded="false" aria-controls="tnv-dd-products">Products <svg class="size-4 motion-safe:transition-transform group-aria-expanded:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/></svg></button>
          <div id="tnv-dd-products" class="hidden peer-aria-expanded:block md:absolute md:top-full md:left-0 md:z-20 md:mt-2 md:w-80">
            <ul class="rounded-xl border border-gray-200 bg-white p-2 md:shadow-lg">
              <li><a href="#" class="flex gap-3 rounded-lg p-3 hover:bg-gray-50 focus-visible:outline-2 focus-visible:outline-orange-700"><span class="grid size-9 shrink-0 place-items-center rounded-lg bg-orange-100 text-orange-800"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"/></svg></span><span><span class="block text-sm font-semibold text-gray-900">Invoicing</span><span class="block text-sm text-gray-600">Branded invoices with card and bank payments</span></span></a></li>
              <li><a href="#" class="flex gap-3 rounded-lg p-3 hover:bg-gray-50 focus-visible:outline-2 focus-visible:outline-orange-700"><span class="grid size-9 shrink-0 place-items-center rounded-lg bg-orange-100 text-orange-800"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"/></svg></span><span><span class="block text-sm font-semibold text-gray-900">Payroll</span><span class="block text-sm text-gray-600">Run pay for a team of 2 or 200</span></span></a></li>
              <li><a href="#" class="flex gap-3 rounded-lg p-3 hover:bg-gray-50 focus-visible:outline-2 focus-visible:outline-orange-700"><span class="grid size-9 shrink-0 place-items-center rounded-lg bg-orange-100 text-orange-800"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Z"/></svg></span><span><span class="block text-sm font-semibold text-gray-900">Expense cards</span><span class="block text-sm text-gray-600">Spending limits and receipts that match themselves</span></span></a></li>
            </ul>
          </div>
        </li>
        <li class="tnv-dd-item relative">
          <button type="button" class="tnv-dd group peer flex w-full items-center justify-between gap-1 rounded-lg px-3 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-orange-700 aria-expanded:bg-orange-50 aria-expanded:text-orange-900 md:w-auto md:py-2" aria-expanded="false" aria-controls="tnv-dd-resources">Resources <svg class="size-4 motion-safe:transition-transform group-aria-expanded:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/></svg></button>
          <div id="tnv-dd-resources" class="hidden peer-aria-expanded:block md:absolute md:top-full md:left-0 md:z-20 md:mt-2 md:w-56">
            <ul class="rounded-xl border border-gray-200 bg-white p-2 md:shadow-lg">
              <li><a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-orange-700">Guides</a></li>
              <li><a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-orange-700">Webinars</a></li>
              <li><a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-orange-700">API reference</a></li>
              <li><a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-orange-700">System status</a></li>
            </ul>
          </div>
        </li>
        <li><a href="#" class="block rounded-lg px-3 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-orange-700 md:py-2">Pricing</a></li>
      </ul>
      <a href="#" class="mt-3 mb-1 block rounded-lg bg-orange-700 px-4 py-2.5 text-center text-sm font-semibold text-white hover:bg-orange-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-700 md:my-0">Book a demo</a>
    </div>
  </nav>
</header>

Dropdowns with details and summary

  • <details name>
  • group-open:
  • list-none
  • [&::-webkit-details-marker]:hidden

Click a menu. On a phone, open the menu button first where there is one.

Each dropdown is a native details element, so opening and closing works with no script and the summary is keyboard accessible out of the box. A shared name attribute makes the group exclusive, so opening Academics closes Admissions, and group-open: rotates the chevron and tints the trigger. list-none and the webkit marker selector remove the default triangle. The small script adds what details lacks: closing on an outside click and on Escape.

View code
<header class="w-full border-t-4 border-emerald-900 bg-white shadow-sm">
  <nav aria-label="Main" class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-y-3 px-4 py-3 md:px-6">
    <a href="#" class="flex items-center gap-2.5 rounded-md focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-emerald-700">
      <svg class="h-9 w-8" viewBox="0 0 32 36" aria-hidden="true"><path d="M16 1 30 6v12c0 9-6 14-14 17C8 32 2 27 2 18V6Z" fill="#064e3b"/><path d="M16 9v18M10 15c3 0 6 1 6 4m6-4c-3 0-6 1-6 4" fill="none" stroke="#a7f3d0" stroke-width="2" stroke-linecap="round"/></svg>
      <span class="font-serif text-lg leading-tight font-semibold text-emerald-950">Ashgrove College</span>
    </a>
    <button type="button" class="tnv-toggle group inline-flex size-11 items-center justify-center rounded-lg text-gray-700 hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-emerald-700 md:hidden" aria-controls="tnv-college-menu" aria-expanded="false">
      <span class="sr-only">Main menu</span>
      <svg class="size-6 group-aria-expanded:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg>
      <svg class="hidden size-6 group-aria-expanded:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
    </button>
    <div id="tnv-college-menu" class="hidden w-full md:flex md:w-auto md:items-center md:gap-4">
      <ul class="flex flex-col gap-1 md:flex-row md:items-center">
        <li class="relative">
          <details name="tnv-college" class="tnv-det group">
            <summary class="flex cursor-pointer list-none items-center justify-between gap-1 rounded-lg px-3 py-2.5 text-sm font-medium text-gray-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:outline-2 focus-visible:outline-emerald-700 group-open:bg-emerald-50 group-open:text-emerald-900 md:py-2 [&::-webkit-details-marker]:hidden">Admissions <svg class="size-4 motion-safe:transition-transform group-open:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/></svg></summary>
            <ul class="mt-1 rounded-xl border border-gray-200 bg-white p-2 md:absolute md:top-full md:left-0 md:z-20 md:mt-2 md:w-60 md:shadow-lg">
              <li><a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:outline-2 focus-visible:outline-emerald-700">Undergraduate</a></li>
              <li><a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:outline-2 focus-visible:outline-emerald-700">Graduate programs</a></li>
              <li><a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:outline-2 focus-visible:outline-emerald-700">Transfer students</a></li>
              <li><a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:outline-2 focus-visible:outline-emerald-700">Tuition and aid</a></li>
            </ul>
          </details>
        </li>
        <li class="relative">
          <details name="tnv-college" class="tnv-det group">
            <summary class="flex cursor-pointer list-none items-center justify-between gap-1 rounded-lg px-3 py-2.5 text-sm font-medium text-gray-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:outline-2 focus-visible:outline-emerald-700 group-open:bg-emerald-50 group-open:text-emerald-900 md:py-2 [&::-webkit-details-marker]:hidden">Academics <svg class="size-4 motion-safe:transition-transform group-open:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/></svg></summary>
            <ul class="mt-1 rounded-xl border border-gray-200 bg-white p-2 md:absolute md:top-full md:left-0 md:z-20 md:mt-2 md:w-60 md:shadow-lg">
              <li><a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:outline-2 focus-visible:outline-emerald-700">Programs A to Z</a></li>
              <li><a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:outline-2 focus-visible:outline-emerald-700">Departments</a></li>
              <li><a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:outline-2 focus-visible:outline-emerald-700">Library</a></li>
              <li><a href="#" class="block rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:outline-2 focus-visible:outline-emerald-700">Academic calendar</a></li>
            </ul>
          </details>
        </li>
        <li><a href="#" class="block rounded-lg px-3 py-2.5 text-sm font-medium text-gray-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:outline-2 focus-visible:outline-emerald-700 md:py-2">Campus life</a></li>
        <li><a href="#" class="block rounded-lg px-3 py-2.5 text-sm font-medium text-gray-700 hover:bg-emerald-50 hover:text-emerald-900 focus-visible:outline-2 focus-visible:outline-emerald-700 md:py-2">Visit</a></li>
      </ul>
      <a href="#" class="mt-3 mb-1 block rounded-lg bg-emerald-800 px-5 py-2.5 text-center text-sm font-semibold text-white hover:bg-emerald-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-800 md:my-0">Apply now</a>
    </div>
  </nav>
</header>

Mega menu with the Popover API

  • popover
  • popovertarget
  • open:block
  • starting:open:opacity-0
  • transition-discrete

Click a menu. On a phone, open the menu button first where there is one.

The panel is a popover element opened by a button with popovertarget, so the browser handles light dismiss, Escape, the top layer and the expanded state for assistive technology. In Tailwind v4 the open: variant also matches :popover-open, which is why the panel uses open:block rather than a plain display class that would override the hidden state. starting:open: with transition-discrete fades it in, and group-has-[#tnv-mega:popover-open]: rotates the chevron.

View code
<header class="group w-full border-b border-sky-900/10 bg-[#f4f8fb]">
  <nav aria-label="Main" class="mx-auto flex max-w-6xl items-center justify-between gap-3 px-4 py-3 md:px-6">
    <a href="#" class="flex items-center gap-2 rounded-md text-lg font-semibold text-sky-950 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-700">
      <svg class="size-7" viewBox="0 0 28 28" aria-hidden="true"><circle cx="14" cy="14" r="13" fill="#0c4a6e"/><path d="M14 5l3 9-3 9-3-9Z" fill="#7dd3fc"/><path d="M14 14l3 0-3 9Z" fill="#e0f2fe"/></svg>
      Waypost
    </a>
    <ul class="flex items-center gap-1 md:mr-auto md:ml-6">
      <li>
        <button type="button" popovertarget="tnv-mega" class="flex items-center gap-1 rounded-lg px-3 py-2.5 text-sm font-semibold text-sky-950 hover:bg-sky-100 focus-visible:outline-2 focus-visible:outline-sky-700">Destinations <svg class="size-4 motion-safe:transition-transform group-has-[#tnv-mega:popover-open]:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/></svg></button>
      </li>
      <li class="hidden md:block"><a href="#" class="block rounded-lg px-3 py-2.5 text-sm font-semibold text-sky-950 hover:bg-sky-100 focus-visible:outline-2 focus-visible:outline-sky-700">Trip styles</a></li>
      <li class="hidden md:block"><a href="#" class="block rounded-lg px-3 py-2.5 text-sm font-semibold text-sky-950 hover:bg-sky-100 focus-visible:outline-2 focus-visible:outline-sky-700">Journal</a></li>
    </ul>
    <a href="#" class="hidden rounded-full bg-sky-800 px-4 py-2.5 text-sm font-semibold text-white hover:bg-sky-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-800 sm:inline-block">Start planning</a>
  </nav>
  <div id="tnv-mega" popover class="top-[4.5rem] right-3 bottom-auto left-3 mx-auto my-0 max-h-[calc(100vh-5.5rem)] w-auto max-w-4xl overflow-y-auto rounded-2xl border border-sky-900/10 bg-white p-0 text-left text-sky-950 shadow-xl open:block motion-safe:transition-all motion-safe:transition-discrete motion-safe:duration-200 starting:open:-translate-y-2 starting:open:opacity-0">
    <div class="grid gap-6 p-5 md:grid-cols-[1fr_1fr_1.15fr] md:p-6">
      <div>
        <h2 class="px-2 text-sm font-semibold text-sky-800">By region</h2>
        <ul class="mt-2">
          <li><a href="#" class="block rounded-lg px-2 py-2 text-sm text-sky-950 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700">Northern Europe</a></li>
          <li><a href="#" class="block rounded-lg px-2 py-2 text-sm text-sky-950 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700">The Mediterranean</a></li>
          <li><a href="#" class="block rounded-lg px-2 py-2 text-sm text-sky-950 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700">Alps and Dolomites</a></li>
          <li><a href="#" class="block rounded-lg px-2 py-2 text-sm text-sky-950 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700">Atlantic islands</a></li>
        </ul>
      </div>
      <div>
        <h2 class="px-2 text-sm font-semibold text-sky-800">By season</h2>
        <ul class="mt-2">
          <li><a href="#" class="block rounded-lg px-2 py-2 text-sm text-sky-950 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700">Spring walks</a></li>
          <li><a href="#" class="block rounded-lg px-2 py-2 text-sm text-sky-950 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700">Summer on the coast</a></li>
          <li><a href="#" class="block rounded-lg px-2 py-2 text-sm text-sky-950 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700">Autumn harvest towns</a></li>
          <li><a href="#" class="block rounded-lg px-2 py-2 text-sm text-sky-950 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700">Winter city breaks</a></li>
        </ul>
      </div>
      <a href="#" class="group/card block overflow-hidden rounded-xl bg-sky-950 text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">
        <span class="block overflow-hidden"><img src="canal-houses.webp" width="800" height="600" alt="Old brick houses along a canal with small boats moored at a wooden jetty" class="aspect-video w-full object-cover motion-safe:transition-transform motion-safe:duration-500 group-hover/card:scale-105"></span>
        <span class="block p-4">
          <span class="block font-semibold">Canal towns in five days</span>
          <span class="mt-1 block text-sm text-sky-100">A slow train route with small hotels and one bag.</span>
        </span>
      </a>
    </div>
    <div class="flex flex-wrap items-center justify-between gap-3 border-t border-sky-900/10 bg-sky-50 px-5 py-3 md:px-6">
      <p class="text-sm text-sky-900">Not sure where to go yet?</p>
      <a href="#" class="inline-flex items-center gap-1.5 rounded-md text-sm font-semibold text-sky-800 hover:underline focus-visible:outline-2 focus-visible:outline-sky-700">Take the trip quiz <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"/></svg></a>
    </div>
    <button type="button" popovertarget="tnv-mega" popovertargetaction="hide" class="absolute top-2 right-2 inline-flex size-11 items-center justify-center rounded-full text-sky-900 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-700 md:hidden"><span class="sr-only">Close menu</span><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg></button>
  </div>
</header>

Slide-in drawer with dialog

  • <dialog>
  • showModal()
  • backdrop:
  • starting:open:-translate-x-full
  • open:flex

Click the menu button.

The drawer is a dialog opened with showModal(), which gives it the top layer, an inert page behind it, Escape to close and focus that returns to the menu button. backdrop: dims the page, open:flex is used instead of flex so the closed dialog stays hidden, and starting:open:-translate-x-full with transition-discrete slides it in from the left. The script adds the open and close buttons and closes on a backdrop click; closedby="any" does the same natively in browsers that support it.

View code
<div class="w-full bg-[#f6f8f1]">
  <header class="border-b border-lime-900/10">
    <div class="mx-auto flex max-w-6xl items-center justify-between gap-3 px-4 py-3 md:px-6">
      <div class="flex items-center gap-2">
        <button type="button" class="tnv-open inline-flex size-11 items-center justify-center rounded-lg text-lime-950 hover:bg-lime-900/10 focus-visible:outline-2 focus-visible:outline-lime-800" aria-haspopup="dialog" aria-expanded="false" aria-controls="tnv-drawer"><span class="sr-only">Open menu</span><svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg></button>
        <a href="#" class="flex items-center gap-2 rounded-md text-lg font-semibold text-lime-950 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-lime-800"><svg class="size-6 text-lime-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M5 19c6 0 14-4 14-14C9 5 5 10 5 15v4Zm0 0 7-7"/></svg>Greenhouse Row</a>
      </div>
      <a href="#" class="relative inline-flex size-11 items-center justify-center rounded-lg text-lime-950 hover:bg-lime-900/10 focus-visible:outline-2 focus-visible:outline-lime-800"><span class="sr-only">Basket, 1 item</span><svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 10.5V6a3.75 3.75 0 1 0-7.5 0v4.5m11.356-1.993 1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 0 1-1.12-1.243l1.264-12A1.125 1.125 0 0 1 5.513 7.5h12.974c.576 0 1.059.435 1.119 1.007Z"/></svg><span aria-hidden="true" class="absolute top-1.5 right-1.5 size-2.5 rounded-full bg-lime-700 ring-2 ring-[#f6f8f1]"></span></a>
    </div>
  </header>
  <div class="mx-auto max-w-6xl px-4 py-14 md:px-6">
    <h2 class="max-w-md text-3xl font-semibold tracking-tight text-lime-950">Repotting weekend, April 12 and 13</h2>
    <p class="mt-3 max-w-md text-lime-900">Bring a plant, leave with a better pot. Free soil mix and advice at both shops.</p>
  </div>
  <dialog id="tnv-drawer" aria-labelledby="tnv-drawer-title" closedby="any" class="fixed inset-y-0 left-0 m-0 h-dvh max-h-none w-80 max-w-[85vw] -translate-x-full bg-white p-0 text-lime-950 shadow-2xl backdrop:bg-lime-950/50 open:flex open:translate-x-0 open:flex-col motion-safe:transition-[translate,display,overlay] motion-safe:transition-discrete motion-safe:duration-300 starting:open:-translate-x-full">
    <div class="flex items-center justify-between border-b border-lime-900/10 py-3 pr-3 pl-5">
      <h2 id="tnv-drawer-title" class="text-lg font-semibold">Menu</h2>
      <button type="button" class="tnv-close inline-flex size-11 items-center justify-center rounded-lg hover:bg-lime-50 focus-visible:outline-2 focus-visible:outline-lime-800"><span class="sr-only">Close menu</span><svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg></button>
    </div>
    <nav aria-label="Main" class="flex-1 overflow-y-auto px-3 py-4">
      <ul class="space-y-1">
        <li><a href="#" aria-current="page" class="flex items-center gap-3 rounded-lg px-3 py-3 text-[15px] font-medium text-lime-950 hover:bg-lime-50 focus-visible:outline-2 focus-visible:outline-lime-800 aria-[current=page]:bg-lime-100 aria-[current=page]:text-lime-900"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955a1.126 1.126 0 0 1 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>Home</a></li>
        <li><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-3 text-[15px] font-medium text-lime-950 hover:bg-lime-50 focus-visible:outline-2 focus-visible:outline-lime-800 aria-[current=page]:bg-lime-100 aria-[current=page]:text-lime-900"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M5 19c6 0 14-4 14-14C9 5 5 10 5 15v4Zm0 0 7-7"/></svg>Shop all plants</a></li>
        <li><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-3 text-[15px] font-medium text-lime-950 hover:bg-lime-50 focus-visible:outline-2 focus-visible:outline-lime-800 aria-[current=page]:bg-lime-100 aria-[current=page]:text-lime-900"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M5 9h14l-1.6 10.2A2 2 0 0 1 15.4 21H8.6a2 2 0 0 1-2-1.8L5 9Zm-1-3h16v3H4z"/></svg>Pots and planters</a></li>
        <li><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-3 text-[15px] font-medium text-lime-950 hover:bg-lime-50 focus-visible:outline-2 focus-visible:outline-lime-800 aria-[current=page]:bg-lime-100 aria-[current=page]:text-lime-900"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"/></svg>Care guides</a></li>
        <li><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-3 text-[15px] font-medium text-lime-950 hover:bg-lime-50 focus-visible:outline-2 focus-visible:outline-lime-800 aria-[current=page]:bg-lime-100 aria-[current=page]:text-lime-900"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5"/></svg>Workshops</a></li>
      </ul>
      <p class="mt-6 px-3 text-sm font-semibold text-lime-800">Your account</p>
      <ul class="mt-2 space-y-1">
        <li><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-3 text-[15px] font-medium text-lime-950 hover:bg-lime-50 focus-visible:outline-2 focus-visible:outline-lime-800 aria-[current=page]:bg-lime-100 aria-[current=page]:text-lime-900">Orders</a></li>
        <li><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-3 text-[15px] font-medium text-lime-950 hover:bg-lime-50 focus-visible:outline-2 focus-visible:outline-lime-800 aria-[current=page]:bg-lime-100 aria-[current=page]:text-lime-900">Saved plants</a></li>
        <li><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-3 text-[15px] font-medium text-lime-950 hover:bg-lime-50 focus-visible:outline-2 focus-visible:outline-lime-800 aria-[current=page]:bg-lime-100 aria-[current=page]:text-lime-900">Settings</a></li>
      </ul>
    </nav>
    <div class="border-t border-lime-900/10 p-4">
      <a href="#" class="block rounded-lg bg-lime-800 px-4 py-3 text-center text-sm font-semibold text-white hover:bg-lime-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lime-800">Book a workshop</a>
    </div>
  </dialog>
</div>

App bars and tab bars

Navigation for products people log in to: a top bar with notifications and an account menu, a command palette trigger, a breadcrumbs bar and a bottom tab bar for phone screens.

App bar with notification badge and avatar menu

  • popover
  • data-unread:
  • group-data-unread:visible
  • ring-2

Open the bell, then try Mark all as read.

The bell and the avatar each open a popover, and because both are auto popovers, opening one closes the other. The count badge is aria-hidden while an sr-only label inside the button says how many items are unread, so the number is not read twice. Unread rows carry a data-unread attribute that data-unread: and group-data-unread: style; Mark all as read removes it, removes the badge and updates the label.

View code
<header class="w-full bg-indigo-950">
  <nav aria-label="Main" class="mx-auto flex max-w-6xl items-center justify-between gap-3 px-4 py-2.5 md:px-6">
    <div class="flex items-center gap-6">
      <a href="#" class="flex items-center gap-2 rounded-md text-lg font-semibold text-white focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white">
        <svg class="size-7" viewBox="0 0 28 28" aria-hidden="true"><rect width="28" height="28" rx="8" fill="#818cf8"/><path d="M8 19V9h6a4 4 0 0 1 0 8h-6m6 0 5 4" fill="none" stroke="#1e1b4b" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
        Relay
      </a>
      <ul class="hidden items-center gap-1 md:flex">
        <li><a href="#" aria-current="page" class="block rounded-md px-3 py-2 text-sm font-medium text-indigo-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white aria-[current=page]:bg-white/10 aria-[current=page]:text-white">Dashboard</a></li>
        <li><a href="#" class="block rounded-md px-3 py-2 text-sm font-medium text-indigo-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white aria-[current=page]:bg-white/10 aria-[current=page]:text-white">Projects</a></li>
        <li><a href="#" class="block rounded-md px-3 py-2 text-sm font-medium text-indigo-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white aria-[current=page]:bg-white/10 aria-[current=page]:text-white">Team</a></li>
        <li><a href="#" class="block rounded-md px-3 py-2 text-sm font-medium text-indigo-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white aria-[current=page]:bg-white/10 aria-[current=page]:text-white">Reports</a></li>
      </ul>
    </div>
    <div class="flex items-center gap-1.5">
      <button type="button" popovertarget="tnv-notes" class="relative inline-flex size-11 items-center justify-center rounded-full text-indigo-100 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white">
        <span id="tnv-bell-label" class="sr-only">Notifications, 3 unread</span>
        <svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
        <span id="tnv-badge" aria-hidden="true" class="absolute top-1.5 right-1.5 grid h-[18px] min-w-[18px] place-items-center rounded-full bg-rose-500 px-1 text-[11px] font-bold text-white ring-2 ring-indigo-950">3</span>
      </button>
      <button type="button" popovertarget="tnv-account" class="flex items-center gap-2 rounded-full p-1 text-white hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-white md:pr-3">
        <svg class="size-9" viewBox="0 0 36 36" aria-hidden="true"><circle cx="18" cy="18" r="18" fill="#fbbf24"/><text x="18" y="23" text-anchor="middle" font-size="13" font-weight="700" fill="#451a03" font-family="system-ui, sans-serif">MK</text></svg>
        <span class="sr-only">Account menu for </span><span class="sr-only md:not-sr-only md:text-sm md:font-medium">Mira Kovac</span>
      </button>
    </div>
  </nav>
  <div id="tnv-notes" popover aria-labelledby="tnv-notes-title" class="top-16 right-3 bottom-auto left-auto m-0 w-[min(22rem,calc(100vw-1.5rem))] overflow-hidden rounded-xl border border-gray-200 bg-white p-0 text-gray-900 shadow-xl">
    <div class="flex items-center justify-between gap-2 border-b border-gray-200 px-4 py-3">
      <h2 id="tnv-notes-title" class="text-sm font-semibold">Notifications</h2>
      <button type="button" class="tnv-read rounded-md px-1 py-1 text-sm font-medium text-indigo-700 hover:underline focus-visible:outline-2 focus-visible:outline-indigo-600 disabled:text-gray-500 disabled:no-underline">Mark all as read</button>
    </div>
    <ul class="divide-y divide-gray-100">
      <li class="tnv-note group flex gap-3 px-4 py-3 data-unread:bg-indigo-50/70" data-unread><span class="invisible mt-1.5 size-2 shrink-0 rounded-full bg-indigo-600 group-data-unread:visible" aria-hidden="true"></span><div><p class="text-sm"><span class="font-semibold">Priya Shah</span> commented on Onboarding v2</p><p class="mt-0.5 text-xs text-gray-600">4 minutes ago</p></div></li>
      <li class="tnv-note group flex gap-3 px-4 py-3 data-unread:bg-indigo-50/70" data-unread><span class="invisible mt-1.5 size-2 shrink-0 rounded-full bg-indigo-600 group-data-unread:visible" aria-hidden="true"></span><div><p class="text-sm"><span class="font-semibold">Deploy 418</span> finished on staging</p><p class="mt-0.5 text-xs text-gray-600">22 minutes ago</p></div></li>
      <li class="tnv-note group flex gap-3 px-4 py-3 data-unread:bg-indigo-50/70" data-unread><span class="invisible mt-1.5 size-2 shrink-0 rounded-full bg-indigo-600 group-data-unread:visible" aria-hidden="true"></span><div><p class="text-sm"><span class="font-semibold">Tom Reyes</span> assigned you Billing page copy</p><p class="mt-0.5 text-xs text-gray-600">1 hour ago</p></div></li>
      <li class="tnv-note group flex gap-3 px-4 py-3 data-unread:bg-indigo-50/70"><span class="invisible mt-1.5 size-2 shrink-0 rounded-full bg-indigo-600 group-data-unread:visible" aria-hidden="true"></span><div><p class="text-sm">Weekly report for <span class="font-semibold">Q3 launch</span> is ready</p><p class="mt-0.5 text-xs text-gray-600">Yesterday</p></div></li>
    </ul>
    <a href="#" class="block border-t border-gray-200 px-4 py-3 text-center text-sm font-medium text-indigo-700 hover:bg-gray-50 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600">View all notifications</a>
  </div>
  <div id="tnv-account" popover aria-label="Account" class="top-16 right-3 bottom-auto left-auto m-0 w-60 rounded-xl border border-gray-200 bg-white p-0 text-gray-900 shadow-xl">
    <div class="border-b border-gray-200 px-4 py-3">
      <p class="text-sm font-semibold">Mira Kovac</p>
      <p class="truncate text-sm text-gray-600">mira@example.com</p>
    </div>
    <ul class="p-1.5">
      <li><a href="#" class="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-950 focus-visible:outline-2 focus-visible:outline-indigo-600"><svg class="size-5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"/></svg>Your profile</a></li>
      <li><a href="#" class="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-950 focus-visible:outline-2 focus-visible:outline-indigo-600"><svg class="size-5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 0 1 1.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.559.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.894.149c-.424.07-.764.383-.929.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 0 1-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.398.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 0 1-.12-1.45l.527-.737c.25-.35.272-.806.108-1.204-.165-.397-.506-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.108-1.204l-.526-.738a1.125 1.125 0 0 1 .12-1.45l.773-.773a1.125 1.125 0 0 1 1.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894ZM15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/></svg>Settings</a></li>
      <li><a href="#" class="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 hover:text-gray-950 focus-visible:outline-2 focus-visible:outline-indigo-600"><svg class="size-5 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Z"/></svg>Billing</a></li>
    </ul>
    <div class="border-t border-gray-200 p-1.5">
      <button type="button" class="w-full rounded-lg px-3 py-2 text-left text-sm font-medium text-rose-700 hover:bg-rose-50 focus-visible:outline-2 focus-visible:outline-rose-600">Sign out</button>
    </div>
  </div>
</header>

App top bar with a command search button

  • <dialog>
  • md:not-sr-only
  • backdrop:
  • aria-keyshortcuts

Click the search button or press Ctrl+K, then type.

The wide search button opens a dialog command palette instead of holding a real input, so the bar stays compact and the palette gets the whole screen. On phones the label shrinks to an icon with sr-only, and md:not-sr-only brings it back from 768px. The script opens the dialog on click or Ctrl/Cmd+K, filters the list with the hidden attribute, announces the result count in a status region and moves through results with the arrow keys.

View code
<div class="w-full bg-slate-900">
  <header class="border-b border-slate-800 bg-slate-950">
    <div class="mx-auto flex max-w-6xl items-center gap-3 px-4 py-2.5 md:gap-6 md:px-6">
      <a href="#" class="flex items-center gap-2 rounded-md font-semibold text-white focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-emerald-400">
        <svg class="size-7" viewBox="0 0 28 28" aria-hidden="true"><rect width="28" height="28" rx="7" fill="#10b981"/><path d="M7 9h14M7 14h14M7 19h14M11 6v16M17 6v16" stroke="#022c22" stroke-width="1.6"/></svg>
        Gridline
      </a>
      <nav aria-label="Main" class="hidden md:block">
        <ul class="flex gap-1">
          <li><a href="#" aria-current="page" class="block rounded-md px-3 py-2 text-sm font-medium text-slate-300 hover:bg-slate-800 hover:text-white focus-visible:outline-2 focus-visible:outline-emerald-400 aria-[current=page]:bg-slate-800 aria-[current=page]:text-white">Builds</a></li>
          <li><a href="#" class="block rounded-md px-3 py-2 text-sm font-medium text-slate-300 hover:bg-slate-800 hover:text-white focus-visible:outline-2 focus-visible:outline-emerald-400">Deploys</a></li>
          <li><a href="#" class="block rounded-md px-3 py-2 text-sm font-medium text-slate-300 hover:bg-slate-800 hover:text-white focus-visible:outline-2 focus-visible:outline-emerald-400">Logs</a></li>
        </ul>
      </nav>
      <button type="button" class="tnv-cmd ml-auto flex h-11 w-11 items-center justify-center gap-2 rounded-lg border border-slate-700 bg-slate-900 text-sm text-slate-300 hover:border-slate-500 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-400 md:w-72 md:justify-start md:px-3" aria-haspopup="dialog" aria-controls="tnv-palette" aria-keyshortcuts="Control+K Meta+K">
        <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/></svg>
        <span class="sr-only md:not-sr-only">Search or jump to</span>
        <kbd class="ml-auto hidden rounded border border-slate-700 px-1.5 font-sans text-xs text-slate-400 md:inline">Ctrl K</kbd>
      </button>
      <a href="#" class="rounded-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-400"><span class="sr-only">Your account</span><svg class="size-9" viewBox="0 0 36 36" aria-hidden="true"><circle cx="18" cy="18" r="18" fill="#334155"/><text x="18" y="23" text-anchor="middle" font-size="13" font-weight="700" fill="#e2e8f0" font-family="system-ui, sans-serif">AL</text></svg></a>
    </div>
  </header>
  <div class="mx-auto max-w-6xl px-4 py-8 md:px-6">
    <h2 class="text-lg font-semibold text-white">Recent builds</h2>
    <ul class="mt-4 divide-y divide-slate-800 rounded-xl border border-slate-800">
      <li class="flex items-center justify-between gap-3 px-4 py-3 text-sm"><span class="text-slate-200">main, fix cache headers</span><span class="rounded-full bg-emerald-400/10 px-2.5 py-0.5 text-xs font-medium text-emerald-300">Passed</span></li>
      <li class="flex items-center justify-between gap-3 px-4 py-3 text-sm"><span class="text-slate-200">feature/onboarding</span><span class="rounded-full bg-amber-400/10 px-2.5 py-0.5 text-xs font-medium text-amber-300">Running</span></li>
      <li class="flex items-center justify-between gap-3 px-4 py-3 text-sm"><span class="text-slate-200">release 3.12.0</span><span class="rounded-full bg-rose-400/10 px-2.5 py-0.5 text-xs font-medium text-rose-300">Failed</span></li>
    </ul>
  </div>
  <dialog id="tnv-palette" aria-label="Command palette" closedby="any" class="mx-auto mt-16 w-[min(36rem,calc(100%-2rem))] max-w-none rounded-xl border border-slate-700 bg-slate-900 p-0 text-slate-100 shadow-2xl backdrop:bg-slate-950/70">
    <div class="flex items-center gap-2 border-b border-slate-700 px-4 focus-within:bg-slate-800/50">
      <svg class="size-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/></svg>
      <label for="tnv-cmd-input" class="sr-only">Search commands and pages</label>
      <input id="tnv-cmd-input" type="text" autocomplete="off" placeholder="Type a command or page" aria-controls="tnv-cmd-list" class="h-14 w-full bg-transparent text-base text-white placeholder:text-slate-400 focus:outline-hidden">
      <kbd class="rounded border border-slate-700 px-1.5 font-sans text-xs text-slate-400">Esc</kbd>
    </div>
    <ul id="tnv-cmd-list" class="max-h-72 overflow-y-auto p-2">
      <li class="tnv-cmd-item"><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-slate-200 hover:bg-slate-800 hover:text-white focus-visible:bg-slate-800 focus-visible:text-white focus-visible:outline-2 focus-visible:outline-emerald-400"><svg class="size-5 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"/></svg>Go to builds<span class="ml-auto text-xs text-slate-400">Page</span></a></li>
      <li class="tnv-cmd-item"><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-slate-200 hover:bg-slate-800 hover:text-white focus-visible:bg-slate-800 focus-visible:text-white focus-visible:outline-2 focus-visible:outline-emerald-400"><svg class="size-5 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M15.59 14.37a6 6 0 0 1-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 0 0 6.16-12.12A14.98 14.98 0 0 0 9.631 8.41m5.96 5.96a14.926 14.926 0 0 1-5.841 2.58m-.119-8.54a6 6 0 0 0-7.381 5.84h4.8m2.581-5.84a14.927 14.927 0 0 0-2.58 5.84m2.699 2.7c-.103.021-.207.041-.311.06a15.09 15.09 0 0 1-2.448-2.448 14.9 14.9 0 0 1 .06-.312m-2.24 2.39a4.493 4.493 0 0 0-1.757 4.306 4.493 4.493 0 0 0 4.306-1.758M16.5 9a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z"/></svg>Go to deploys<span class="ml-auto text-xs text-slate-400">Page</span></a></li>
      <li class="tnv-cmd-item"><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-slate-200 hover:bg-slate-800 hover:text-white focus-visible:bg-slate-800 focus-visible:text-white focus-visible:outline-2 focus-visible:outline-emerald-400"><svg class="size-5 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"/></svg>Open logs<span class="ml-auto text-xs text-slate-400">Page</span></a></li>
      <li class="tnv-cmd-item"><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-slate-200 hover:bg-slate-800 hover:text-white focus-visible:bg-slate-800 focus-visible:text-white focus-visible:outline-2 focus-visible:outline-emerald-400"><svg class="size-5 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>Create a project<span class="ml-auto text-xs text-slate-400">Action</span></a></li>
      <li class="tnv-cmd-item"><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-slate-200 hover:bg-slate-800 hover:text-white focus-visible:bg-slate-800 focus-visible:text-white focus-visible:outline-2 focus-visible:outline-emerald-400"><svg class="size-5 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"/></svg>Invite a teammate<span class="ml-auto text-xs text-slate-400">Action</span></a></li>
      <li class="tnv-cmd-item"><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-slate-200 hover:bg-slate-800 hover:text-white focus-visible:bg-slate-800 focus-visible:text-white focus-visible:outline-2 focus-visible:outline-emerald-400"><svg class="size-5 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"/></svg>Rotate deploy key<span class="ml-auto text-xs text-slate-400">Action</span></a></li>
      <li class="tnv-cmd-item"><a href="#" class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-slate-200 hover:bg-slate-800 hover:text-white focus-visible:bg-slate-800 focus-visible:text-white focus-visible:outline-2 focus-visible:outline-emerald-400"><svg class="size-5 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Z"/></svg>Billing settings<span class="ml-auto text-xs text-slate-400">Page</span></a></li>
    </ul>
    <p id="tnv-cmd-empty" class="hidden px-5 pb-5 text-sm text-slate-400">No matches. Try another word.</p>
    <p id="tnv-cmd-count" role="status" class="sr-only"></p>
  </dialog>
</div>

Bottom tab bar for a mobile app

  • grid-cols-5
  • group-aria-[current=page]:
  • env(safe-area-inset-bottom)
  • min-h-14

Tap the tabs.

A five-column grid gives every tab the same width and a 56px tall target, and the current tab is marked with aria-current so group-aria-[current=page]: can draw the pill behind its icon. The middle action is a real button, not a tab, and lifts out of the bar with a negative margin. pb-[max(0.5rem,env(safe-area-inset-bottom))] keeps the labels above the home indicator on notched phones.

View code
<div class="w-full bg-indigo-50 px-4 py-8">
  <div class="mx-auto flex h-[34rem] w-full max-w-sm flex-col overflow-hidden rounded-[2rem] border-8 border-slate-900 bg-slate-50 shadow-xl">
    <div class="flex items-center justify-between px-5 pt-5 pb-3">
      <h2 id="tnv-app-title" class="text-2xl font-bold text-slate-900">Home</h2>
      <svg class="size-9" viewBox="0 0 36 36" aria-hidden="true"><circle cx="18" cy="18" r="18" fill="#c7d2fe"/><text x="18" y="23" text-anchor="middle" font-size="13" font-weight="700" fill="#312e81" font-family="system-ui, sans-serif">SN</text></svg>
    </div>
    <div class="flex-1 space-y-3 overflow-y-auto px-5 pb-4">
      <div class="rounded-2xl bg-white p-4 shadow-xs ring-1 ring-slate-200">
        <p class="text-sm text-slate-600">This week</p>
        <p class="mt-1 text-2xl font-semibold text-slate-900">18.4 <span class="text-base font-medium text-slate-600">of 25 km</span></p>
        <div class="mt-3 h-2 rounded-full bg-slate-100"><div class="h-2 w-[74%] rounded-full bg-indigo-600"></div></div>
      </div>
      <div class="rounded-2xl bg-white p-4 shadow-xs ring-1 ring-slate-200">
        <p class="text-sm text-slate-600">Next session</p>
        <p class="mt-1 font-semibold text-slate-900">Tempo run, 6 km</p>
        <p class="text-sm text-slate-600">Thursday at 7:00</p>
      </div>
      <div class="rounded-2xl bg-indigo-600 p-4 text-white">
        <p class="text-sm text-indigo-100">Streak</p>
        <p class="mt-1 text-2xl font-semibold">12 days</p>
      </div>
    </div>
    <nav aria-label="Primary" class="border-t border-slate-200 bg-white px-2 pt-1.5 pb-[max(0.5rem,env(safe-area-inset-bottom))]">
      <ul class="grid grid-cols-5 items-end">
        <li><a href="#" aria-current="page" data-title="Home" class="tnv-tab group flex min-h-14 flex-col items-center justify-center gap-0.5 rounded-xl text-[11px] font-medium text-slate-600 hover:text-slate-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600 aria-[current=page]:text-indigo-700"><span class="relative grid h-7 w-12 place-items-center rounded-full group-aria-[current=page]:bg-indigo-100 motion-safe:transition-colors"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955a1.126 1.126 0 0 1 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg></span>Home</a></li>
        <li><a href="#" data-title="Activity" class="tnv-tab group flex min-h-14 flex-col items-center justify-center gap-0.5 rounded-xl text-[11px] font-medium text-slate-600 hover:text-slate-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600 aria-[current=page]:text-indigo-700"><span class="relative grid h-7 w-12 place-items-center rounded-full group-aria-[current=page]:bg-indigo-100 motion-safe:transition-colors"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z"/></svg></span>Activity</a></li>
        <li class="flex justify-center"><button type="button" class="-mt-6 grid size-14 place-items-center rounded-full bg-indigo-600 text-white shadow-lg ring-4 ring-white hover:bg-indigo-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"><span class="sr-only">Record a workout</span><svg class="size-7" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg></button></li>
        <li><a href="#" data-title="Inbox" class="tnv-tab group flex min-h-14 flex-col items-center justify-center gap-0.5 rounded-xl text-[11px] font-medium text-slate-600 hover:text-slate-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600 aria-[current=page]:text-indigo-700"><span class="relative grid h-7 w-12 place-items-center rounded-full group-aria-[current=page]:bg-indigo-100 motion-safe:transition-colors"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.076-4.076a1.526 1.526 0 0 1 1.037-.443 48.282 48.282 0 0 0 5.68-.494c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z"/></svg><span aria-hidden="true" class="absolute top-0 right-1.5 min-w-4 rounded-full bg-rose-600 px-1 text-center text-[10px] leading-4 font-bold text-white">3</span></span>Inbox<span class="sr-only">, 3 unread</span></a></li>
        <li><a href="#" data-title="Profile" class="tnv-tab group flex min-h-14 flex-col items-center justify-center gap-0.5 rounded-xl text-[11px] font-medium text-slate-600 hover:text-slate-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600 aria-[current=page]:text-indigo-700"><span class="relative grid h-7 w-12 place-items-center rounded-full group-aria-[current=page]:bg-indigo-100 motion-safe:transition-colors"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"/></svg></span>Profile</a></li>
      </ul>
    </nav>
  </div>
</div>

How to build a basic Tailwind navbar with flexbox utilities

Everything starts with a nav element. Flexbox handles the layout, spacing and width utilities size it, and variants such as md: and hover: add the behavior on top.

Parts of the navbar

The nav element wraps the bar and gives screen readers a navigation landmark. The logo link sits on the left and points home. Links belong in a ul of li and a elements, not loose anchors.

On the right goes an action area, usually a log-in or sign-up button, sometimes a search field. Last comes a hamburger button that only shows below the md breakpoint.

The action area should hold one primary call-to-action button and nothing competing with it.

Steps to build the layout

  1. Wrap everything in a nav element with a background and a light shadow, such as bg-white shadow-sm.
  2. Add an inner div with mx-auto max-w-7xl px-4 to center the content and set the side gutters.
  3. Turn that div into a row with flex items-center justify-between.
  4. Place the logo link first, then one div with flex items-center gap-6 that holds the link list and the button.
  5. Style the button last, with rounded-md px-4 py-2 and a filled background.
<nav class="bg-white shadow-sm">
  <div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-3">
    <a href="/" class="text-lg font-semibold">Brand</a>
    <div class="flex items-center gap-6">
      <ul class="flex gap-6 text-sm">
        <li><a href="/features">Features</a></li>
        <li><a href="/pricing">Pricing</a></li>
      </ul>
      <a href="/signup" class="rounded-md bg-indigo-600 px-4 py-2 text-sm text-white">Sign up</a>
    </div>
  </div>
</nav>

That right-hand wrapper matters. Without it you’d have three direct children, and justify-between would push the links into the middle of the bar.

The ul isn’t decoration either. Bare anchors look identical on screen, but a screen reader gets no list to count.

How do you make a Tailwind navbar responsive?

The link list gets hidden below the md breakpoint with hidden md:flex. The toggle button does the reverse with md:hidden, and a click flips the hidden class on the mobile menu. Breakpoint prefixes are min-width based, so md:flex applies from 768px upward.

Showing and hiding links with breakpoint prefixes

Unprefixed utilities apply at every width, and prefixed ones apply at that breakpoint and above. That mobile-first approach means the base classes on the navbar describe the phone layout, and the md: classes rewrite it for wider screens.

The Tailwind docs flag the usual trap, which is that sm: starts at 640px and isn’t a phone-only prefix.

The defaults in the Tailwind CSS docs (v4.3) run like this.

  • sm: 40rem (640px)
  • md: 48rem (768px)
  • lg: 64rem (1024px)
  • xl: 80rem (1280px)
  • 2xl: 96rem (1536px)

With a long link list, switch at lg instead of md, because the row runs out of room at 768px first.

Custom breakpoints go through --breakpoint-* theme variables. The docs warn that mixing units, say px next to the default rem values, can sort the generated classes in the wrong order.

Hamburger toggle with vanilla JavaScript

A button opens a panel that starts hidden, and a short click handler flips the hidden class.

<button id="menu-btn" type="button" class="md:hidden" aria-controls="mobile-menu" aria-expanded="false">Menu</button>

<div id="mobile-menu" class="hidden md:hidden">
<a href="/features" class="block px-4 py-2">Features</a>
<a href="/pricing" class="block px-4 py-2">Pricing</a>
</div>

<script>
const btn = document.getElementById("menu-btn");
const menu = document.getElementById("mobile-menu");

btn.addEventListener("click", () => {
  const isHidden = menu.classList.toggle("hidden");
  btn.setAttribute("aria-expanded", String(!isHidden));
});
</script>

The button usually shows the classic hamburger menu icon, swapped for a close icon while the panel is open. Heroicons, the icon set from Tailwind Labs, ships both as inline SVG.

The setAttribute line mirrors the open state into aria-expanded, so a screen reader announces what a sighted user sees.

There are zero dependencies here, though close-on-link-click and Escape handling are yours to write.

Hamburger toggle with Alpine.js

Alpine.js needs x-data="{ open: false }" on the nav, @click="open = !open" on the button, and x-show="open" on the mobile panel. The price is one extra script tag.

Component libraries ask for a script too, since Flowbite’s interactive parts run on flowbite.js and Material Tailwind’s navbar docs require collapse.js.

It pays off on pages that already use Alpine for other small interactions. Without that, the vanilla version above is shorter.

Sticky vs fixed positioning in a Tailwind navbar

Use sticky top-0 for most navbars, because the bar keeps its place in the document flow and nothing slides underneath it. fixed only makes sense when the bar has to float over a hero image, and then the content below needs matching padding.

UtilityDocument flowOffsets measured fromContent below needs padding
stickyStays in flowIts normal positionNo
fixedLeaves the flowThe browser windowYes

According to the Tailwind position docs (v4.3), sticky acts like relative until it crosses its threshold, then like fixed until its parent leaves the screen. That makes a sticky navigation bar the lower-risk default.

Sticky breaks in a few predictable ways. It does nothing without top-0 or another inset. A wrapper that is barely taller than the bar lets it scroll away with the wrapper. And MDN’s position reference says sticky follows the nearest ancestor with a scrolling mechanism, which overflow hidden, scroll, auto, or overlay creates, even if that ancestor never scrolls.

A clipping wrapper added for rounded corners is a common offender.

Stacking works differently from what most people expect. Sticky and fixed elements form their own stacking context, so z-50 on the nav places the whole bar, open dropdowns included, relative to the rest of the page.

For a frosted bar, pair a translucent background with backdrop-blur-md, which applies a 12px blur (Tailwind docs, v4.3). The look is known as glassmorphism, and the docs show it with a background such as bg-white/30.

The same docs show a fallback pattern: a mostly opaque background by default, then a lighter one plus the blur under supports-backdrop-filter:. For a fully transparent bar over a hero, use bg-transparent and swap in a solid background after scroll with a few lines of JavaScript.

How to add a dropdown menu to a Tailwind navbar

The trigger and the panel share a relative parent. The panel is absolute and hidden by default, and group-hover:block reveals it on hover (a toggled class does the job for click). top-full keeps it flush under the trigger.

The same structure works for a standalone dropdown built with Tailwind, with the navbar adding only the horizontal layout.

Hover dropdowns with group-hover

<div class="group relative">
  <a href="#" class="px-3 py-2">Products</a>
  <div class="absolute left-0 top-full hidden min-w-48 rounded-md bg-white shadow-lg group-hover:block">
    <a href="#" class="block px-4 py-2">Analytics</a>
    <a href="#" class="block px-4 py-2">Reports</a>
  </div>
</div>

Mark the wrapper with group and style the panel with group-hover, the parent-state variant described in the Tailwind state variants docs. When two dropdowns nest, named groups such as group/item and group-hover/item keep them apart.

Watch the gap. A margin between trigger and panel breaks the hover path, and the menu closes as the pointer crosses it. Use top-full with no margin, or pad inside the panel.

Touch devices are a separate problem. As of v4.3, the docs list the hover: variant as compiling inside @media (hover: hover), so touch-only devices skip those styles. A dropdown that opens on hover alone needs a tap path, so test it on a phone. Honestly, I wouldn’t ship a hover-only menu on anything public.

Click dropdowns with a state toggle

The button toggles the hidden class on the panel and flips the aria-expanded value. A document click listener closes the panel when menu.contains(event.target) is false, and a keydown listener handles Escape by closing the panel and returning focus to the trigger.

To keep one source of truth, put aria-expanded on the button, mark the button peer, and style the panel with hidden peer-aria-expanded:block. The docs include aria-expanded among the default ARIA variants, and peer-* only reaches later siblings, so the panel has to follow the button in the markup.

Headless UI Menu

Tailwind Labs introduced Headless UI in October 2020 on the pitch that a hand-built dropdown with keyboard navigation, ARIA management, and focus handling takes 3 to 4 weeks to get right (Tailwind Labs announcement, October 6, 2020). That is a vendor claim, but the problem it names is real.

The Menu component, available for React and Vue, supplies that behavior unstyled, and Tailwind classes handle the look. Per the Headless UI docs, disabled items are skipped by the arrow keys.

Picking a dropdown approach

group-hover needs no JavaScript and only two classes. The catch is that touch devices get no tap path, and keyboard users need group-focus-within:block added.

A click toggle works on touch and keyboard, and the state is visible in aria-expanded. Outside-click and Escape handling are yours to write, though.

Headless UI Menu handles keyboard, ARIA, and focus for you, at the cost of needing React or Vue and one more package.

A marketing site with one small dropdown can live with the click toggle. An app with several menus and keyboard-heavy users should reach for Headless UI.

Active link styles and dark mode in a Tailwind navbar

Mark the current page with aria-current="page" and style it through the aria-[current=page]: variant, then add dark: variants to the same elements. The dark: variant follows the operating system theme by default, and a one-line custom variant switches it to a class or data attribute for manual toggles.

Active and hover states

A nav link has to look right in its default, hover, keyboard focus, and current states.

<a href="/pricing" aria-current="page"
  class="text-gray-600 hover:text-gray-900
  focus-visible:outline-2 focus-visible:outline-indigo-500
  aria-[current=page]:font-semibold aria-[current=page]:text-indigo-600">
Pricing
  </a>

Using aria-current as the styling hook ties the visual state and the screen reader announcement to one attribute. MDN lists page as the value for the current page within a set of links.

The focus-visible: variant targets keyboard focus (Tailwind docs, v4.3). The same variants cover broader link styles, such as visited: colors and underline offsets, when a navbar needs them.

Dark mode in the navbar

Pair each light class with a dark: override on the same element, for example bg-white dark:bg-gray-900 on the nav and text-gray-600 dark:text-gray-300 on links.

The system theme is the default, so dark: maps to prefers-color-scheme: dark with no setup. For a manual toggle, add @custom-variant dark (&:where(.dark, .dark *)); to the CSS and put the dark class on the html element. A data attribute works the same way, just with [data-theme=dark] selectors instead of the class.

The setup changed between versions. Tailwind v3.4.1 replaced the class strategy with darkMode: 'selector' in tailwind.config.js (v3 docs), while v4.3 does the same job in CSS with @custom-variant (v4.3 docs). Older tutorials that open a config file and set darkMode describe the earlier setup.

A three-way toggle (light, dark, system) reads localStorage.theme and window.matchMedia("(prefers-color-scheme: dark)"). The docs recommend running that script inline in the head, so the correct theme is set before first paint.

Recheck the contrast between link and background colors in both themes. A muted gray that reads fine on white can fail on a dark background.

Making a Tailwind navbar accessible

The W3C disclosure pattern is the one to build on. That means a named nav landmark, toggle buttons carrying aria-expanded and aria-controls, Escape to close, and visible keyboard focus. The sticky bar is where things go wrong most often, since it can’t be allowed to hide the focused link.

Landmark, names, and the current page

Wrap the links in a nav element, and add an aria-label when the page has more than one, such as header and footer navigation. An icon-only hamburger button needs a text name, so put <span class="sr-only">Open main menu</span> inside it and mark the SVG aria-hidden="true".

The active link gets aria-current="page", the same attribute the W3C navigation example uses. A dropdown’s links go in a nested ul inside the li that owns it, so assistive technology reads the hierarchy correctly.

Stop there for a link-only bar. The APG page repeats its own warning that “No ARIA is better than Bad ARIA”, and every extra ARIA attribute is one more thing to keep in sync.

Keyboard behavior

The W3C disclosure navigation example (page updated January 2026) documents the keys below.

KeyResult
Tab, Shift+TabMoves between top-level buttons, then into the links of an open dropdown
Enter, SpaceToggles the dropdown when a disclosure button has focus
EscapeCloses an open dropdown and returns focus to its button
Arrow keys, Home, EndOptional extras that supplement Tab

Moving focus out of the navigation region also closes an open dropdown. The APG states that the Escape behavior is needed to meet WCAG 2.1 success criterion 1.4.13, Content on Hover or Focus.

A CSS-only group-hover dropdown has no Escape handler, so it needs a small keydown listener before it passes that check.

Keeping a sticky bar from covering focus

WCAG 2.2 added success criterion 2.4.11, Focus Not Obscured (Minimum), at Level AA. A focused component must not be entirely hidden by author-created content.

The W3C understanding document names sticky headers as a typical cause. It lists failure F110 for a sticky header that completely hides focused elements, and CSS scroll padding as the sufficient technique (C43).

  1. Measure the rendered height of the sticky bar.
  2. Put a scroll-pt-* utility on the html element at or above that height, for example scroll-pt-20 (5rem).
  3. Tab through the page and confirm each link stays at least partly visible below the bar.

A frosted bar adds a second check. The W3C notes that blur or semi-transparent overlays can fail 1.4.11 Non-text Contrast even when focus is not hidden, so test the focus ring against whatever scrolls behind the bar.

Both criteria are only a slice of the web accessibility requirements, so run a full audit before launch.

Menu role or disclosure for dropdown links

Two authoritative sources point to different patterns for navbar dropdowns.

The W3C APG says site navigation should use disclosure buttons and plain links, not the menu role, because typical site navigation does not need the keyboard behavior that role promises.

Headless UI goes the other way for Menu, which is built on the menu role. Its v1 docs say content should be restricted to a list of links or buttons, that focus is trapped in an open menu, and that arrow keys move between items. The current Popover docs call Popover a fit for floating panels such as navigation menus, mobile menus, and flyout menus, and the v1 Menu docs name Popover for header navigation items with expandable content.

Use Menu for action lists such as profile options and sign out. Use Popover, Disclosure, or plain disclosure markup for a Products dropdown full of destination links.

That limits Headless UI Menu to action lists inside a navbar. The library still covers navigation through Popover and Disclosure.

Building a Tailwind navbar in React, Vue, and Next.js

Component state replaces the class toggle. React uses useState, Vue uses a ref, and Next.js uses the same React state inside a Client Component. The markup and utility classes stay identical, so only the toggle and the active-link logic change.

Project setup is the same as in any project that uses Tailwind CSS in React, so the sections below stick to navbar logic.

React: state replaces the class toggle

The open state lives in useState, and aria-expanded reads from it directly, so the attribute and the panel cannot drift apart.

import { useState } from "react";

export default function Navbar() {
  const [open, setOpen] = useState(false);

  return (
  <nav className="bg-white shadow-sm">
  <div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-3">
  <a href="/" className="text-lg font-semibold">Brand</a>
  <button
  type="button"
  className="md:hidden"
  aria-controls="mobile-menu"
  aria-expanded={open}
  onClick={() => setOpen(!open)}
  >
  <span className="sr-only">Open main menu</span>
  </button>
  </div>
  {open && (
    <div id="mobile-menu" className="md:hidden">
    <a href="/pricing" className="block px-4 py-2" onClick={() => setOpen(false)}>
    Pricing
    </a>
    </div>
    )}
  </nav>
  );
}

Pasted markup needs the JSX treatment, so class becomes className and every tag must close. An HTML to JSX converter handles that in one pass.

Each link in the mobile panel calls setOpen(false), so the panel closes after a click.

Vue: v-show or v-if for the mobile menu

v-show keeps the element in the DOM and toggles its display CSS property, which makes toggles cheap but the initial render heavier. v-if destroys and recreates the block on every toggle, so toggles cost more and the first render is lighter, per the Vue docs.

A mobile menu opens and closes constantly, so v-show is the better default.

Drop the hidden class when v-show runs the toggle. v-show sets an inline display value and removes it when true, so a leftover hidden class would keep the panel invisible.

Next.js: Link and the active route

The Link component from next/link is the main way to move between routes, with prefetching built in. For active-link checks, usePathname() from next/navigation returns the current path, and because that is a hook, the navbar has to be a Client Component ("use client").

Feed the result into the attribute with aria-current={pathname === href ? "page" : undefined}. The aria-[current=page]: classes then apply without any extra conditional class logic.

Headless UI Disclosure and shadcn/ui

Headless UI Disclosure is a show and hide primitive for React and Vue. The docs call it useful for mobile menus that should close when a link is followed, through a CloseButton component or the close render prop.

The shadcn/ui NavigationMenu is built on Radix UI and added with npx shadcn@latest add navigation-menu. Wrap a Next.js Link with NavigationMenuLink asChild, as the docs show.

Reach for shadcn/ui only when the project already uses it. A standalone marketing site does not need the dependency.

Tailwind navbar component sources compared

As of October 2026, Flowbite, daisyUI, HyperUI, and Material Tailwind all publish navbar examples, while Tailwind Plus no longer takes new customers. Tailwind Labs said on September 9, 2026 that it is joining Shopify and closing Tailwind Plus sign-ups, and existing customers keep access.

SourceCostProCon
Tailwind PlusOne-time license, closed to new buyersExisting licenses keep workingNo new sign-ups
FlowbiteOpen source, paid Pro tierWorks with Tailwind v3 and v4Needs its JavaScript; Vue apps call initFlowbite()
daisyUIFree, open sourceShort navbar class namesVersion 5 requires Tailwind CSS 4
HyperUIFree, MIT licenseNothing to installCopied code never updates
Material TailwindFree blocks, paid PRO tierHTML and React examplesMore variants behind PRO

The daisyUI navbar docs still build the responsive switch from plain Tailwind prefixes, lg:hidden on the toggle and hidden lg:flex on the center menu. Its submenu example uses a native details and summary pair.

HyperUI’s FAQ says many components include dark mode variants, but not all, and some interactive components ship a little vanilla JavaScript.

For one landing page, write the markup from this article or copy a HyperUI block, since a plugin adds weight to a single header. A site with many pages and one look suits daisyUI, because its navbar classes keep repeated markup short.

Flowbite fits when you want interactive parts without writing state, as long as the project can load its JavaScript. In a React app with a design system, go with shadcn/ui NavigationMenu or Headless UI Popover.

A team that already holds a Tailwind Plus license should keep using it. Nothing in the announcement revokes existing access.

When a hand-built Tailwind navbar stops being the right choice

Hand-built markup stops paying off once the navbar needs three or more menu levels, a mega menu, or identical behavior across several frameworks and teams. Past that point the markup isn’t the hard part. Focus handling is.

Deep menus are the first wall. On July 2, 2024, the W3C APG task force decided to extend its disclosure navigation example with top-level links to a third level (aria-practices issue #3045), but that published example with top-level links, last updated August 12, 2025, still shows two levels, so there is no official three-level pattern to copy.

Library versions drift too. Flowbite’s quickstart now gives full steps only for Tailwind v4 and tells v3 projects to upgrade first, and daisyUI 5 installs with an @plugin line in CSS that v3 projects do not use.

Inside a framework, data-attribute libraries must start after mount, which is why Flowbite’s Vue guide calls initFlowbite() inside onMounted.

And a static navbar pasted into every page drifts as links change, so move it into a framework component or a template partial.

Once the navbar must match tokens and components shared across products, it belongs in a design system rather than in utility classes copied between repositories.

For a single header with one dropdown level, hand-built markup stays the better call. Fewer dependencies, smaller HTML, and nothing to re-initialize.

Tailwind Navbar FAQ

How do I center the logo in a Tailwind navbar?

Give the container a three-column grid with grid grid-cols-3 items-center. Put the links in the first column, the logo in the second with justify-self-center, and the actions in the third with justify-self-end.

Equal columns keep the logo centered no matter how many links sit on either side.

How do I add a search box to a Tailwind navbar?

Place a form with role=”search” in the action area and give its input a label, even if the label is visually hidden with sr-only.

Show the form from the md breakpoint with hidden md:block, and repeat it inside the mobile panel, because the narrow bar has no room for it.

Why does the mobile menu stay open after I click a link?

Same-page anchor links do not reload the document, so nothing resets the panel. Attach a click listener to each link that runs the same close function as the toggle button. That function restores the hidden class and sets aria-expanded to false.

Do Tailwind v3 navbar examples still work in v4?

Mostly yes, with some visual drift. Tailwind’s v4 upgrade guide renamed shadow-sm to shadow-xs, rounded to rounded-sm, outline-none to outline-hidden, and ring to ring-3.

A v3 navbar renders, but its shadows, corners, and focus rings change. Run the official upgrade tool, then review the bar by eye.

What to Test Before a Tailwind Navbar Ships

Before launch, the navbar gets a full keyboard pass, a focus check beneath the sticky bar, and a screen reader pass on the mobile toggle.

Run them in that order. Keyboard failures stop users from operating the bar at all, so they outrank problems that only look wrong. In practice the first pass is Tab, Enter, Space, and Escape through every dropdown.

Screen reader testing comes last because it needs stable markup and takes the longest. The W3C APG treats testing with assistive technologies as a prerequisite for production use of its own example code.

These priorities reflect sources checked on October 29, 2026. WCAG 2.2 guidance and APG patterns get revised, so repeat the sequence after each redesign.

A web accessibility checklist extends the same pass to the rest of the site.