shadcn Header: How to Build One Step by Step (2026)

Build a production-ready shadcn header from scratch: NavigationMenu, mobile Sheet menu, dark mode toggle, sticky scroll behaviour, and a command palette — with live preview and full code.

AshFull-stack developer and the maker behind ShadcnDeck. Writes practical guides on React, Next.js, Tailwind, and shadcn/ui — the things he wishes existed when he started.
Published Aug 23, 2026
Updated Aug 23, 2026
12 min read
shadcn/uiReactNext.jsHeader ComponentNavigation MenuUI ComponentsTypeScriptTailwind CSSSticky HeaderFrontend Development
shadcn header component guide cover image showing a NavigationMenu-based header

The shadcn header is built by composing shadcn/ui's NavigationMenu primitive with Tailwind CSS, and every layer — mobile menu, dark mode, sticky scroll, search — bolts on top of the same base. This guide builds one shadcn ui header from the ground up in 5 steps, each with a live preview and full TypeScript code you can copy straight into your project.

A header is just one building block — explore all shadcn UI components for the rest of the set.

How Do You Install the shadcn NavigationMenu?

Every shadcn header in this guide starts from the same two primitives. Run both commands before you begin — the rest of the guide assumes both are already in components/ui/.

bash

This installs navigation-menu.tsx, sheet.tsx, and command.tsx to components/ui/. Both Next.js App Router and Pages Router use the identical component code below — see the router differences section near the end for where each one gets imported.

Step 1: Build the Base shadcn Header with NavigationMenu

Start with a desktop-only header: a brand mark, a NavigationMenu for the primary links, and a call-to-action button. Every later step in this guide builds on top of this file.

NavigationMenuLink wraps asChild around a Next.js Link so routing stays client-side while Radix UI still manages focus and keyboard behaviour underneath. At this stage the header has no responsive fallback — Step 2 fixes that.

🎨 Not sold on stone and emerald? Every color here is a Tailwind CSS variable under the hood. Preview a different palette across this exact header shape with the free shadcn theme generator before you touch a single class name.

Step 2: Add a Mobile Menu with the shadcn Sheet Component

A shadcn header menu needs a mobile fallback. Hide the desktop NavigationMenu below the md breakpoint and swap in a Sheet that slides in from the right.

The preview below defaults to the mobile layout so the difference from Step 1 is visible immediately — on an actual desktop-width browser, a header using real md: classes would show the desktop nav instead. Use the toggle to compare both; in your app the switch happens automatically at the md breakpoint, no toggle required.

Recent entries

Field Guide 12

Cold mornings on the Kerry coast

Film 08

Notes on shooting Portra 400 in fog

Slow Travel 05

Twelve hours in a town with one road

Close the sheet with onClick={() => setOpen(false)} on every link — without it, the drawer stays open after navigation and the user has to dismiss it manually. This is the single most common shadcn header menu bug in production.

🚀 Skip wiring this up by hand. Every ShadcnDeck template ships with a header like this one already built — sticky, mobile Sheet menu, and dark mode toggle included. Browse free shadcn/ui templates →

Step 3: Add a Dark Mode Toggle to the shadcn Header

Add a segmented pill toggle that flips dark on the header's own state. In a real app, wire this to next-themes instead of local state so the whole site — not just the header — switches together.

Note where the colours live. NavigationMenuLink ships its own hover:bg-accent hover:text-accent-foreground defaults, and asChild merges the two class strings by concatenation — not through tailwind-merge. Put your hover colour on the child <Link> and it doesn't reliably win: the component's --accent-foreground can take over instead, which in a dark header means a near-black label on a near-black bar. Set the colour on NavigationMenuLink itself — that className does go through cn(), so the defaults are properly replaced.

Swap the local useState for useTheme() from next-themes in production — install it with npm install next-themes and wrap your root layout in <ThemeProvider attribute="class">.

Launch your SaaS faster with a modern Shadcn UI template - Explore Free Templates

Step 4: Make the shadcn Header Sticky on Scroll

Track window.scrollY and shrink the header once the user scrolls past 12px. Add a backdrop blur and border so the header lifts off the page instead of blending into it.

A real sticky header watches the page's own scroll position, so if you tried that inside a long blog post it would already read as "scrolled" the moment you reached this section. The preview below scopes the same logic to its own scrollable frame instead, purely so you can see both the resting and shrunk states without losing your place on the page.

Scroll inside the frame below ↓

Keep scrolling this frame to watch the header shrink, gain a border, and pick up a blurred backdrop.

That's the same window.scrollY > 12 check from the code sample below — just scoped to this box instead of the real page, so you can see both states without leaving this section.

Recent entries

Field Guide 12

Cold mornings on the Kerry coast

Film 08

Notes on shooting Portra 400 in fog

Slow Travel 05

Twelve hours in a town with one road

Film 07

The case for the 35mm compact

Slow Travel 04

Where the last ferry still runs

Field Guide 11

Packing light for a week of rain

Scroll back up to reset the header to its resting height.

Use { passive: true } on the scroll listener — it tells the browser your handler never calls preventDefault(), which keeps scrolling smooth on mobile. Always clean up the listener in the useEffect return function to avoid a memory leak on unmount.

Step 5: Add a Command Palette (⌘K) to the shadcn Header

Wire a global keyboard listener for ⌘K / Ctrl+K that opens shadcn's CommandDialog. This pattern is standard on developer tools and SaaS dashboards, and it is the final layer this guide adds to the header.

Command Palette

Search for a command to run...

Always call e.preventDefault() before toggling the dialog — without it, browsers intercept ⌘K as a "search this page" shortcut and your palette never opens. For a dashboard layout that pairs a header like this with a sidebar, see the shadcn resizable sidebar component guide.

⌘ Building a dashboard, not a marketing site? The ChatDeck SaaS template pairs a command-palette header with a full app shell, so you don't have to assemble Steps 1 through 5 yourself.

How Do You Use This shadcn Header in App Router vs Pages Router?

Every component above works identically in both routers. Only the import location changes, and any header using useState, useEffect, or an event listener needs the "use client" directive already present at the top of each file above.

tsx

Which shadcn Header Features Does Your Project Actually Need?

Not every project needs all 5 layers. Match the feature to the project type before you add complexity you will not use.

Project typeRecommended steps
Portfolio or personal siteStep 1 + Step 2
SaaS marketing pageStep 1, 2, 4
Product with theme switchingStep 1, 2, 3
Developer tool or dashboardStep 1, 2, 5
Full-featured product siteAll 5 steps

Is a shadcn Header Accessible by Default?

Yes. NavigationMenu, Sheet, and Command all build on Radix UI, which implements WAI-ARIA roles, keyboard navigation, and focus management without extra code from you. Three rules keep a custom shadcn header accessible as you extend it:

  • Never remove the semantic wrapper. Keep content inside <header> and let NavigationMenuLink asChild pass through to your own Link — don't replace the whole primitive with plain div elements.
  • Label icon-only controls. The hamburger, dark mode toggle, and search buttons above all carry aria-label — screen readers have nothing else to announce them by.
  • Close overlays on navigation. Always reset open state when a link inside a Sheet or CommandDialog is selected, or keyboard and screen reader users get trapped in a closed-but-still-mounted overlay.

One honest caveat for 2026: Radix UI's NavigationMenu is still the default primitive shadcn/ui scaffolds when you run the CLI, but not every shadcn primitive stayed on Radix. See Radix vs Base UI for what changed elsewhere in the component tree and why it's worth checking before you lock in a header pattern long-term.

Launch your SaaS faster with a modern Shadcn UI template - Explore Free Templates

Explore more shadcn component and build guides:

Frequently Asked Questions

A
Ash

Full-stack developer and the maker behind ShadcnDeck. Writes practical guides on React, Next.js, Tailwind, and shadcn/ui — the things he wishes existed when he started.

Related Articles