How to create a responsive navbar in Next.js with TypeScript and Zustand
Build an accessible, responsive navigation bar in the Next.js App Router using TypeScript and Zustand for predictable global state.
On this page
A navbar looks trivial until you support a keyboard, a screen reader, a mobile drawer, and a sticky scroll state at the same time. This is the structure I keep coming back to.
Model the state first
Navigation state is small and global: which drawer is open, whether the page is scrolled. Zustand keeps that out of the component tree.
import { create } from "zustand";
type NavState = {
isMobileOpen: boolean;
isScrolled: boolean;
openMobile: () => void;
closeMobile: () => void;
setScrolled: (value: boolean) => void;
};
export const useNavStore = create<NavState>((set) => ({
isMobileOpen: false,
isScrolled: false,
openMobile: () => set({ isMobileOpen: true }),
closeMobile: () => set({ isMobileOpen: false }),
setScrolled: (value) => set({ isScrolled: value }),
}));
Selecting a single value avoids re-rendering the whole header on every scroll tick.
const isScrolled = useNavStore((state) => state.isScrolled);
Scroll state without thrashing
Listening to scroll is fine as long as you do almost nothing in the handler.
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, [setScrolled]);
Two details matter here: passive: true tells the browser the handler will not call preventDefault, and the boolean is written only when it changes.
Make the drawer accessible
The drawer is a dialog. Treat it like one.
- Focus the first item when it opens
- Return focus to the trigger on close
- Close on
Escape - Prevent the page behind it from scrolling
useEffect(() => {
if (!isMobileOpen) return;
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") closeMobile();
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [isMobileOpen, closeMobile]);
Layout that does not jump
The header is fixed, so it overlaps content. Reserve the space once:
[id] {
scroll-margin-top: 6rem;
}
This single rule fixes anchor links that would otherwise hide their target under a sticky bar.
Summary
Zustand gives you predictable state, TypeScript gives you confidence at the edges, and a handful of accessibility rules give you a navbar that works for everyone.
Continue reading
Related articles
Building a modern todo app with Zustand, Firebase, and Next.js
A real-time todo app with authentication, CRUD operations, and predictable global state, built with Next.js, Firebase, and Zustand.
Serverless file uploads with Supabase Storage and Next.js API routes
Secure, serverless file uploads in Next.js using Supabase Storage, with validation, size limits, and public access handling.
Integrating Razorpay payments in Next.js: a step-by-step guide
Set up checkout, verify signatures on the server, and handle webhooks safely when integrating Razorpay with Next.js.