Navegação
Top bars, side navs, bottom nav, tabs, breadcrumbs, paginação, segmented control.
O que é esta categoria
Componentes que ajudam o usuário a se localizar e se mover pelo app. Eles se dividem por escopo:
- Navegação primária (entre seções do app):
Navbar(topo),Sidebar(lateral desktop),BottomNavigation(rodapé mobile) — tipicamente os três slots de umAppShell. - Navegação local (dentro de uma tela):
Tabs,SegmentedControl,Stepper. - Orientação e travessia:
Breadcrumbs(onde estou) ePagination(próxima/anterior em listas).
Quando usar: escolha pelo escopo — não use Tabs para navegar entre rotas de nível superior (isso é Navbar/Sidebar), nem Navbar para alternar visões de uma mesma tela (isso é Tabs/SegmentedControl).
Padrão responsivo Sidebar + BottomNavigation
A combinação idiomática: Sidebar dentro de <Show above="md"> no desktop e BottomNavigation dentro de <Hide above="md"> no mobile, ambos compartilhando o mesmo value/onChange. O AppShell já faz essa troca automaticamente quando você passa os dois slots.
Navbar
Seção nav-extra da gallery — rode localmente para interagir.
Quando usar: barra superior persistente com marca + ações globais (busca, avatar, notificações). É a navegação de mais alto nível.
App bar superior. Três slots (logo / nav / actions). Sticky por padrão.
import { Avatar, Button, Navbar } from "tempest-react-sdk";
import { NavLink } from "react-router";
import { Search } from "lucide-react";
const user = { photo: "/avatars/ana.jpg" };
export function TopBar() {
return (
<Navbar
logo={<img src="/logo.svg" alt="App" />}
nav={
<>
<NavLink to="/orders">Pedidos</NavLink>
<NavLink to="/products">Produtos</NavLink>
</>
}
actions={
<>
<Button variant="ghost" iconOnly aria-label="Buscar">
<Search size={16} />
</Button>
<Avatar src={user.photo} />
</>
}
/>
);
}
| Prop | Tipo | Default |
|---|---|---|
logo |
ReactNode |
— |
nav |
ReactNode |
— |
actions |
ReactNode |
— |
sticky |
boolean |
true |
tone |
"surface" \| "primary" \| "inverse" \| "transparent" |
"surface" |
bordered |
boolean |
true |
Safe-area: aplica padding-top: max(space-3, env(safe-area-inset-top)) automático.
Barra na cor da marca
tone="primary" pinta o fundo da barra com --tempest-primary e o texto dela
com --tempest-primary-foreground — e só. Os componentes dentro dela
continuam com os tokens da página. Medido no Chromium sobre o azul do SDK
(#0066ff, tema claro): um Button variant="link" some (1:1), o Button
primário tem a mesma cor da barra (borda 1:1), o texto muted fica em 1,59:1 e
o anel de foco em 1,41:1.
Quando a barra leva botão, link ou campo, use tone="inverse": ela recebe
data-tempest-tone="inverse" e todo token de cor se inverte lá dentro, como num
Section tone="inverse".
import "tempest-react-sdk/styles/inverse.css";
import { AppBar, Button, Navbar } from "tempest-react-sdk";
export function Barras() {
return (
<>
<Navbar
tone="inverse"
logo={<strong>Tempest</strong>}
nav={
<Button variant="link" href="/docs">
Docs
</Button>
}
actions={<Button>Criar conta</Button>}
/>
<AppBar title="Pedido" showBack tone="inverse" actions={<Button>Salvar</Button>} />
</>
);
}
Medido no Chromium. As linhas inverse dão o mesmo número na página clara e na
escura; a linha primary é a da página clara (na escura o 500 vira
#3b82f6: texto 5,25, link 1, muted 1,72, anel 1,45):
| marca | fundo | texto e ícone | link e ghost |
muted |
rótulo do botão primário | anel de foco |
|---|---|---|---|---|---|---|
SDK (#0066ff), primary |
#0066ff |
4,83 | 1 · 3,67 | 1,59 | 4,83 (botão = barra) | 1,41 |
SDK (#0066ff), inverse |
#042e75 |
12,71 | 12,71 · 12,71 | 9,60 | 12,71 | 6,95 |
#facc15, inverse |
#facc15 |
11,59 | 11,59 · 11,59 | 8,82 | 11,59 | 6,40 |
Por que um tom novo, e não primary invertendo
O fundo da superfície invertida nem sempre é o 500 da marca: o gerador
desce a rampa até o degrau que carrega os três níveis de texto. Para o
#0066ff esse degrau é o 800 (#042e75) — branco sobre o 500 mede
4,83:1 e não deixa espaço para muted. Fazer primary inverter repintaria
toda barra que já o usa. E só pôr o atributo não basta: dentro da
superfície, --tempest-primary é a cor de ação (branco no azul do SDK),
e a barra medida ficou #ffffff. Então primary segue igual à 0.74.0 —
capturas antes/depois idênticas byte a byte — e inverse é o pedido
explícito.
Sem a superfície invertida carregada
tone="inverse" depende do opt-in: tempest-react-sdk/styles/inverse.css
(azul do SDK) ou createTheme({ primary, inverse: createInverseSurface })
(sua marca). Sem nenhum dos dois, a barra cai nos tokens da página
(--tempest-bg e --tempest-text): legível — texto 17,75:1 no claro e
17,51:1 no escuro —, só não na cor da marca.
AppBar
Seção navigation da gallery — rode localmente para interagir.
Quando usar: app bar mobile-first de PWA — o padrão "voltar + título + ação" que toda tela de detalhe repete. Use AppBar em apps mobile/PWA; use Navbar quando precisar do nav horizontal de desktop (três slots).
Layout em grade: slot leading (botão voltar + marca) · título (<h1>) · actions (à direita). Sticky + safe-area por padrão. O botão voltar é acessível e, sem onBack, cai em window.history.back() — com router, passe onBack={() => navigate(-1)}.
import { AppBar, Avatar, Button } from "tempest-react-sdk";
import { Settings } from "lucide-react";
const user = { photo: "/avatars/ana.jpg" };
export function Barras({
navigate,
openSettings,
}: {
navigate: (delta: number) => void;
openSettings: () => void;
}) {
return (
<>
<AppBar
title="Perfil"
showBack
onBack={() => navigate(-1)}
actions={
<Button variant="ghost" iconOnly aria-label="Ajustes" onClick={openSettings}>
<Settings size={16} />
</Button>
}
/>
<AppBar brand="Famachapp" actions={<Avatar src={user.photo} />} />
<AppBar title="Histórico" showBack centered />
</>
);
}
| Prop | Tipo | Default |
|---|---|---|
title |
ReactNode |
— |
leading |
ReactNode (substitui voltar + marca) |
— |
showBack |
boolean |
false |
onBack |
() => void |
history.back() |
backLabel |
string (aria-label do botão) |
"Go back" |
backIcon |
ReactNode |
seta ← |
brand |
ReactNode |
— |
actions |
ReactNode |
— |
centered |
boolean |
false |
sticky |
boolean |
true |
tone |
"surface" \| "primary" \| "inverse" \| "transparent" |
"surface" |
bordered |
boolean |
true |
safeArea |
boolean |
true |
Com botão ou campo em actions sobre a cor da marca, use tone="inverse" — veja
Barra na cor da marca.
Customização visual
O SDK entrega só o layout + comportamento. Cor, altura e tipografia saem dos tokens --tempest-* (sobrescreva no :root). Para um ícone/menu customizado no lado direito, passe qualquer node em actions; para substituir o lado esquerdo inteiro (ex.: avatar no lugar do voltar), use leading.
A barra some ao rolar?
Quase nunca é a AppBar — é uma linha do CSS global do seu app:
/* ❌ o clamp horizontal que mata o sticky */
body {
overflow-x: hidden;
}
/* ✅ clampa igual, sem virar scroll container */
html,
body {
overflow-x: clip;
}
overflow-x: hidden no body força o overflow-y computado a virar auto — regra do CSS, não bug de browser — e com isso o body vira scroll container. Todo elemento sticky passa a se prender ao scrollport do body em vez do viewport, e esse scrollport rola junto com o documento. Medido no Chromium a 390px, numa página longa: a barra ficou em top: -900px depois de rolar 900px. Ou seja, fora da tela.
No desktop o sintoma quase não aparece, porque as telas costumam caber sem rolagem. No Chrome Android a barra de URL que encolhe transforma qualquer tela em tela longa — e aí some sempre.
O clip precisa estar nos dois elementos: com ele só no html, ou só no body, o documento continua arrastando na horizontal.
Em desenvolvimento a própria AppBar avisa no console quando detecta esse CSS. 💡
Sidebar
Side nav desktop. items: SidebarEntry[] (itens, seções e separadores), slots
header/footer, modo collapsed (apenas ícones).
import { useState } from "react";
import { Button, Sidebar } from "tempest-react-sdk";
import { Cog, Home, Package } from "lucide-react";
export function Lateral() {
const [tab, setTab] = useState("home");
const [collapsed, setCollapsed] = useState(false);
return (
<Sidebar
header={<strong>{collapsed ? "T" : "Tempest"}</strong>}
items={[
{ key: "home", label: "Início", icon: <Home size={16} /> },
{ key: "orders", label: "Pedidos", icon: <Package size={16} />, badge: 3 },
{ key: "settings", label: "Ajustes", icon: <Cog size={16} /> },
]}
value={tab}
onChange={setTab}
footer={<Button onClick={() => setCollapsed(!collapsed)}>Colapsar</Button>}
collapsed={collapsed}
width={240}
collapsedWidth={64}
/>
);
}
| Prop | Tipo | Default |
|---|---|---|
header |
ReactNode |
— |
items |
SidebarEntry[] |
— |
value |
string |
— |
onChange |
(key: string) => void |
— |
match |
"key" \| "route" |
"key" |
footer |
ReactNode |
— |
collapsed |
boolean |
false |
width |
number \| string (px ou CSS) |
240 |
collapsedWidth |
number \| string |
64 |
type SidebarItem = {
key: string;
label: ReactNode;
icon?: ReactNode;
badge?: ReactNode;
disabled?: boolean;
href?: string;
};
type SidebarEntry =
| ({ type?: "item" } & SidebarItem)
| { type: "section"; key: string; label: ReactNode }
| { type: "separator"; key: string };
type é opcional no ramo do item, então um SidebarItem[] continua sendo um
SidebarEntry[] válido — nenhum call site existente muda uma linha.
Agrupando em seções
Lista corrida funciona até uns 8 itens. Acima disso vira parede: 16 telas sem rótulo deixam "Diagnósticos" visualmente colado em "Campanhas", e o admin perde a âncora que dizia em que parte do painel ele está.
Uma seção abre um grupo, e os itens seguintes pertencem a ele até a próxima seção ou separador:
import { useState } from "react";
import { Sidebar } from "tempest-react-sdk";
import { Activity, BarChart3, FileText, Settings, Users } from "lucide-react";
function AdminNav() {
const [tab, setTab] = useState("overview");
return (
<Sidebar
items={[
{ type: "section", key: "monitoring", label: "Monitoramento" },
{ key: "overview", label: "Visão Geral", icon: <BarChart3 /> },
{ key: "activity", label: "Atividade", icon: <Activity /> },
{ key: "reports", label: "Relatórios", icon: <FileText /> },
{ type: "section", key: "users", label: "Gestão de Usuários" },
{ key: "users", label: "Usuários", icon: <Users /> },
{ type: "separator", key: "before-admin" },
{ key: "settings", label: "Configurações", icon: <Settings /> },
]}
value={tab}
onChange={setTab}
/>
);
}
O que sai no HTML:
<nav aria-label="Navegação lateral">
<div role="group" aria-labelledby="…-monitoring">
<div id="…-monitoring" role="presentation">Monitoramento</div>
<!-- Visão Geral, Atividade, Relatórios -->
</div>
<div role="group" aria-labelledby="…-users">…</div>
<hr />
<div><!-- Configurações, solto --></div>
</nav>
Por que role="group" e não um item estilizado
Usar disabled: true como rótulo renderiza <button disabled>: passa
visualmente com CSS, mas o leitor de tela anuncia "botão indisponível" no
lugar de um cabeçalho, e a entrada continua na árvore de navegação. Com
role="group" + aria-labelledby, o anúncio é "Monitoramento, grupo, 3
itens" — sem inventar um botão que não existe.
Itens antes da primeira seção ficam soltos
É o comportamento de sempre. Quem não usa seção nenhuma não vê role="group"
no HTML — a lista sai exatamente como saía antes.
No modo collapsed o rótulo sai de vista, não da árvore
64px não cabe "Gestão de Usuários", então o rótulo vira clip-path: inset(50%)
e o grupo ganha uma linha divisória no topo. O aria-labelledby continua
apontando pra ele, para o leitor de tela não perder a estrutura quando o admin
recolhe a coluna.
Item que é link
href renderiza um <a> em vez de <button>, e onChange continua disparando
no clique:
import { useState } from "react";
import { Sidebar } from "tempest-react-sdk";
export function ComLink() {
const [tab, setTab] = useState("overview");
return (
<Sidebar
items={[{ key: "overview", label: "Visão geral", href: "/overview" }]}
value={tab}
onChange={setTab}
/>
);
}
Com isso o middle-click abre em outra aba, o ctrl-click funciona, "copiar endereço
do link" aparece no menu de contexto, e o leitor de tela anuncia link em vez de
botão. Item disabled ignora o href e continua <button disabled>: âncora não
tem estado desabilitado, e tirar o href pra simular um deixaria um link que se
anuncia acionável e não é.
Item ativo a partir da rota
Quando a key (ou o href) de cada item é uma URL, quase nunca o pathname é
igual a uma delas: /dashboard/tips/42 tem que destacar /dashboard/tips. Passe a
rota em value com match="route" e o componente resolve sozinho:
import { useLocation } from "react-router";
import { Sidebar } from "tempest-react-sdk";
export function PainelNav() {
const { pathname } = useLocation();
return (
<Sidebar
match="route"
value={pathname}
items={[
{ key: "overview", label: "Visão geral", href: "/dashboard" },
{ key: "tips", label: "Dicas", href: "/dashboard/tips" },
{ key: "users", label: "Usuários", href: "/users" },
{ key: "admins", label: "Admins", href: "/users-admin" },
]}
/>
);
}
O item ativo é o de caminho mais longo que cobre a rota, em fronteira de
segmento. Compara contra o href do item, ou a key quando ele não tem href;
seções e separadores ficam de fora.
| Rota | Ativo | Por quê |
|---|---|---|
/dashboard/tips/42 |
Dicas | o mais longo vence — /dashboard também cobre |
/dashboard/tip |
Visão geral | /dashboard/tip não é segmento de /dashboard/tips |
/users-admin/3 |
Admins | /users não cobre /users-admin |
/users/?tab=2#topo |
Usuários | barra final, query e hash são ignorados |
/relatorios |
nenhum | nada cobre a rota |
Por que não find + startsWith
O items.find((i) => pathname.startsWith(i.href)) que todo painel escreve
primeiro erra 8 dos 11 casos medidos no #357: a ordem do
array decide quem vence (/dashboard fica aceso em toda tela) e
/dashboard/tip acende /dashboard/tips. O NavLink do react-router também
não resolve: cada link decide sozinho, então em /dashboard/tips/42 dois
itens ficam com aria-current="page".
A raiz / só acende em /
É a mesma regra do NavLink. Uma raiz que prefixasse tudo acenderia "Início"
em toda tela que o menu não lista.
O componente não lê router nenhum — value chega como string —, então continua
funcionando sem <Router> montado (window.location.pathname serve).
activeNavKey — a mesma resolução, fora do Sidebar
BottomNavigation, NavigationRail ou o menu dentro de um Drawer recebem
value como key. Para eles, a função pura devolve a key ativa:
import { useLocation } from "react-router";
import { activeNavKey, BottomNavigation } from "tempest-react-sdk";
const items = [
{ key: "/dashboard", label: "Painel" },
{ key: "/dashboard/tips", label: "Dicas" },
];
export function NavMobile() {
const { pathname } = useLocation();
return (
<BottomNavigation
items={items}
value={activeNavKey(pathname, items.map((item) => item.key))}
onChange={() => {}}
/>
);
}
activeNavKey(pathname, keys) devolve a key exatamente como veio em keys, ou
"" quando nada cobre a rota — nunca undefined, nunca lança. A comparação
diferencia maiúsculas, como caminho de URL. Com basename no router, passe o
pathname do useLocation() (que já vem sem ele) e keys sem o basename.
Mobile: esconda com <Show above="md"> e exponha via <Drawer> no menu hambúrguer.
NavigationRail
Seção material da gallery — rode localmente para interagir.
Quando usar: coluna de navegação vertical e compacta para desktop/tablet — uma alternativa mais estreita à Sidebar quando você só precisa de ícones empilhados sobre rótulos curtos. Cada item empilha ícone sobre o label; o ativo recebe aria-current="page".
items: NavigationRailItem[], slots header/footer e controle de rótulos via labelVisibility.
import { useState } from "react";
import { NavigationRail, FloatingActionButton } from "tempest-react-sdk";
import { Home, Inbox, Settings, Plus } from "lucide-react";
function AppRail() {
const [tab, setTab] = useState("home");
return (
<NavigationRail
header={<FloatingActionButton icon={<Plus />} aria-label="Novo" position="none" />}
items={[
{ key: "home", label: "Início", icon: <Home /> },
{ key: "inbox", label: "Caixa", icon: <Inbox />, badge: 3 },
{ key: "settings", label: "Ajustes", icon: <Settings /> },
]}
value={tab}
onChange={setTab}
labelVisibility="all"
/>
);
}
| Prop | Tipo | Default |
|---|---|---|
items |
NavigationRailItem[] |
— |
value |
string (key selecionada) |
— |
onChange |
(key: string) => void |
— |
header |
ReactNode (topo — ex.: FAB) |
— |
footer |
ReactNode (rodapé) |
— |
labelVisibility |
"all" \| "selected" \| "none" |
"all" |
Tipo NavigationRailItem = { key, label, icon?, badge?, disabled? }.
labelVisibility controla a densidade
Use "selected" para mostrar só o rótulo do item ativo (rail mais estreito) ou "none" para um rail puramente de ícones. Em telas pequenas prefira a BottomNavigation.
BottomNavigation
Tab bar fixa no rodapé pra mobile. 3-5 items.
import { useState } from "react";
import { BottomNavigation, Show } from "tempest-react-sdk";
import { Home, Search, ShoppingCart, User } from "lucide-react";
export function BarraInferior({ cartCount }: { cartCount: number }) {
const [tab, setTab] = useState("home");
return (
<Show below="md">
<BottomNavigation
items={[
{ key: "home", label: "Início", icon: <Home size={20} /> },
{ key: "search", label: "Buscar", icon: <Search size={20} /> },
{
key: "cart",
label: "Carrinho",
icon: <ShoppingCart size={20} />,
badge: cartCount,
},
{ key: "profile", label: "Perfil", icon: <User size={20} /> },
]}
value={tab}
onChange={setTab}
/>
</Show>
);
}
| Prop | Tipo | Default |
|---|---|---|
items |
BottomNavigationItem[] (3–5) |
— |
value |
string |
— |
onChange |
(key: string) => void |
— |
showLabels |
boolean |
true |
Tipo BottomNavigationItem = { key, label, icon?, badge?, disabled? }.
Safe-area: aplica padding-bottom: env(safe-area-inset-bottom) automático.
Tabs
Quando usar: alternar entre painéis de conteúdo dentro de uma mesma tela (visão geral / detalhes / logs). Não use para navegar entre rotas.
Tabs controlled/uncontrolled. Fade-edge mask em overflow horizontal. Variantes visuais via variant ("underline" default ou "pill").
import { useState } from "react";
import { Tabs } from "tempest-react-sdk";
export function Abas() {
const [tab, setTab] = useState("overview");
return (
<Tabs
activeId={tab}
onChange={setTab}
items={[
{ id: "overview", label: "Visão geral", content: <p>Visão geral</p> },
{ id: "details", label: "Detalhes", content: <p>Detalhes</p> },
{ id: "logs", label: "Logs", content: <p>Logs</p> },
]}
/>
);
}
| Prop | Tipo | Default |
|---|---|---|
items |
TabItem[] |
— |
activeId |
string (controlado) |
— |
defaultId |
string (não-controlado) |
— |
defaultValue |
string (uncontrolled) |
— |
onChange |
(key: string) => void |
— |
Stepper
Seção advanced da gallery — rode localmente para interagir.
Quando usar: mostrar progresso em um fluxo linear de múltiplas etapas (checkout, onboarding, wizard). É indicador de progresso, não um seletor — controle o current pela lógica do fluxo.
Wizard linear com steps numerados. orientation aceita "horizontal" (default) ou "vertical".
import { useState } from "react";
import { Stepper } from "tempest-react-sdk";
export function Passos() {
const [step] = useState(1);
return (
<Stepper
current={step}
steps={[
{ label: "Informações" },
{ label: "Pagamento" },
{ label: "Revisão" },
]}
/>
);
}
Breadcrumbs
Quando usar: sinalizar a posição em uma hierarquia profunda (Home › Pedidos › #12345) e permitir voltar a níveis anteriores. Dispensável em apps de 1-2 níveis.
Navegação hierárquica.
import { Breadcrumbs } from "tempest-react-sdk";
export function Trilha() {
return (
<Breadcrumbs
items={[
{ label: "Início", href: "/" },
{ label: "Pedidos", href: "/orders" },
{ label: "#12345" },
]}
/>
);
}
A11y: último item é marcado com aria-current="page".
Pagination
Seção table da gallery — rode localmente para interagir.
Quando usar: percorrer listas grandes em páginas discretas (resultados de busca, tabelas). Para feeds contínuos, prefira scroll infinito (VirtualList + usePoll/Query).
Numeric com siblings + page-size opcional.
page é 1-indexed, e quem conta as páginas é você
totalPages é o número de páginas — o componente não o deriva de pageSize. totalItems é opcional e só alimenta o texto de resumo ("1–20 de 137"). Lembre de resetar page para 1 quando o filtro muda, senão você pode parar numa página que não existe mais.
import { useState } from "react";
import { Pagination } from "tempest-react-sdk";
export function Paginacao({ totalItems }: { totalItems: number }) {
const [page, setPage] = useState(1);
const [size, setSize] = useState(20);
return (
<Pagination
page={page}
pageSize={size}
totalPages={Math.max(1, Math.ceil(totalItems / size))}
totalItems={totalItems}
onPageChange={setPage}
onPageSizeChange={setSize}
siblingCount={1}
/>
);
}
| Prop | Tipo | Default |
|---|---|---|
page |
number (1-indexed) |
— |
pageSize |
number |
— |
totalPages |
number |
— |
totalItems |
number (contagem, opcional) |
— |
onPageChange |
(page: number) => void |
— |
onPageSizeChange |
(size: number) => void |
— |
pageSizeOptions |
number[] — o pageSize atual sempre entra na lista |
[10, 25, 50, 100] |
siblingCount |
number (vizinhos) |
3 |
compactOnMobile |
boolean |
true |
Abaixo de 640px: sequencial por default, aleatório se você pedir
O default esconde os números e deixa ‹ / › mais o resumo. Num app desktop-first isso é razoável; num mobile-first custa exatamente o que a paginação existe para dar — chegar na página 7 vira seis toques em "próxima" em vez de um toque no 7.
<Pagination page={page} totalPages={pages} onPageChange={setPage} compactOnMobile={false} />
Com false, os números ficam em qualquer largura: a faixa rola sozinha na horizontal em vez de alargar a página, e a página atual é trazida para a área visível a cada mudança.
Por que um booleano e não uma variant
Só desligar o display: none empurraria uma fileira de nove botões para além de um viewport de 360px e daria barra de rolagem horizontal ao documento inteiro. O opt-out precisa trazer o próprio layout junto — então ele traz, e não há terceira opção para escolher.
SegmentedControl
Quando usar: alternar entre 2-5 visões mutuamente exclusivas da mesma tela (lista/grade/mapa). É mais compacto que Tabs e não tem painéis de conteúdo embutidos — você troca a view manualmente pelo value.
iOS-style pill bar (2-5 opções).
import { useState } from "react";
import { SegmentedControl } from "tempest-react-sdk";
import { LayoutGrid, List, Map } from "lucide-react";
export function Visao() {
const [view, setView] = useState("list");
return (
<SegmentedControl
value={view}
onChange={setView}
options={[
{ value: "list", label: "Lista", icon: <List size={14} /> },
{ value: "grid", label: "Grade", icon: <LayoutGrid size={14} /> },
{ value: "map", label: "Mapa", icon: <Map size={14} /> },
]}
size="md"
fullWidth
/>
);
}
| Prop | Tipo | Default |
|---|---|---|
options |
SegmentedControlOption[] |
— |
value |
string |
— |
onChange |
(value: string) => void |
— |
size |
"sm" \| "md" \| "lg" |
"md" |
fullWidth |
boolean |
false |
A11y: role="radiogroup" + role="radio" com aria-checked.
A11y geral
- Navbar: use
<nav>(já incluso); marque ativos comaria-current="page". - Sidebar/BottomNavigation: keyboard accessible — Tab cycle entre items.
- Tabs: setas ←→ trocam tab quando focada.
- Breadcrumbs: separador (
/) é decorativo (aria-hidden).
Marque o item ativo com aria-current
Navbar/Sidebar/BottomNavigation precisam que o item da rota atual carregue aria-current="page" — sem isso, leitores de tela não anunciam onde o usuário está. Breadcrumbs já faz isso no último item automaticamente.
Resumo
- Escolha pelo escopo:
Navbar/Sidebar/NavigationRail/BottomNavigationpara navegar entre seções;Tabs/SegmentedControl/Stepperpara mover-se dentro de uma tela. - O trio
Navbar+Sidebar+BottomNavigationsão os slots doAppShell— deixe ele orquestrar a troca desktop/mobile. Paginationpara listas paginadas;Breadcrumbspara hierarquias profundas.- Sempre marque o item ativo com
aria-current="page"na navegação primária.
Páginas relacionadas:
- Layout —
AppShellque compõeNavbar/Sidebar/BottomNavigation+Page. - Sobreposições —
Drawerpara expor aSidebarno menu hambúrguer mobile. - Dados —
Table/DataTableque usamPaginationno rodapé. - Roteamento —
defineRoutes/<AppRouter>/<RouteGuard>que ligam a navegação às rotas.




