Sidebar

Navegacao

Navegação lateral composta, com recolhimento (offcanvas ou só ícones), persistência em cookie, atalho de teclado e formato de folha no mobile.

Carregando

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

GrupoValoresPadrão
variant
sidebarfloatinginset
sidebar

Props

SidebarProviderPropsestende React.ComponentPropsWithoutRef<"div">
PropTipoDescrição
defaultOpen?booleanEstado inicial quando o componente nao e controlado. Default aberto.
open?booleanEstado controlado. Quando presente, quem chama decide o valor e recebe `onOpenChange`.
onOpenChange?(open: boolean) => void
SidebarPropsestende React.ComponentPropsWithoutRef<"div">, VariantProps<typeof sidebarInnerVariants>
PropTipoDescriçã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 InputProps
SidebarHeaderPropsestende React.ComponentPropsWithoutRef<"div">
SidebarFooterPropsestende React.ComponentPropsWithoutRef<"div">
SidebarSeparatorPropsestende React.ComponentPropsWithoutRef<typeof Separator>
SidebarContentPropsestende React.ComponentPropsWithoutRef<"div">
SidebarGroupPropsestende React.ComponentPropsWithoutRef<"div">
SidebarGroupLabelPropsestende React.ComponentPropsWithoutRef<"div">
PropTipoDescrição
asChild?booleanRenderiza o filho no lugar do `<div>` (por exemplo um `<Collapsible.Trigger>`).
SidebarGroupActionPropsestende React.ComponentPropsWithoutRef<"button">
PropTipoDescrição
asChild?boolean
SidebarGroupContentPropsestende React.ComponentPropsWithoutRef<"div">
SidebarMenuPropsestende React.ComponentPropsWithoutRef<"ul">
SidebarMenuItemPropsestende React.ComponentPropsWithoutRef<"li">
SidebarMenuButtonPropsestende React.ComponentPropsWithoutRef<"button">, VariantProps<typeof sidebarMenuButtonVariants>
PropTipoDescrição
asChild?boolean
isActive?boolean
tooltip?string | TooltipContentPropsLegenda 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">
PropTipoDescrição
asChild?boolean
showOnHover?booleanSo aparece ao passar o mouse no `SidebarMenuItem` (ou com foco dentro dele) no desktop.
SidebarMenuBadgePropsestende React.ComponentPropsWithoutRef<"div">
SidebarMenuSkeletonPropsestende React.ComponentPropsWithoutRef<"div">
PropTipoDescrição
showIcon?booleanReserva 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">
PropTipoDescriçã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-width e --sidebar-width-icon como 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 a Sidebar e o SidebarInset juntos, como dois filhos irmaos.
  • Sidebar (data-slot="sidebar"): a barra em si. Props side (left | right), variant (sidebar | floating | inset) e collapsible (offcanvas | icon | none).
  • SidebarTrigger: botao (Button por baixo, variante ghost) que chama toggleSidebar(). Coloque um dentro do SidebarInset, perto do conteudo.
  • SidebarRail: alca fina na borda da barra, um atalho de mouse a mais para o mesmo toggleSidebar(). aria-hidden de proposito (ver secao de acessibilidade abaixo).
  • SidebarInset: o <main> do conteudo, irmao da Sidebar dentro do SidebarProvider.
  • SidebarHeader, SidebarFooter, SidebarContent: as tres regioes verticais da barra (topo fixo, meio que rola, rodape fixo).
  • SidebarInput: um Input com o fundo e a borda ja ajustados para viver dentro da barra.
  • SidebarSeparator: um Separator horizontal para dividir secoes dentro da barra. Para um divisor vertical (por exemplo ao lado do SidebarTrigger, no cabecalho do conteudo), use o Separator puro com orientation="vertical": o SidebarSeparator assume w-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. O SidebarMenuButton aceita isActive, variant (default | outline), size (default | sm | lg) e tooltip (rotulo que vira Tooltip quando a barra esta recolhida no modo icon).
  • SidebarMenuAction, SidebarMenuBadge: um botao de acao (showOnHover para so aparecer no hover do item) e um contador (sempre font-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 ler state (expanded | collapsed), open, setOpen, openMobile, setOpenMobile, isMobile e toggleSidebar em qualquer componente dentro do SidebarProvider.

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). Cada SidebarMenuButton com prop tooltip mostra o rotulo em um Tooltip ao lado, porque o texto sumiu da barra.
  • none: a barra nunca recolhe nem vira Sheet no 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 Sheet do mobile precisa de nome acessivel (o Radix exige em todo role="dialog"): a Sidebar ja inclui um SheetHeader com SheetTitle/SheetDescription marcados sr-only para isso, sem duplicar visualmente o cabecalho que voce compoe em SidebarHeader.
  • SidebarRail e aria-hidden: sem isso, um leitor de tela anuncia dois controles com o mesmo nome ("Alternar barra lateral") sempre que a Sidebar e composta com o SidebarTrigger, que e o caso comum. A alca continua clicavel para mouse e touch; o caminho por teclado e sempre o SidebarTrigger.
  • SidebarMenuButton tem h-8 (32px) no tamanho default, e h-6 (24px) no tamanho sm, 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 fundo bg-sidebar quanto no bg-background do SidebarInset.

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