A production-quality sticky sidebar in Next.js needs four things: CSS position sticky for the layout, active link detection with usePathname, a mobile collapse mechanism, and dark mode tokens. Here is the complete implementation.
Sidebar navigation is one of the most common patterns in SaaS dashboards - and one of the most commonly broken. A good sidebar is sticky, collapses on mobile, highlights the active route, and works with dark mode. Here's how to build it correctly with Next.js App Router and Tailwind CSS v4.
The layout structure
The key to a sticky sidebar is the parent layout. The sidebar and main content sit side-by-side in a flex container, and the sidebar uses sticky top-0 h-screen to stay in view while the content scrolls.
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex min-h-screen bg-background">
<Sidebar />
<main className="flex-1 overflow-y-auto p-6">
{children}
</main>
</div>
);
}
The sidebar component
// components/Sidebar.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState } from "react";
const navItems = [
{ href: "/dashboard", label: "Overview" },
{ href: "/dashboard/analytics", label: "Analytics" },
{ href: "/dashboard/users", label: "Users" },
{ href: "/dashboard/billing", label: "Billing" },
{ href: "/dashboard/settings", label: "Settings" },
];
export function Sidebar() {
const pathname = usePathname();
const [open, setOpen] = useState(false);
return (
<>
<button
className="fixed top-4 left-4 z-50 md:hidden p-2 bg-card border border-border"
onClick={() => setOpen(!open)}
aria-label="Toggle navigation"
>
<span className="block w-5 h-0.5 bg-foreground mb-1" />
<span className="block w-5 h-0.5 bg-foreground mb-1" />
<span className="block w-5 h-0.5 bg-foreground" />
</button>
{open && (
<div
className="fixed inset-0 z-40 bg-black/50 md:hidden"
onClick={() => setOpen(false)}
/>
)}
<aside
className={`
fixed md:sticky top-0 left-0 z-40 h-screen w-64
bg-card border-r border-border flex flex-col
transition-transform duration-200 md:translate-x-0
${open ? "translate-x-0" : "-translate-x-full"}
`}
>
<div className="p-6 border-b border-border">
<span className="font-bold text-foreground">Dashboard</span>
</div>
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
{navItems.map((item) => {
const active = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
onClick={() => setOpen(false)}
className={`
flex items-center px-3 py-2 text-sm font-medium transition-colors
${active
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
}
`}
>
{item.label}
</Link>
);
})}
</nav>
</aside>
</>
);
}
Tailwind CSS v4 tokens for dark mode
/* globals.css */
@import "tailwindcss";
@theme {
--color-background: #ffffff;
--color-foreground: #0c0c0d;
--color-card: #f8f8f6;
--color-border: #e8e8e4;
--color-muted-foreground: #6b6b6e;
--color-muted: #f0f0ee;
--color-primary: #e5402a;
}
@variant dark (&:where(.dark, .dark *)) {
--color-background: #0c0c0d;
--color-foreground: #fcfcfa;
--color-card: #141416;
--color-border: rgba(255,255,255,0.08);
--color-muted-foreground: #9a9a9c;
--color-muted: rgba(255,255,255,0.05);
}
Common mistakes to avoid
- Using
position:fixedinstead ofsticky- fixed sidebars require manual padding on the main content - Active link detection with
pathname.includes()- use exact match orstartsWithcarefully - Forgetting
overflow-y-autoon the nav - long lists will overflow without it - Not closing the mobile sidebar on navigation - add
onClick={() => setOpen(false)}to each link - Using
window.innerWidthfor mobile detection - use Tailwind responsive prefixes instead
Frequently asked questions
How do I make a sidebar sticky in Next.js?
Use position: sticky (Tailwind: sticky top-0 h-screen) on the sidebar inside a flex parent. The parent must be display: flex and taller than the viewport.
How do I highlight the active link in a Next.js sidebar?
Use the usePathname hook from next/navigation to get the current route. Compare it to each navigation item's href and apply active styles conditionally. Mark the component "use client".
Originally published at thekitbase.app
Top comments (0)