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
- Wrap everything in a
nav element with a background and a light shadow, such as bg-white shadow-sm. - Add an inner div with
mx-auto max-w-7xl px-4 to center the content and set the side gutters. - Turn that div into a row with
flex items-center justify-between. - Place the logo link first, then one div with
flex items-center gap-6 that holds the link list and the button. - 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.
| Utility | Document flow | Offsets measured from | Content below needs padding |
|---|
| sticky | Stays in flow | Its normal position | No |
| fixed | Leaves the flow | The browser window | Yes |
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.
| Key | Result |
|---|
| Tab, Shift+Tab | Moves between top-level buttons, then into the links of an open dropdown |
| Enter, Space | Toggles the dropdown when a disclosure button has focus |
| Escape | Closes an open dropdown and returns focus to its button |
| Arrow keys, Home, End | Optional 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).
- Measure the rendered height of the sticky bar.
- Put a
scroll-pt-* utility on the html element at or above that height, for example scroll-pt-20 (5rem). - 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.
| Source | Cost | Pro | Con |
|---|
| Tailwind Plus | One-time license, closed to new buyers | Existing licenses keep working | No new sign-ups |
| Flowbite | Open source, paid Pro tier | Works with Tailwind v3 and v4 | Needs its JavaScript; Vue apps call initFlowbite() |
| daisyUI | Free, open source | Short navbar class names | Version 5 requires Tailwind CSS 4 |
| HyperUI | Free, MIT license | Nothing to install | Copied code never updates |
| Material Tailwind | Free blocks, paid PRO tier | HTML and React examples | More 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.