AppSidebar
AppSidebar mantém os destinos de uma aplicação à vista. Escolha a porta de children quando os links
precisarem preservar o comportamento do router; escolha groups para uma interface rápida de seleção
com links ou ações.
Exemplos
Responsive application shell
A rich workspace header, stationary account footer and scrolling navigation share a Shell with a native topbar trigger. Try the icon rail, compact density, keyboard shortcut and mobile offcanvas.
Grupos convenientes
groups usa links para itens com destino e botão para ações e informa seu id por onSelect.
Seu label é uma string de propósito, para que o trilho sempre possa nomear o item e mostrar um
tooltip nativo.
Quando usar
Use AppSidebar para destinos agrupados e persistentes no shell de uma aplicação.
Prefira outro componente quando:
- Uma linha curta de rotas no nível do site é suficiente — use Navbar.
- Os destinos são visualizações pares na página atual — use Tabs.
- Você precisa de apenas uma seção de navegação — use SidebarGroup.
Largura e propriedade do Shell
A marcação CSS-only de AppSidebar usa 260px por padrão; adicione lyra-appsidebar--rail para o
padrão de trilho de 64px. Defina --appsidebar-width quando a aplicação precisar de outra largura.
Quando AppSidebar for filho direto da barra lateral de um Shell com scroll de conteúdo, AppSidebar é
dona desse trilho; Shell.sidebarWidth continua dimensionando conteúdo comum de barra lateral e
trilhos de Shell com scroll de página.
<!-- O padrão é 260px. -->
<nav class="lyra-appsidebar" aria-label="Navegação do espaço de trabalho"></nav>
<!-- O padrão é 64px; esta propriedade explícita também é suportada. -->
<nav
class="lyra-appsidebar lyra-appsidebar--rail"
style="--appsidebar-width: 64px"
aria-label="Navegação do espaço de trabalho"
></nav>
<!-- Uma substituição do consumidor para a barra lateral expandida. -->
<nav
class="lyra-appsidebar"
style="--appsidebar-width: 296px"
aria-label="Navegação do espaço de trabalho"
></nav>Acessibilidade
- AppSidebar renderiza um
<nav>nativo. Passearia-labelpara ele quando a página tiver mais de um landmark de navegação. - No modo
groups,hrefgera um link nativo,asChildpreserva o link do router e itens de ação continuam botões. O item ativo recebearia-current="page". O callback do item roda antes deonSelect(id, item). Usetarget="_blank"para abrir um link nativo em outra aba. - No modo de composição, os filhos permanecem seus elementos originais. Use âncoras reais para rotas;
no modo trilho, descendentes de link recebem
titleearia-labelde um rótulo explícito, título ou de seu conteúdo textual. - O controle de recolhimento é um botão nativo cujo nome acessível e tooltip vêm de
labels(em inglês por padrão). No modo trilho, rótulos de grupo, texto de item e badges ficam visualmente ocultos, enquanto os nomes dos controles e itens continuam disponíveis. - Um WorkspaceSwitcher no espaço da marca vira um gatilho compacto de avatar no modo trilho. O nome do workspace continua disponível para tecnologia assistiva; o popover mantém largura utilizável e fica dentro de um viewport estreito.
Destinos com React Router e Next.js
// React Router mantém a navegação no cliente.
<AppSidebar groups={[{ items: [{ id: 'relatorios', label: 'Relatórios', active: true,
asChild: <Link to="/relatorios" /> }] }]} labels={{ collapse: 'Recolher barra lateral', expand: 'Expandir barra lateral' }} />
// Next.js usa o mesmo contrato.
<AppSidebar groups={[{ items: [{ id: 'cobranca', label: 'Cobrança',
asChild: <Link href="/cobranca" /> }] }]} labels={{ collapse: 'Recolher barra lateral', expand: 'Expandir barra lateral' }} />Para um link nativo, use href: '/relatorios'. Para nova aba, acrescente
target: '_blank' e rel: 'noopener noreferrer'. asChild substitui o conteúdo do Link
pelo ícone e rótulo do item. Os rótulos padrão do controle são em inglês; passe labels para traduzi-los, como nos exemplos acima.
Controlled React shell
The example uses Shell mainAs="div" because this documentation page already owns the main
landmark. The topbar's AppSidebarTrigger controls an existing nav by id. Desktop collapse and
mobile opening are independent application state; the package does not choose breakpoints or
persist preferences. The initial server render is desktop, then a local media query reconciles
below 768px. Only one application navigation is active. Moving between viewports closes mobile
navigation, preserves the desktop rail preference and reconciles focus to the visible trigger.
AppSidebarTrigger always renders a native type="button", forwards its button ref, and provides
aria-controls and aria-expanded. Its default label is “Toggle sidebar”, variant is ghost, and
size is md; every size retains a 44px target. A consumer click handler runs first and may cancel
with preventDefault(). Disabled triggers do not request changes.
<AppSidebarTrigger
controls="workspace-navigation"
collapsed={desktopCollapsed}
onCollapsedChange={setDesktopCollapsed}
hotkey
/>
<AppSidebar id="workspace-navigation" collapsed={desktopCollapsed} />The optional shortcut is Cmd/Ctrl+B. It ignores editable fields, composing input, repeats, Alt/Shift and canceled events. Duplicate triggers for one id share one logical owner. The deepest eligible navigation or trigger containing the event target wins; outside those scopes, only one unambiguous owner can act. Background modal content cannot toggle. Focus inside desktop navigation returns to its eligible controlling trigger before either expansion or collapse; focus in application content stays in place. Offcanvas navigation retains its modal focus ownership.
Offcanvas navigation
AppSidebarOffcanvas adapts Drawer at the logical start edge, including RTL. It keeps Drawer's
modal focus trap, inert background, scroll lock, Escape/backdrop dismissal, initial focus,
return focus and exit presence. Its ref points to the outer panel; the child's ref still points
to the native nav. The required title names the dialog, and closeLabel defaults to “Close”.
A controlled open panel remains shortcut-eligible from inside even while its external trigger
is inert. Closed navigation is absent, and an exiting panel is inert.
<AppSidebarTrigger
controls="workspace-panel"
collapsed={!mobileOpen}
onCollapsedChange={(collapsed) => setMobileOpen(!collapsed)}
hotkey
/>
<AppSidebarOffcanvas
id="workspace-panel"
title="Application navigation"
open={mobileOpen}
onClose={() => setMobileOpen(false)}
returnFocusTo={() => triggerRef.current}
>
<AppSidebar aria-label="Application navigation" />
</AppSidebarOffcanvas>The host omits Shell.sidebar in mobile mode so no empty rail remains. Links keep their real
hrefs and router handlers. The example explicitly closes on accepted, unmodified navigation;
Offcanvas never intercepts routes. Desktop modal navigation can use the same adapter.
Rail hints and density
railTooltips defaults to false. Opt in to shared clipping-safe Tooltip hints for collapsed
data items. Their accessible names, active state, hrefs and router refs remain available. A child
handler's preventDefault() also cancels the sidebar selection callback. Expanded items and the
legacy default retain their current presentation.
Compose <Tooltip tip="Projects" escapeClipping> explicitly around custom links or opaque router
components. In rail mode AppSidebar preserves their accessible names without adding a duplicate
native title. Tooltip hints do not move focus and can be dismissed with Escape.
density="compact" is local: rows use --text-sm and a 32px minimum on fine-only pointers, with
44px targets on coarse or hybrid devices. The default SidebarGroup rhythm remains 36px.
The example's “Compact rows” control lets you compare both densities.
Scoped customization
Set aliases on the owning AppSidebar, or on the Offcanvas panel for its header and child sidebar.
For a portal to retain an application scope, supply a container inside that scope, as this
example does. These aliases introduce no global token values and do not style an independent
SidebarGroup. Existing theme, brand and direction scopes continue to apply.
| Alias | Owning treatment and unchanged fallback |
|---|---|
--appsidebar-background | Nav and offcanvas surface: --surface-card |
--appsidebar-foreground | Item text: --text-secondary; icons, role and toggle: --text-muted; user name and panel title: --text-primary; nav otherwise inherits |
--appsidebar-primary | Active icon and active badge ink: --accent |
--appsidebar-primary-foreground | Active badge surface: --surface-card |
--appsidebar-accent | Active row: --accent-soft; idle row/toggle hover: --surface-sunken |
--appsidebar-accent-foreground | Active row: --accent-soft-text; toggle hover ink: --text-primary |
--appsidebar-border | Nav, footer and panel separators: --border-default |
--appsidebar-ring | Focus color; shadow controls fall back to --focus-ring, native outlines retain their default |
--appsidebar-width-mobile | Offcanvas width: 18rem, capped to the actual visual viewport width |
Keep the existing --appsidebar-width contract (260px expanded, 64px rail), the global
--sidebar-width token and custom inline width precedence outside Offcanvas. Inside the adapter,
AppSidebar fills the panel even when its desktop width is explicit. The panel follows its owner
document's visual viewport width, height and start-edge offsets during zoom, resize and scrolling.
The mobile width alias still chooses the preferred width; the visible viewport caps it.
These companions and state rules are React capabilities, exported from both @lyra-ds/react
and the existing @lyra-ds/react/app-sidebar entry. Canonical HTML, Alpine and Blade producers
are unchanged; their tabs below describe their existing contracts, not the React adapter.
API e código
| Nome | Tipo | Obrigatória | Descrição |
|---|---|---|---|
brand | ReactNode | — | Optional brand content placed above the navigation groups. |
groups | AppSidebarGroup[] | — | Convenience data mode. Each group is composed through {@link SidebarGroup}; destinations render as links, while items without destinations remain buttons. |
footer | ReactNode | — | Optional utility links or user content separated below the navigation groups. |
width | number | — | Sidebar width in pixels while expanded. Default: `260`. In a content-scroll {@link Shell} where AppSidebar is the direct sidebar child, this width owns that rail. |
collapsible | boolean | — | Whether to render a control that switches between expanded and icon-rail modes. |
collapsed | boolean | — | Controlled icon-rail state. |
defaultCollapsed | boolean | — | Initial icon-rail state when uncontrolled. |
onCollapsedChange | (collapsed: boolean) => void | — | Called whenever the icon-rail state changes. |
onSelect | (id: string, item: AppSidebarGroupItem) => void | — | Called after an item-level callback in data mode. |
railTooltips | boolean | — | Show clipping-safe hints for collapsed data items. Default `false`. Compose Tooltip explicitly for custom children. |
density | 'default' | 'compact' | — | Local row density. Compact uses 32px fine-pointer rows and retains 44px coarse targets. Default `"default"`. |
labels | AppSidebarLabels | — | Localized labels for the collapse control. |
children | ReactNode | — | Composition mode. Pass {@link SidebarGroup} children (including link children) to preserve their original element type and routing behavior. |
AppSidebarTrigger
| Nome | Tipo | Obrigatória | Descrição |
|---|---|---|---|
collapsed | boolean | Obrigatória | Application-owned state: `true` means the controlled sidebar is collapsed or closed. |
onCollapsedChange | (collapsed: boolean) => void | Obrigatória | Requests the next state without keeping an internal mirror. |
controls | string | Obrigatória | Id of the controlled navigation or offcanvas panel. |
hotkey | boolean | — | Opt into document-scoped Cmd/Ctrl+B ownership. Default `false`. |
label | string | — | Accessible name and native tooltip. Default `"Toggle sidebar"`. |
variant | IconButtonProps['variant'] | — | Visual variant. Default `"ghost"`. |
size | IconButtonProps['size'] | — | Glyph sizing preset. Default `"md"`; the target is at least 44px for every size. |
children | ReactNode | — | Optional glyph; defaults to an inline panel-left icon. |
AppSidebarOffcanvas
| Nome | Tipo | Obrigatória | Descrição |
|---|---|---|---|
open | boolean | Obrigatória | Controls visibility. Closed navigation is absent; the exiting panel is inert. |
title | ReactNode | Obrigatória | Heading rendered in the existing Drawer header and used as the dialog name. |
closeLabel | string | — | Accessible name for the close button. Default `"Close"`. |
container | HTMLElement | — | Portal host. Defaults to `document.body`; use a scoped host to retain local tokens. |
initialFocusTo | () => HTMLElement | null | — | Resolves the initial focus target inside the panel on each accepted opening. |
returnFocusTo | () => HTMLElement | null | — | Resolves the current eligible focus destination after an accepted close. |
onClose | () => void | Obrigatória | Requests closure; the application owns `open`. |
children | ReactElement | Obrigatória | One AppSidebar element, filling the panel regardless of its desktop width, retaining its native nav and ref. |
x-data="lyraAppSidebar({ … })"
| Opção | Tipo | Obrigatória | Descrição |
|---|---|---|---|
defaultCollapsed | boolean | — | Whether the sidebar starts in its icon-rail state. Default: `false`. |
width | number | — | Sidebar width in pixels while expanded. Default: `260`. |
labels | LyraAppSidebarLabels | — | Localized labels for the collapse control. |
A porta de links compostos é uma marcação de navegação comum. Fora do React, implemente o
recolhimento, os rótulos do trilho e a seleção. Renderize o controle de recolhimento como um botão
nativo com aria-label; injete um chevron <svg> e inverta-o entre os estados expandido e trilho.
Se deixar o botão vazio, a folha de estilos fornece um chevron de reserva para o controle nunca
aparecer como uma caixa em branco, mas um ícone injetado permite refletir o estado de recolhimento:
<nav class="lyra-appsidebar" aria-label="Navegação do espaço de trabalho">
<div class="lyra-appsidebar__brand"><strong>Acme</strong></div>
<div class="lyra-appsidebar__groups">
<div class="lyra-sbgroup">
<div class="lyra-sbgroup__label">Espaço de trabalho</div>
<div class="lyra-sbgroup__items">
<a
class="lyra-sbgroup__item lyra-sbgroup__item--active"
href="/overview"
aria-current="page"
>
<span class="lyra-sbgroup__item-label">Visão geral</span>
</a>
</div>
</div>
</div>
<button class="lyra-appsidebar__toggle" type="button" aria-label="Recolher barra lateral">
<svg
aria-hidden="true"
width="15"
height="15"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- expandido: chevron aponta para dentro; troque por m9 18 6-6-6-6 no trilho -->
<path d="m15 18-6-6 6-6" />
</svg>
</button>
</nav><lyra:app-sidebar> Gerado do lyra-ds/blade v0.10.0.
O comportamento vem de lyraAppSidebar() — instale @lyra-ds/alpine e veja a aba HTML + Alpine.
| Prop | Padrão | Obrigatória | Valores de exemplo |
|---|---|---|---|
brand | null | — | — |
groups | [] | — | — |
footer | null | — | — |
width | 260 | — | — |
collapsible | false | — | — |
defaultCollapsed | false | — | — |
labels | [] | — | — |
<lyra:app-sidebar
:groups="[
['heading' => 'Workspace', 'items' => [
['id' => 'overview', 'label' => 'Overview', 'active' => true],
['id' => 'projects', 'label' => 'Projects', 'badge' => '12'],
]],
['heading' => 'Account', 'items' => [
['id' => 'billing', 'label' => 'Billing'],
['id' => 'settings', 'label' => 'Settings'],
]],
]"
collapsible
/>