Sidebar
Um painel de navegação vertical que fica ao lado do conteúdo principal, fornecendo acesso persistente a rotas e seções de nível superior em um layout de aplicativo.
Busque em todas as páginas da documentação
Um painel de navegação vertical que fica ao lado do conteúdo principal, fornecendo acesso persistente a rotas e seções de nível superior em um layout de aplicativo.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
"use client";
interface SidebarProps {
items: { label: string; href: string }[];
}
export function Sidebar({ items }: SidebarProps) {
return (
<nav className="flex h-screen w-64 flex-col bg-gray-900 p-4">
<ul className="space-y-1">
{items.map((item) => (
<li key={item.href}>
<a
href={item.href}
className="block rounded-lg px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-800 hover:text-white"
>
{item.label}
</a>
</li>
))}
</ul>
</nav>
);
}Uma barra lateral mínima que renderiza uma lista de links. Ela usa h-screen para preencher a altura da viewport e um fundo escuro para separá-la visualmente da área de conteúdo principal.
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
interface SidebarItem {
label: string;
href: string;
}
interface SidebarProps {
items: SidebarItem[];
}
export function Sidebar({ items }: SidebarProps) {
const pathname = usePathname();
return (
<nav className="flex h-screen w-64 flex-col bg-gray-900 p-4">
<ul className="space-y-1">
{items.map((item) => {
const active = pathname === item.href;
return (
<li key={item.href}>
<Link
href={item.href}
className={`block rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
active
? "bg-gray-800 text-white"
: "text-gray-400 hover:bg-gray-800 hover:text-white"
}`}
>
{item.label}
</Link>
</li>
);
})}
</ul>
</nav>
);
}Usa usePathname() do Next.js para comparar a rota atual com o href de cada item. O item ativo recebe um fundo e cor de texto distintos para que o usuário sempre saiba onde está.
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
interface SidebarItem {
label: string;
href: string;
icon: React.ReactNode;
}
interface SidebarProps {
items: SidebarItem[];
}
export function Sidebar({ items }: SidebarProps) {
const pathname = usePathname();
return (
<nav className="flex h-screen w-64 flex-col bg-gray-900 p-4">
<ul className="space-y-1">
{items.map((item) => {
const active = pathname === item.href;
return (
<li key={item.href}>
<Link
href={item.href}
className={`flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
active
? "bg-gray-800 text-white"
: "text-gray-400 hover:bg-gray-800 hover:text-white"
}`}
>
<span className="h-5 w-5 flex-shrink-0">{item.icon}</span>
{item.label}
</Link>
</li>
);
})}
</ul>
</nav>
);
}
// Uso
// <Sidebar items={[
// { label: "Dashboard", href: "/", icon: <HomeIcon /> },
// { label: "Settings", href: "/settings", icon: <GearIcon /> },
// ]} />Cada item aceita um icon ReactNode renderizado à esquerda do rótulo. O flex-shrink-0 no wrapper do ícone impede que ele encolha quando os rótulos são longos.
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
interface SidebarItem {
label: string;
href: string;
}
interface SidebarSection {
title: string;
items: SidebarItem[];
}
interface SidebarProps {
sections: SidebarSection[];
}
export function Sidebar({ sections }: SidebarProps) {
const pathname = usePathname();
return (
<nav className="flex h-screen w-64 flex-col overflow-y-auto bg-gray-900 p-4">
{sections.map((section) => (
<div key={section.title} className="mb-6">
<h3 className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">
{section.title}
</h3>
<ul className="space-y-1">
{section.items.map((item) => {
const active = pathname === item.href;
return (
<li key={item.href}>
<Link
href={item.href}
className={`block rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
active
? "bg-gray-800 text-white"
: "text-gray-400 hover:bg-gray-800 hover:text-white"
}`}
>
{item.label}
</Link>
</li>
);
})}
</ul>
</div>
))}
</nav>
);
}Agrupa itens sob títulos de seção usando um rótulo em maiúsculas. O overflow-y-auto garante que a barra lateral role independentemente quando há muitas seções.
"use client";
import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
interface SidebarItem {
label: string;
href: string;
icon: React.ReactNode;
}
interface SidebarProps {
items: SidebarItem[];
}
export function CollapsibleSidebar({ items }: SidebarProps) {
const [collapsed, setCollapsed] = useState(false);
const pathname = usePathname();
return (
<nav
className={`flex h-screen flex-col bg-gray-900 p-4 transition-all duration-200 ${
collapsed ? "w-16" : "w-64"
}`}
>
<button
onClick={() => setCollapsed((prev) => !prev)}
aria-label={collapsed ? "Expandir barra lateral" : "Colapsar barra lateral"}
className="mb-4 self-end rounded-lg p-1.5 text-gray-400 hover:bg-gray-800 hover:text-white"
>
<svg
className={`h-5 w-5 transition-transform ${collapsed ? "rotate-180" : ""}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<ul className="space-y-1">
{items.map((item) => {
const active = pathname === item.href;
return (
<li key={item.href}>
<Link
href={item.href}
title={collapsed ? item.label : undefined}
className={`flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
active
? "bg-gray-800 text-white"
: "text-gray-400 hover:bg-gray-800 hover:text-white"
}`}
>
<span className="h-5 w-5 flex-shrink-0">{item.icon}</span>
{!collapsed && <span>{item.label}</span>}
</Link>
</li>
);
})}
</ul>
</nav>
);
}A barra lateral alterna entre larguras w-64 e w-16. Quando colapsada, apenas os ícones são mostrados e um atributo title nativo fornece uma dica de ferramenta ao passar o mouse. O chevron gira para indicar o estado.
"use client";
import { useEffect, useRef } from "react";
import Link from "next/link";
interface SidebarItem {
label: string;
href: string;
}
interface MobileSidebarProps {
items: SidebarItem[];
open: boolean;
onClose: () => void;
}
export function MobileSidebar({ items, open, onClose }: MobileSidebarProps) {
const navRef = useRef<HTMLElement>(null);
useEffect(() => {
if (open) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [open]);
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
}
if (open) {
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}
}, [open, onClose]);
return (
<>
{/* Backdrop */}
<div
className={`fixed inset-0 z-40 bg-black/50 transition-opacity ${
open ? "opacity-100" : "pointer-events-none opacity-0"
}`}
onClick={onClose}
aria-hidden="true"
/>
{/* Drawer */}
<nav
ref={navRef}
className={`fixed inset-y-0 left-0 z-50 w-72 bg-gray-900 p-4 transition-transform duration-200 ${
open ? "translate-x-0" : "-translate-x-full"
}`}
aria-label="Navegação móvel"
>
<button
onClick={onClose}
aria-label="Fechar menu"
className="mb-4 rounded-lg p-1.5 text-gray-400 hover:bg-gray-800 hover:text-white"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<ul className="space-y-1">
{items.map((item) => (
<li key={item.href}>
<Link
href={item.href}
onClick={onClose}
className="block rounded-lg px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-800 hover:text-white"
>
{item.label}
</Link>
</li>
))}
</ul>
</nav>
</>
);
}Uma gaveta deslizante para viewports móveis. O overlay de fundo impede a interação com o conteúdo principal e fecha a barra lateral ao clicar. A rolagem do corpo é bloqueada enquanto a gaveta está aberta, e pressionar Escape a fecha.
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
interface SidebarItem {
label: string;
href: string;
icon: React.ReactNode;
}
interface SidebarProps {
items: SidebarItem[];
user: { name: string; email: string; avatarUrl?: string };
logo: React.ReactNode;
}
export function Sidebar({ items, user, logo }: SidebarProps) {
const pathname = usePathname();
return (
<nav className="flex h-screen w-64 flex-col bg-gray-900">
{/* Logo */}
<div className="flex h-16 items-center px-6">
{logo}
</div>
{/* Navegação */}
<ul className="flex-1 space-y-1 overflow-y-auto px-4">
{items.map((item) => {
const active = pathname === item.href;
return (
<li key={item.href}>
<Link
href={item.href}
className={`flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
active
? "bg-gray-800 text-white"
: "text-gray-400 hover:bg-gray-800 hover:text-white"
}`}
>
<span className="h-5 w-5 flex-shrink-0">{item.icon}</span>
{item.label}
</Link>
</li>
);
})}
</ul>
{/* Perfil do usuário */}
<div className="border-t border-gray-800 px-4 py-4">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-700 text-sm font-medium text-white">
{user.avatarUrl ? (
<img src={user.avatarUrl} alt="" className="h-full w-full rounded-full object-cover" />
) : (
user.name.charAt(0).toUpperCase()
)}
</div>
<div className="min-w-0">
<p className="truncate text-sm font-medium text-white">{user.name}</p>
<p className="truncate text-xs text-gray-400">{user.email}</p>
</div>
</div>
</div>
</nav>
);
}Uma barra lateral de altura total com três zonas: logo no topo, navegação rolável no meio e um perfil de usuário fixado na parte inferior. O utilitário truncate evita que nomes ou e-mails longos quebrem o layout.
"use client";
import { createContext, useContext, useState, useCallback, useEffect, useRef } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
// --- Contexto ---
interface SidebarContextValue {
collapsed: boolean;
toggle: () => void;
mobileOpen: boolean;
setMobileOpen: (open: boolean) => void;
}
const SidebarContext = createContext<SidebarContextValue | null>(null);
function useSidebar() {
const ctx = useContext(SidebarContext);
if (!ctx) throw new Error("useSidebar deve ser usado dentro de SidebarProvider");
return ctx;
}
// --- Provider ---
interface SidebarProviderProps {
children: React.ReactNode;
defaultCollapsed?: boolean;
}
export function SidebarProvider({ children, defaultCollapsed = false }: SidebarProviderProps) {
const [collapsed, setCollapsed] = useState(defaultCollapsed);
const [mobileOpen, setMobileOpen] = useState(false);
const toggle = useCallback(() => setCollapsed((prev) => !prev), []);
// Fecha a gaveta móvel na mudança de rota
const pathname = usePathname();
useEffect(() => setMobileOpen(false), [pathname]);
// Bloqueia a rolagem do corpo quando a gaveta móvel está aberta
useEffect(() => {
document.body.style.overflow = mobileOpen ? "hidden" : "";
return () => {
document.body.style.overflow = "";
};
}, [mobileOpen]);
return (
<SidebarContext.Provider value={{ collapsed, toggle, mobileOpen, setMobileOpen }}>
{children}
</SidebarContext.Provider>
);
}
// --- Sidebar ---
interface NavItem {
label: string;
href: string;
icon: React.ReactNode;
badge?: string;
}
interface NavSection {
title?: string;
items: NavItem[];
}
interface SidebarProps {
sections: NavSection[];
logo: React.ReactNode;
logoCollapsed?: React.ReactNode;
footer?: React.ReactNode;
}
export function Sidebar({ sections, logo, logoCollapsed, footer }: SidebarProps) {
const { collapsed, toggle, mobileOpen, setMobileOpen } = useSidebar();
const pathname = usePathname();
const navRef = useRef<HTMLElement>(null);
// Tecla Escape fecha a gaveta móvel
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") setMobileOpen(false);
}
if (mobileOpen) {
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}
}, [mobileOpen, setMobileOpen]);
const navContent = (
<>
{/* Logo */}
<div className="flex h-16 items-center justify-between px-4">
<div className="flex items-center">
{collapsed && logoCollapsed ? logoCollapsed : logo}
</div>
<button
onClick={toggle}
aria-label={collapsed ? "Expandir barra lateral" : "Colapsar barra lateral"}
className="hidden rounded-lg p-1.5 text-gray-400 hover:bg-gray-800 hover:text-white lg:block"
>
<svg
className={`h-5 w-5 transition-transform duration-200 ${collapsed ? "rotate-180" : ""}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
</div>
{/* Seções */}
<div className="flex-1 space-y-6 overflow-y-auto px-3 py-4">
{sections.map((section, idx) => (
<div key={section.title ?? idx}>
{section.title && !collapsed && (
<h3 className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-500">
{section.title}
</h3>
)}
<ul className="space-y-1">
{section.items.map((item) => {
const active = pathname === item.href || pathname.startsWith(item.href + "/");
return (
<li key={item.href}>
<Link
href={item.href}
title={collapsed ? item.label : undefined}
className={`group flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
active
? "bg-gray-800 text-white"
: "text-gray-400 hover:bg-gray-800 hover:text-white"
} ${collapsed ? "justify-center" : ""}`}
>
<span className="h-5 w-5 flex-shrink-0">{item.icon}</span>
{!collapsed && (
<>
<span className="flex-1">{item.label}</span>
{item.badge && (
<span className="rounded-full bg-blue-600 px-2 py-0.5 text-xs font-medium text-white">
{item.badge}
</span>
)}
</>
)}
</Link>
</li>
);
})}
</ul>
</div>
))}
</div>
{/* Rodapé */}
{footer && (
<div className="border-t border-gray-800 px-4 py-4">
{footer}
</div>
)}
</>
);
return (
<>
{/* Barra lateral do desktop */}
<nav
ref={navRef}
className={`hidden h-screen flex-col bg-gray-900 transition-all duration-200 lg:flex ${
collapsed ? "w-16" : "w-64"
}`}
aria-label="Navegação principal"
>
{navContent}
</nav>
{/* Fundo móvel */}
<div
className={`fixed inset-0 z-40 bg-black/50 transition-opacity lg:hidden ${
mobileOpen ? "opacity-100" : "pointer-events-none opacity-0"
}`}
onClick={() => setMobileOpen(false)}
aria-hidden="true"
/>
{/* Gaveta móvel */}
<nav
className={`fixed inset-y-0 left-0 z-50 flex w-72 flex-col bg-gray-900 transition-transform duration-200 lg:hidden ${
mobileOpen ? "translate-x-0" : "-translate-x-full"
}`}
aria-label="Navegação móvel"
>
{navContent}
</nav>
</>
);
}
// --- Gatilho para móvel ---
export function SidebarTrigger() {
const { setMobileOpen } = useSidebar();
return (
<button
onClick={() => setMobileOpen(true)}
aria-label="Abrir menu"
className="rounded-lg p-2 text-gray-600 hover:bg-gray-100 lg:hidden"
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
);
}Aspectos chave:
SidebarProvider gerencia os estados colapsado e mobile-open através do contexto, para que a barra lateral, o botão de gatilho e o layout possam coordenar sem prop drilling.<nav> de desktop oculta e uma gaveta móvel fixa coexistem. As classes responsivas do Tailwind (lg:flex, lg:hidden) alternam qual delas é visível, evitando a detecção de breakpoint baseada em JavaScript.pathname.startsWith(item.href + "/") para destacar itens pais quando uma rota aninhada está ativa, não apenas correspondências exatas.title nativo fornece contexto ao passar o mouse.useEffect em pathname fecha a gaveta móvel sempre que a rota muda, para que o usuário veja a nova página imediatamente.Não bloquear a rolagem do corpo na gaveta móvel - Sem overflow: hidden no corpo, os usuários podem rolar a página atrás da gaveta aberta. Sempre bloqueie a rolagem quando a gaveta estiver aberta.
Usar window.innerWidth em vez de breakpoints CSS - Verificações de breakpoint baseadas em JavaScript causam mismatches de hidratação em SSR. Use classes responsivas do Tailwind (lg:flex, lg:hidden) em vez disso.
Estado ativo correspondendo apenas a caminhos exatos - Usar pathname === href perde rotas filhas. Use startsWith para navegação hierárquica, mas tome cuidado com / correspondendo a tudo.
Falta de aria-label no elemento nav - Leitores de tela precisam distinguir entre vários elementos <nav> em uma página. Sempre rotule barras laterais com aria-label="Navegação principal" ou similar.
Barra lateral empurrando o conteúdo em vez de sobrepor no celular - Uma barra lateral de largura fixa no fluxo do documento encolherá o conteúdo principal em telas pequenas. Use position: fixed ou um padrão de gaveta para celular.
Esquecer de fechar a gaveta móvel ao clicar no link - Se a gaveta permanecer aberta após a navegação, os usuários verão conteúdo desatualizado atrás dela. Feche a gaveta na mudança de rota ou no clique do link.
Conflitos de z-index com modais ou dropdowns - A barra lateral e seu fundo precisam de valores de z-index consistentes que não entrem em conflito com outros overlays. Estabeleça uma escala de z-index em seu projeto.
Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥