Ir para o conteúdo

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 um AppShell.
  • Navegação local (dentro de uma tela): Tabs, SegmentedControl, Stepper.
  • Orientação e travessia: Breadcrumbs (onde estou) e Pagination (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 · Sidebar · Bottom nav na gallery

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

AppBar · Tabs · Tooltip · Drawer na gallery

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. 💡

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.

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.

Material (ListTile · FAB · Rail) na gallery

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

Stepper · Progress · VirtualList na gallery

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" },
            ]}
        />
    );
}

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

Table & Pagination na gallery

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 com aria-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/BottomNavigation para navegar entre seções; Tabs/SegmentedControl/Stepper para mover-se dentro de uma tela.
  • O trio Navbar + Sidebar + BottomNavigation são os slots do AppShell — deixe ele orquestrar a troca desktop/mobile.
  • Pagination para listas paginadas; Breadcrumbs para hierarquias profundas.
  • Sempre marque o item ativo com aria-current="page" na navegação primária.

Páginas relacionadas:

  • Layout — AppShell que compõe Navbar/Sidebar/BottomNavigation + Page.
  • Sobreposições — Drawer para expor a Sidebar no menu hambúrguer mobile.
  • Dados — Table/DataTable que usam Pagination no rodapé.
  • Roteamento — defineRoutes/<AppRouter>/<RouteGuard> que ligam a navegação às rotas.