Tema
BaseSegmented
Filtro rápido de uma lista: poucas opções lado a lado, cada uma com quantos registros traz. Só uma fica escolhida por vez.
Filtro escolhido: all
Uso
- Use acima de uma tabela ou lista para trocar o recorte com um clique: "Todos", "Em atraso", "Paralisadas".
- De duas a cinco opções, com rótulos curtos. Com mais opções ou rótulos longos, use uma lista de seleção.
- A primeira opção costuma ser "Todos", com o total da lista, e vem escolhida ao abrir a tela.
- A contagem é opcional. Quando existe, diz quantos registros a opção traz com os outros filtros da tela aplicados, e o número muda junto com a busca.
- Uma opção sem registros pode ficar desabilitada, mas prefira mostrá-la com 0: a pessoa entende que não há nada ali, em vez de achar que o filtro não funciona.
- O filtro age na hora, sem botão de aplicar. Para escolher uma resposta dentro de um formulário, use o BaseRadioGroup; para navegar entre seções da tela, use abas.
Estados
Use disabled enquanto a lista recarrega. As opções continuam no foco do teclado, mas ignoram cliques e setas, para a pessoa não perder o lugar.
Estados de formulário (invalid, warn, readonly) não se aplicam: o filtro não guarda dado e sempre tem uma opção válida. Também não há loading: quem carrega é a lista, que recebe aria-busy enquanto busca.
Tamanhos
A altura total segue a dos controles: 32, 40 e 48 px. Use o mesmo tamanho da busca ao lado.
Estilo
| Propriedade | Token |
|---|---|
| Trilho | --tj-layer-accent-01, contorno interno --tj-border-subtle-02 |
| Opção | texto --tj-text-helper, peso --tj-weight-medium; hover --tj-text-primary |
| Opção pressionada | --tj-layer-accent-02 |
| Opção escolhida | fundo --tj-layer-01, contorno interno de 1 px --tj-border-strong-02, texto --tj-text-primary, peso --tj-weight-semibold, sombra leve |
| Contagem | --tj-layer-accent-02; na opção escolhida --tj-layer-selected-01 e --tj-link-primary-hover |
| Desabilitada | texto --tj-icon-secondary |
| Foco | contorno sólido de 2 px --tj-border-interactive, por dentro da opção |
| Fonte | --tj-text-sm (--tj-text-xs no pequeno, --tj-text-md no grande) |
| Raio | --tj-radius-md na opção; no trilho, o mesmo raio mais o recuo de 3 px |
| Altura | --tj-control-sm/md/lg (32, 40, 48 px) no trilho |
O rótulo reserva a largura do peso semibold, então trocar a opção escolhida não empurra as outras.
Código
vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { BaseSegmented, type SegmentedOption } from '@lab-inovacao/design-system'
interface Contract { id: number; status: 'running' | 'late' | 'stopped' }
// Vem da consulta da tela, já com a busca e os outros filtros aplicados.
const contracts = ref<Contract[]>([])
const countOf = (status: Contract['status']) => contracts.value.filter((contract) => contract.status === status).length
const situation = ref('all')
const options = computed<SegmentedOption[]>(() => [
{ value: 'all', label: 'Todos', count: contracts.value.length, countLabel: 'contratos' },
{ value: 'late', label: 'Em atraso', count: countOf('late'), countLabel: 'contratos' },
{ value: 'stopped', label: 'Paralisadas', count: countOf('stopped'), countLabel: 'contratos' },
])
</script>
<template>
<BaseSegmented v-model="situation" label="Situação" controls="contracts-table" :options="options" />
<table id="contracts-table">…</table>
</template>| Propriedade | Tipo | Padrão | Descrição |
|---|---|---|---|
options | SegmentedOption[] | — | Opções na ordem em que aparecem |
label | string | — | O que as opções filtram; vira o nome acessível do grupo |
v-model | string | — | value da opção escolhida |
size | 'sm' | 'md' | 'lg' | 'md' | Altura do controle |
disabled | boolean | false | Ignora cliques e teclas; as opções seguem focáveis |
controls | string | — | Opcional. id da tabela ou lista que o filtro muda; vira aria-controls no grupo |
SegmentedOption:
| Campo | Tipo | Descrição |
|---|---|---|
value | string | Valor gravado no v-model; único no grupo |
label | string | Texto da opção |
count | number | Opcional. Quantos registros a opção traz, num BaseCount |
countLabel | string | Opcional. O que a contagem conta, como "contratos"; o leitor de tela ouve "Em atraso 5 contratos" |
disabled | boolean | Opcional. Desabilita só essa opção |
Evento: update:modelValue, com o value da opção escolhida.
Acessibilidade
Segue o padrão Radio Group do WAI-ARIA APG.
- Por que radiogroup e não botões
aria-pressed. O protótipo usava botões alternáveis numrole="group". Esses botões são anunciados como interruptores independentes ("botão alternável, pressionado"), o que esconde que só um fica ativo e sugere que clicar de novo desliga o filtro. Comrole="radiogroup"erole="radio", o leitor de tela diz que a escolha é única e a posição ("2 de 3"), e as setas seguem o mesmo modelo do BaseRadioGroup. - O grupo é um
<div role="radiogroup">comaria-labelvindo delabel. Cada opção é um<button type="button" role="radio">comaria-checked. - O nome de cada opção é o rótulo mais a contagem: "Em atraso 5". Com
countLabel, ganha a unidade: "Em atraso 5 contratos". Acima de 99 o contador mostra "99+", mas o leitor de tela ouve o número real. - Com
controls, o grupo aponta comaria-controlspara a tabela ou lista que filtra. - Teclado:
| Tecla | Ação |
|---|---|
| Tab | Entra no grupo pela opção escolhida (sem nenhuma, pela primeira habilitada) e sai dele na próxima vez |
| → ↓ | Move para a próxima opção e a escolhe; da última, volta à primeira |
| ← ↑ | Move para a opção anterior e a escolhe; da primeira, vai à última |
| Home End | Vai à primeira ou à última opção habilitada e a escolhe |
| Espaço Enter | Escolhe a opção em foco |
Home, End e Enter vão além do padrão Radio Group do APG e do BaseRadioGroup, como numa barra de ferramentas. Não atrapalham quem usa só as setas e o espaço.
Setas com Alt, Ctrl ou Cmd ficam com o navegador: Alt+← continua voltando a página.
As setas pulam opções desabilitadas. Opções desabilitadas usam
aria-disabledem vez dedisabled, para o leitor de tela continuar anunciando-as; o grupo desabilitado também recebearia-disabled.A opção escolhida não depende só da cor: ganha superfície elevada com contorno interno de 1 px em
--tj-border-strong-02(acima de 3:1 contra o trilho nos temas claro e escuro), sombra e peso semibold.O foco aparece como contorno sólido de 2 px em
--tj-border-interactive, por dentro da opção, acima das vizinhas e junto da sombra da opção escolhida. Fica por dentro porque o trilho rola de lado e cortaria um anel externo, e porque o anel translúcido--tj-ringfica abaixo de 3:1 sobre o trilho.Se a contagem muda enquanto a pessoa está na tela, avise com uma região
aria-liveperto da lista ("5 contratos em atraso"), não no filtro.Sem animação com
prefers-reduced-motion. No alto contraste do Windows (forced-colors), trilho, opção escolhida e foco usam as cores do sistema: a escolhida ganha bordaHighlighte o foco, contornoCanvasText, para não se confundirem. Toda opção tem a borda, transparente quando não escolhida, então a linha não muda de largura.Testado com axe em todos os estados: com e sem contagem, contagem acima do limite, sem opção escolhida, opção desabilitada (escolhida ou não), todas as opções desabilitadas, grupo desabilitado (com e sem opção escolhida), contagem com unidade, cada tamanho e com foco (numa opção comum, numa opção desabilitada escolhida e no grupo desabilitado). O axe roda sem layout, então o contraste das cores é conferido na documentação renderizada.