Sidebar
Navegação lateral composta, com recolhimento (offcanvas ou só ícones), persistência em cookie, atalho de teclado e formato de folha no mobile.
Instalação
Recebe correções por update de versão. O caminho recomendado.
npm i @trdr/ui
import { Sidebar } from "@trdr/ui/sidebar"Uso
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent } from "@trdr/ui/sidebar"
export function Exemplo() {
return <Sidebar />
}Variantes
| Grupo | Valores | Padrão |
|---|---|---|
| variant | sidebarfloatinginset | sidebar |
Props
SidebarProviderPropsestende React.ComponentPropsWithoutRef<"div">| Prop | Tipo | Descrição |
|---|---|---|
| defaultOpen? | boolean | Estado inicial quando o componente nao e controlado. Default aberto. |
| open? | boolean | Estado controlado. Quando presente, quem chama decide o valor e recebe `onOpenChange`. |
| onOpenChange? | (open: boolean) => void |
SidebarPropsestende React.ComponentPropsWithoutRef<"div">, VariantProps<typeof sidebarInnerVariants>| Prop | Tipo | Descrição |
|---|---|---|
| side? | "left" | "right" | De qual borda da tela a sidebar nasce. Estrutural, como `side` no `Sheet`, nao um estilo. |
| collapsible? | "offcanvas" | "icon" | "none" | `offcanvas`: some da tela e volta por cima (o comportamento padrao, e o unico que existe no mobile). `icon`: encolhe para so a coluna de icones em vez de sumir. `none`: sempre expandida, sem recolher, sem virar Sheet no mobile: uma sidebar simples e fixa. |
SidebarTriggerPropsestende React.ComponentPropsWithoutRef<typeof Button>SidebarRailPropsestende React.ComponentPropsWithoutRef<"button">SidebarInsetPropsestende React.ComponentPropsWithoutRef<"main">SidebarInputPropsestende InputPropsSidebarHeaderPropsestende React.ComponentPropsWithoutRef<"div">SidebarFooterPropsestende React.ComponentPropsWithoutRef<"div">SidebarSeparatorPropsestende React.ComponentPropsWithoutRef<typeof Separator>SidebarContentPropsestende React.ComponentPropsWithoutRef<"div">SidebarGroupPropsestende React.ComponentPropsWithoutRef<"div">SidebarGroupLabelPropsestende React.ComponentPropsWithoutRef<"div">| Prop | Tipo | Descrição |
|---|---|---|
| asChild? | boolean | Renderiza o filho no lugar do `<div>` (por exemplo um `<Collapsible.Trigger>`). |
SidebarGroupActionPropsestende React.ComponentPropsWithoutRef<"button">| Prop | Tipo | Descrição |
|---|---|---|
| asChild? | boolean |
SidebarGroupContentPropsestende React.ComponentPropsWithoutRef<"div">SidebarMenuPropsestende React.ComponentPropsWithoutRef<"ul">SidebarMenuItemPropsestende React.ComponentPropsWithoutRef<"li">SidebarMenuButtonPropsestende React.ComponentPropsWithoutRef<"button">, VariantProps<typeof sidebarMenuButtonVariants>| Prop | Tipo | Descrição |
|---|---|---|
| asChild? | boolean | |
| isActive? | boolean | |
| tooltip? | string | TooltipContentProps | Legenda do `Tooltip` que aparece quando a sidebar esta recolhida no modo `icon` (o rotulo de texto some, so o icone fica visivel). String vira `{ children: tooltip }`; para atalho de teclado ao lado do texto, passe as props do `TooltipContent` direto: `tooltip={{ children: "Carteira", hotkey: <Kbd>G W</Kbd> }}`. |
SidebarMenuActionPropsestende React.ComponentPropsWithoutRef<"button">| Prop | Tipo | Descrição |
|---|---|---|
| asChild? | boolean | |
| showOnHover? | boolean | So aparece ao passar o mouse no `SidebarMenuItem` (ou com foco dentro dele) no desktop. |
SidebarMenuBadgePropsestende React.ComponentPropsWithoutRef<"div">SidebarMenuSkeletonPropsestende React.ComponentPropsWithoutRef<"div">| Prop | Tipo | Descrição |
|---|---|---|
| showIcon? | boolean | Reserva o espaco do icone de 16px a esquerda, para o layout nao pular quando o dado chega. |
SidebarMenuSubPropsestende React.ComponentPropsWithoutRef<"ul">SidebarMenuSubItemPropsestende React.ComponentPropsWithoutRef<"li">SidebarMenuSubButtonPropsestende React.ComponentPropsWithoutRef<"a">| Prop | Tipo | Descrição |
|---|---|---|
| asChild? | boolean | |
| size? | "sm" | "md" | |
| isActive? | boolean |
Quando usar
Use a sidebar para a navegacao principal de um app inteiro (uma mesa de operacoes, um painel administrativo, o proprio Hub): a lista fixa de secoes que o usuario alterna o dia inteiro, ao contrario de um menu que aparece por cima do conteudo. E o unico primitivo da biblioteca que e um bloco composto (mais de vinte partes) em vez de um componente unico, porque e tambem o unico que precisa resolver tres problemas ao mesmo tempo: recolher sem perder a barra, virar outra coisa no mobile, e lembrar o que o usuario escolheu da ultima vez.
Quando nao usar
Nao use para uma lista de acoes contextuais de um unico registro (isso e DropdownMenu ou
ContextMenu), nem para navegacao secundaria dentro de uma pagina (isso e Tabs ou
NavigationMenu). Se a lista de secoes tem duas ou tres entradas e nunca precisa recolher, uma
barra simples com Buttons ja resolve, sem o custo de montar SidebarProvider.
Anatomia
A sidebar e composta, nao uma prop de configuracao: cada parte e um componente que voce arruma como quiser.
SidebarProvider: a raiz. Guarda o estado aberto/fechado (useSidebar), detecta mobile, e define--sidebar-widthe--sidebar-width-iconcomo variavel CSS (a excecao que o CONTRACT abre na secao 4.7 para definicao de CSS var, nao para valor de design cru). Precisa envolver aSidebare oSidebarInsetjuntos, como dois filhos irmaos.Sidebar(data-slot="sidebar"): a barra em si. Propsside(left|right),variant(sidebar|floating|inset) ecollapsible(offcanvas|icon|none).SidebarTrigger: botao (Buttonpor baixo, varianteghost) que chamatoggleSidebar(). Coloque um dentro doSidebarInset, perto do conteudo.SidebarRail: alca fina na borda da barra, um atalho de mouse a mais para o mesmotoggleSidebar().aria-hiddende proposito (ver secao de acessibilidade abaixo).SidebarInset: o<main>do conteudo, irmao daSidebardentro doSidebarProvider.SidebarHeader,SidebarFooter,SidebarContent: as tres regioes verticais da barra (topo fixo, meio que rola, rodape fixo).SidebarInput: umInputcom o fundo e a borda ja ajustados para viver dentro da barra.SidebarSeparator: umSeparatorhorizontal para dividir secoes dentro da barra. Para um divisor vertical (por exemplo ao lado doSidebarTrigger, no cabecalho do conteudo), use oSeparatorpuro comorientation="vertical": oSidebarSeparatorassumew-auto, pensado para a orientacao horizontal.SidebarGroup,SidebarGroupLabel,SidebarGroupAction,SidebarGroupContent: uma secao com rotulo (maiusculo, cor da marca, herdado do legado) e uma acao opcional no canto (por exemplo "adicionar").SidebarMenu,SidebarMenuItem,SidebarMenuButton: a lista de navegacao. OSidebarMenuButtonaceitaisActive,variant(default|outline),size(default|sm|lg) etooltip(rotulo que viraTooltipquando a barra esta recolhida no modoicon).SidebarMenuAction,SidebarMenuBadge: um botao de acao (showOnHoverpara so aparecer no hover do item) e um contador (semprefont-mono, secao 6 do CONTRACT) no canto do item.SidebarMenuSkeleton: placeholder de carregamento para quando a lista ainda nao chegou.SidebarMenuSub,SidebarMenuSubItem,SidebarMenuSubButton: um nivel de sub-itens, indentado, para quando um item se desdobra.useSidebar: o hook para lerstate(expanded|collapsed),open,setOpen,openMobile,setOpenMobile,isMobileetoggleSidebarem qualquer componente dentro doSidebarProvider.
Recolhimento
collapsible decide o que acontece quando a barra fecha:
offcanvas(padrao): a barra sai da tela inteira e volta por cima quando reaberta.icon: a barra encolhe para--sidebar-width-icon(so os icones ficam visiveis). CadaSidebarMenuButtoncom proptooltipmostra o rotulo em umTooltipao lado, porque o texto sumiu da barra.none: a barra nunca recolhe nem viraSheetno mobile. Uma coluna simples e fixa.
O lado (side="left" ou "right") e independente do recolhimento: qualquer collapsible
funciona dos dois lados.
Persistencia e atalho de teclado
Toda troca de estado grava um cookie (sidebar_state, 7 dias) via document.cookie. A leitura
no primeiro render e responsabilidade de quem consome a biblioteca: o SidebarProvider so
escreve o cookie, porque so o app sabe se roda com acesso a cookies() do servidor antes do
primeiro paint. Em um app Next.js com App Router:
import { cookies } from "next/headers"
import { SidebarProvider } from "@trdr/ui/sidebar"
export default async function Layout({ children }: { children: React.ReactNode }) {
const defaultOpen = (await cookies()).get("sidebar_state")?.value !== "false"
return <SidebarProvider defaultOpen={defaultOpen}>{children}</SidebarProvider>
}Ctrl+B (ou Cmd+B no mac) alterna a barra em qualquer lugar da pagina, sem precisar de foco em
nenhum elemento especifico: o atalho e global dentro do SidebarProvider.
Mobile
Abaixo de 768px a Sidebar deixa de renderizar a barra fixa e vira o Sheet que ja existe na
biblioteca (nao uma segunda implementacao de painel deslizante): mesmo children, mesmo side,
largura propria (--sidebar-width-mobile, 18rem). O SidebarTrigger e o SidebarRail continuam
funcionando sem mudanca de API, so passam a abrir e fechar o Sheet por baixo. Por isso, no
mobile, o SidebarMenuButton nunca monta o Tooltip do modo icon: a barra vira folha de tela
cheia, sempre com o rotulo por extenso, entao o tooltip nao teria função nesse formato.
Acessibilidade
- O
Sheetdo mobile precisa de nome acessivel (o Radix exige em todorole="dialog"): aSidebarja inclui umSheetHeadercomSheetTitle/SheetDescriptionmarcadossr-onlypara isso, sem duplicar visualmente o cabecalho que voce compoe emSidebarHeader. SidebarRailearia-hidden: sem isso, um leitor de tela anuncia dois controles com o mesmo nome ("Alternar barra lateral") sempre que aSidebare composta com oSidebarTrigger, que e o caso comum. A alca continua clicavel para mouse e touch; o caminho por teclado e sempre oSidebarTrigger.SidebarMenuButtontemh-8(32px) no tamanhodefault, eh-6(24px) no tamanhosm, o minimo de alvo de toque que a secao 7 do CONTRACT exige.- O foco visivel usa
ring-sidebar-ring, o alias de anel proprio da familia de tokens da sidebar, para o mesmo contraste funcionar tanto no fundobg-sidebarquanto nobg-backgrounddoSidebarInset.
Migracao do legado
O Sidebar.tsx do Hub antigo (.trdr-sidebar em components.css) era uma barra unica, sem
recolher e sem versao mobile: 240px de largura, grupos com rotulo maiusculo na cor da marca, itens
de 32px com icone Material Symbols e estado ativo. Este componente preserva a largura (15rem,
os mesmos 240px) e a aparencia do rotulo de grupo e do item ativo, e adiciona o que o legado nunca
teve: recolher, o formato mobile, o atalho de teclado e a persistencia entre sessoes.
| Legado (components.css) | TRDR UI |
| --- | --- |
| .trdr-sidebar | Sidebar (dentro de SidebarProvider) |
| .trdr-sidebar-header | SidebarHeader |
| .trdr-sidebar-nav | SidebarContent |
| .trdr-sidebar-group | SidebarGroup |
| .trdr-sidebar-group-label | SidebarGroupLabel |
| .trdr-sidebar-list | SidebarMenu |
| .trdr-sidebar-item / .trdr-sidebar-item-active | SidebarMenuButton / prop isActive |
| .trdr-sidebar-icon | filho do SidebarMenuButton (<Icon aria-hidden />) |
| .trdr-sidebar-footer | SidebarFooter |
O icone deixa de ser uma classe de fonte (Material Symbols Outlined) fixa no CSS e passa a ser
o primeiro filho do SidebarMenuButton, qualquer ReactNode: continue usando Material Symbols
via <span className="mi" aria-hidden="true">home</span> se e assim que o seu app carrega os
icones (como o Hub faz hoje em apps/hub/src/components/sidebar.tsx), a biblioteca nao exige
nenhum icone especifico.
Slots e tokens
Todo elemento renderizado carrega um data-slot para estilizar partes internas sem depender de classe da biblioteca. Os tokens listados são extraídos do código fonte.
[data-slot="sidebar"][data-slot="sidebar-container"][data-slot="sidebar-content"][data-slot="sidebar-footer"][data-slot="sidebar-gap"][data-slot="sidebar-gap-container"][data-slot="sidebar-group"][data-slot="sidebar-group-action"][data-slot="sidebar-group-content"][data-slot="sidebar-group-label"][data-slot="sidebar-header"][data-slot="sidebar-inner"][data-slot="sidebar-inset"][data-slot="sidebar-menu"][data-slot="sidebar-menu-action"][data-slot="sidebar-menu-badge"][data-slot="sidebar-menu-button"][data-slot="sidebar-menu-item"][data-slot="sidebar-menu-skeleton"][data-slot="sidebar-menu-sub"][data-slot="sidebar-menu-sub-button"][data-slot="sidebar-menu-sub-item"][data-slot="sidebar-provider"][data-slot="sidebar-rail"][data-slot="sidebar-separator"][data-slot="sidebar-trigger"]18 tokens usados
bg-backgroundbg-sidebarbg-sidebar-accentbg-sidebar-borderbg-transparentborder-lborder-sidebar-borderoutline-nonering-sidebar-ringshadow-noneshadow-smtext-auxtext-b2text-b3text-content-brandtext-lefttext-sidebar-accent-foregroundtext-sidebar-foreground