Sidebar
Menu lateral recolhível para sistemas e painéis, que vira um painel deslizante no celular.
'use client';
import { CalendarDays, FileText, LayoutDashboard, LifeBuoy, Settings, Stethoscope, Users, Wallet } from 'lucide-react';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarSeparator,
SidebarTrigger,
} from '@/components/ui/sidebar';
const menu = [
{ label: 'Visão geral', icon: <LayoutDashboard />, active: true },
{ label: 'Agenda', icon: <CalendarDays />, badge: '12' },
{ label: 'Pacientes', icon: <Users /> },
{ label: 'Procedimentos', icon: <Stethoscope /> },
{ label: 'Financeiro', icon: <Wallet /> },
{ label: 'Relatórios', icon: <FileText /> },
];
export default function SidebarDemo() {
return (
// Na sua aplicação o SidebarProvider ocupa a tela toda (min-h-svh).
// Aqui ele fica numa caixa de altura fixa só para caber na prévia.
<SidebarProvider className="h-[480px] min-h-0 overflow-hidden rounded-xl">
<Sidebar className="h-full">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" href="#">
<span className="flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary font-heading text-sm font-semibold text-sidebar-primary-foreground">
SP
</span>
<span className="grid flex-1 text-left leading-tight">
<span className="truncate font-semibold">Sorriso Pleno</span>
<span className="truncate text-xs text-sidebar-foreground/70">Unidade Centro</span>
</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Clínica</SidebarGroupLabel>
<SidebarMenu>
{menu.map((item) => (
<SidebarMenuItem key={item.label}>
<SidebarMenuButton href="#" icon={item.icon} isActive={item.active}>
{item.label}
</SidebarMenuButton>
{item.badge && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
<SidebarSeparator />
<SidebarGroup>
<SidebarGroupLabel>Conta</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton href="#" icon={<Settings />}>
Configurações
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton icon={<LifeBuoy />} onPress={() => alert('Abrindo suporte')}>
Suporte
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg">
<Avatar className="size-8 rounded-lg">
<AvatarFallback className="rounded-lg">RO</AvatarFallback>
</Avatar>
<span className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">Dra. Renata Oliveira</span>
<span className="truncate text-xs text-sidebar-foreground/70">renata@sorrisopleno.com.br</span>
</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
<SidebarInset>
<header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger />
<span className="text-sm font-medium">Visão geral</span>
</header>
<div className="grid flex-1 content-start gap-4 overflow-auto p-4 sm:grid-cols-3">
{[1, 2, 3].map((i) => (
<div key={i} className="h-24 rounded-xl bg-muted/60" />
))}
<div className="h-48 rounded-xl bg-muted/60 sm:col-span-3" />
</div>
</SidebarInset>
</SidebarProvider>
);
}Instalação
npx shadcn@latest add @lenstech/sidebarEstrutura
SidebarProvider estado (expandida/recolhida, mobile) + atalho Ctrl/⌘+B
├── Sidebar o menu lateral
│ ├── SidebarHeader logo / nome da empresa
│ ├── SidebarContent área rolável
│ │ └── SidebarGroup (SidebarGroupLabel + SidebarMenu)
│ │ └── SidebarMenuItem → SidebarMenuButton (+ SidebarMenuBadge)
│ └── SidebarFooter usuário logado
└── SidebarInset área principal (cabeçalho com SidebarTrigger + conteúdo)Uso
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupLabel,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"export default function Layout({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider>
<Sidebar>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Clínica</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton href="/agenda" icon={<CalendarDays />} isActive>
Agenda
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
</Sidebar>
<SidebarInset>
<header className="flex h-14 items-center gap-2 border-b px-4">
<SidebarTrigger />
</header>
{children}
</SidebarInset>
</SidebarProvider>
)
}Atalho Ctrl/⌘ + B
O SidebarProvider escuta Ctrl + B (ou ⌘ + B no Mac) na janela inteira para expandir/recolher.
Nesta página há várias prévias, então o atalho alterna todas ao mesmo tempo — no seu app haverá um só provider.
Tela cheia por padrão
No app real o SidebarProvider ocupa a altura da tela (min-h-svh) e a Sidebar fica fixa (h-svh).
Nas prévias desta página usamos className="h-[480px] min-h-0" no provider e className="h-full" na
Sidebar só para caber na caixa — não copie essas classes para o seu layout.
Exemplos
Offcanvas
collapsible="offcanvas" esconde o menu por completo ao recolher.
'use client';
import { Boxes, LayoutDashboard, ShoppingBag, Users } from 'lucide-react';
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupLabel,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from '@/components/ui/sidebar';
const menu = [
{ label: 'Painel', icon: <LayoutDashboard />, active: true },
{ label: 'Pedidos', icon: <ShoppingBag /> },
{ label: 'Produtos', icon: <Boxes /> },
{ label: 'Clientes', icon: <Users /> },
];
export default function SidebarOffcanvas() {
return (
<SidebarProvider className="h-[400px] min-h-0 overflow-hidden rounded-xl">
<Sidebar collapsible="offcanvas" className="h-full">
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Empório Bom Grão</SidebarGroupLabel>
<SidebarMenu>
{menu.map((item) => (
<SidebarMenuItem key={item.label}>
<SidebarMenuButton href="#" icon={item.icon} isActive={item.active}>
{item.label}
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
</Sidebar>
<SidebarInset>
<header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger />
<span className="text-sm text-muted-foreground">Clique no botão: o menu some por completo.</span>
</header>
<div className="flex-1 p-4">
<div className="h-full rounded-xl bg-muted/60" />
</div>
</SidebarInset>
</SidebarProvider>
);
}Começa recolhida
defaultOpen={false} inicia no modo ícones (collapsible="icon", o padrão).
'use client';
import { Briefcase, FileText, LayoutDashboard, Receipt, Users } from 'lucide-react';
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupLabel,
SidebarInset,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from '@/components/ui/sidebar';
const menu = [
{ label: 'Painel', icon: <LayoutDashboard />, active: true },
{ label: 'Clientes', icon: <Users /> },
{ label: 'Processos', icon: <Briefcase />, badge: '5' },
{ label: 'Documentos', icon: <FileText /> },
{ label: 'Honorários', icon: <Receipt /> },
];
export default function SidebarCollapsed() {
return (
<SidebarProvider defaultOpen={false} className="h-[400px] min-h-0 overflow-hidden rounded-xl">
<Sidebar collapsible="icon" className="h-full">
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Escritório</SidebarGroupLabel>
<SidebarMenu>
{menu.map((item) => (
<SidebarMenuItem key={item.label}>
<SidebarMenuButton href="#" icon={item.icon} isActive={item.active}>
{item.label}
</SidebarMenuButton>
{item.badge && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
</Sidebar>
<SidebarInset>
<header className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger />
<span className="text-sm text-muted-foreground">Começa recolhida — só os ícones aparecem.</span>
</header>
<div className="flex-1 p-4">
<div className="h-full rounded-xl bg-muted/60" />
</div>
</SidebarInset>
</SidebarProvider>
);
}Controlada e useSidebar
Controle o estado com open + onOpenChange (por exemplo, para salvar a preferência do usuário) e use o hook useSidebar() em qualquer componente dentro do provider.
'use client';
import { useState } from 'react';
import { CalendarDays, LayoutDashboard, Users } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
useSidebar,
} from '@/components/ui/sidebar';
/** Qualquer componente dentro do SidebarProvider pode ler e alterar o estado com useSidebar(). */
function StatusDoMenu() {
const { state, toggleSidebar } = useSidebar();
return (
<Button variant="outline" size="sm" onPress={toggleSidebar}>
{state === 'expanded' ? 'Recolher menu' : 'Expandir menu'}
</Button>
);
}
export default function SidebarControlled() {
const [aberta, setAberta] = useState(true);
return (
<SidebarProvider open={aberta} onOpenChange={setAberta} className="h-[360px] min-h-0 overflow-hidden rounded-xl">
<Sidebar className="h-full">
<SidebarContent>
<SidebarGroup>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton href="#" icon={<LayoutDashboard />} isActive>
Painel
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton href="#" icon={<CalendarDays />}>
Agenda
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton href="#" icon={<Users />}>
Clientes
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
</Sidebar>
<SidebarInset className="gap-3 p-4">
<StatusDoMenu />
<p className="text-sm text-muted-foreground">
Estado salvo no componente pai: <code className="font-mono">{aberta ? 'aberta' : 'recolhida'}</code>
</p>
</SidebarInset>
</SidebarProvider>
);
}Do lado direito
'use client';
import { Bell, MessageSquare, StickyNote } from 'lucide-react';
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupLabel,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from '@/components/ui/sidebar';
export default function SidebarRight() {
return (
<SidebarProvider className="h-[360px] min-h-0 overflow-hidden rounded-xl">
<Sidebar side="right" collapsible="offcanvas" className="h-full">
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Atividade</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton icon={<Bell />}>3 notificações</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton icon={<MessageSquare />}>Mensagens</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton icon={<StickyNote />}>Anotações</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
</Sidebar>
<SidebarInset>
<header className="flex h-14 shrink-0 items-center justify-between gap-2 border-b px-4">
<span className="text-sm font-medium">Pedido #10482</span>
<SidebarTrigger />
</header>
<div className="flex-1 p-4">
<div className="h-full rounded-xl bg-muted/60" />
</div>
</SidebarInset>
</SidebarProvider>
);
}No celular
Abaixo de 768px a Sidebar vira um painel deslizante (Modal do React Aria) aberto pelo SidebarTrigger,
que fecha com Esc, clique fora ou ao escolher um item. open/defaultOpen valem só para o desktop.
Quando usar
- Use em sistemas com várias áreas: painel da clínica, gestão da loja, área do cliente.
- Não use em sites institucionais e landing pages — use o bloco
Site Header. - Com poucas seções (2–4) dentro de uma mesma tela,
Tabspode bastar. - Veja a composição completa (cabeçalho, breadcrumbs e cards) no bloco
App Shell.
API
SidebarProvider
Aceita as props de uma div, mais:
Prop
Type
Sidebar
Aceita as props de uma div, mais:
Prop
Type
SidebarMenuButton
Com href renderiza um Link do React Aria; sem href, um Button (onPress). Aceita as props de cada um, mais:
Prop
Type
useSidebar
Prop
Type
Demais partes
SidebarHeader, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuItem, SidebarMenuBadge, SidebarSeparator e SidebarInset aceitam as props do elemento HTML correspondente. SidebarTrigger aceita as props do Button.