@ksmv/ui-react
v0.22.1
Published
Accessible, theme-neutral React UI primitives with a semantic CSS token contract.
Maintainers
Readme
@ksmv/ui-react
Componentes React acessíveis e neutros em relação à marca. O pacote traz estrutura, comportamento e estados de interação. Cores, fontes, logos e seletores de tema continuam sendo do consumidor, que os fornece pelo contrato semântico --ui-*.
Antes de depender deste pacote
Os pacotes são publicados no registro público do npm para que as aplicações do próprio mantenedor os instalem sem autenticação. O repositório de origem é privado. Não há equipe de suporte, canal público de issues nem compromisso de atendimento a terceiros. Este README é a documentação completa para quem está de fora.
Vulnerabilidades não devem ser publicadas em nenhum canal público. Quem não tem acesso ao repositório relata pelo suporte do próprio npm, que encaminha relatos sobre pacotes publicados: https://www.npmjs.com/support
Instalação
npm install @ksmv/ui-reactA instalação é anônima: não exige token, .npmrc autenticado nem configuração de registro. O suporte é React e React DOM >=19 <20, declarados como peer dependencies.
CSS
Importe primeiro os estilos estruturais e depois o tema do produto:
@import "@ksmv/ui-react/styles.css";
@import "./theme.css";As regras da biblioteca usam :where(...) e têm especificidade baixa, então overrides do produto carregados depois vencem pela cascata normal.
Com Tailwind CSS 4, declare a ordem das layers e importe a biblioteca na layer ui:
@layer theme, base, ui, components, utilities;
@import "tailwindcss";
@import "@ksmv/ui-react/styles.css" layer(ui);
@import "./theme.css" layer(theme);Tema
Um tema completo define todos os tokens do contrato sob um seletor escolhido pelo consumidor. A lista canônica viaja no pacote:
@ksmv/ui-react/theme-contract.json: tokens obrigatórios emrequirede tokens derivados emderived, cada um com o token do qual deriva por padrão;@ksmv/ui-react/contrast-contract.json: pares de primeiro plano e fundo com o contraste mínimo exigido.
Para listar os tokens obrigatórios da versão instalada:
node -e "console.log(require('@ksmv/ui-react/theme-contract.json').required.join('\n'))"O ponto de partida mais curto é copiar @ksmv/ui-react/themes/neutral.css, que define o contrato inteiro sob .ui-theme-neutral, trocar o seletor e substituir os valores pelos do produto. O tema neutro existe para protótipos e testes, não como identidade de produto.
Use seletores textualmente distintos e disjuntos para claro e escuro:
[data-theme="light"] {
--ui-surface-page: #f8fafc;
--ui-text-primary: #0f172a;
/* ...todos os demais tokens de theme-contract.json */
}
[data-theme="dark"] {
--ui-surface-page: #020617;
--ui-text-primary: #f8fafc;
/* ...todos os demais tokens de theme-contract.json */
}Evite combinações sobrepostas, como .theme e .theme.active: a cascata resultante só pode ser verificada com evidência calculada em navegador, descrita no README de @ksmv/ui-checks.
Defina o atributo do tema antes da primeira pintura, num script no <head> executado antes de carregar o CSS da aplicação, para não haver um flash com tokens errados:
<script>
const savedTheme = localStorage.getItem("theme");
document.documentElement.dataset.theme = savedTheme === "dark" ? "dark" : "light";
</script>Onde persistir a preferência, como respeitar prefers-color-scheme e como sincronizar mudanças são decisões do consumidor; a biblioteca não acessa armazenamento nem impõe seletor.
Dois tokens de cor são opcionais e, sem declaração, herdam a ação primária: --ui-control-checked-bg (de --ui-action-primary-bg), o preenchimento de checkbox, rádio e switch marcados, e --ui-control-checked-fg (de --ui-action-primary-text), a marca sobre ele. O contrato de contraste exige 3:1 do preenchimento contra --ui-surface e da marca contra o preenchimento. Declare-os quando a cor primária servir de fundo de botão mas sumir sobre a superfície.
A borda do botão também tem token opcional por ação: --ui-action-primary-border (de --ui-action-primary-bg), --ui-action-secondary-border (de --ui-border-control) e --ui-action-danger-border (de --ui-action-danger-bg). O contrato de contraste exige 3:1 da borda contra --ui-surface, declarada ou herdada. Declare uma borda própria quando o fundo da ação sumir sobre a superfície, como um vermelho escuro num tema escuro. A borda não é conferida sobre --ui-surface-raised, onde ficam os botões do Dialog e do Drawer: ali o texto do botão, a 4,5:1, já identifica o componente.
As variantes de ação da 0.17.0 têm tokens próprios, todos derivados opcionais: um tema que não declara nenhum usa a origem, que já passa nos mesmos pares. O fantasma de destaque usa --ui-action-accent-text, --ui-action-accent-bg-hover e --ui-action-accent-bg-active (sem declaração, os do fantasma). O contorno de perigo usa --ui-action-danger-outline-text, -border, -bg-hover e -bg-active (sem declaração, os de status de perigo). O aviso usa --ui-action-warning-bg, -bg-hover, -bg-active, -text e -border (sem declaração, os do perigo). A borda do aviso deriva da borda do perigo, não do fundo do aviso: ao declarar o fundo do aviso, declare a borda junto, ou o botão âmbar fica com o contorno do perigo. O TextButton usa --ui-text-button-text (de --ui-link-text), --ui-text-button-accent-text (de --ui-text-button-text) e --ui-text-button-muted-text (de --ui-text-secondary), e o IconButton de 24px usa --ui-icon-button-size-xs (de --ui-space-6).
As variantes da 0.18.1 seguem a mesma regra. O suave de destaque usa --ui-action-soft-accent-bg, -bg-hover, -bg-active, -text e -border (sem declaração, os do fantasma de destaque: o fundo de hover em repouso, o ativo no hover e ao pressionar, e o texto como borda). O suave de aviso usa --ui-action-soft-warning-bg, -bg-hover, -bg-active, -text e -border (sem declaração, os de status de aviso, com o indicador como borda). O contorno de aviso usa --ui-action-warning-outline-text, -border, -bg-hover e -bg-active (sem declaração, os do contorno de perigo: num tema que não declara nada, ele aparece vermelho; declare a família para o âmbar). As bordas das duas variantes suaves e do contorno de aviso são o limite do controle, medidas a 3:1 sobre --ui-surface: uma borda translúcida, como uma cor a 25%, em geral não passa. O TextButton ganha --ui-text-button-strong-text (de --ui-text-primary), um token de hover por tom (--ui-text-button-text-hover, -accent-text-hover, -muted-text-hover e -strong-text-hover, cada um do token de repouso do mesmo tom, então nada muda sem declarar), --ui-text-button-font-size-xs (de --ui-font-size-xs, para levar só o botão de texto a outro tamanho, como 11px) e os pesos --ui-font-weight-medium e --ui-font-weight-bold (de --ui-font-weight-strong).
A superfície inversa (Surface tone="inverse") tem derivados -on-inverse, declarados na raiz do tema como os outros, todos opcionais e, exceto a divisória (--ui-border-on-inverse, decorativa e fora do mínimo de contraste), medidos sobre --ui-surface-inverse: --ui-text-primary-on-inverse (de --ui-text-inverse), --ui-text-secondary-on-inverse e --ui-text-muted-on-inverse (cada um do anterior), --ui-border-control-on-inverse (de --ui-text-inverse) e --ui-border-on-inverse (dele), --ui-link-text-on-inverse (do texto) e --ui-text-button-text-on-inverse (do link), --ui-action-ghost-text-on-inverse (do texto), --ui-action-ghost-text-hover-on-inverse (dele), --ui-action-ghost-bg-hover-on-inverse (transparent) e --ui-action-ghost-bg-active-on-inverse (dele), e --ui-icon-button-ghost-text-on-inverse (do texto do fantasma). Um tema que não declara nenhum pinta tudo no --ui-text-inverse e passa; o preço é que o fantasma não muda de fundo no hover até o tema declarar um, e que a divisória fica forte até o tema declarar o tom mais fraco. Não redeclare os tokens públicos num seletor aninhado: o @ksmv/ui-checks não mede esse tema sem evidência calculada, e os sobrepostos abertos de dentro herdariam o escuro. Desde a 0.22.1 (tema 3.4.0, contraste 12.0.0), --ui-surface-inverse-end (de --ui-surface-inverse) é a segunda cor da superfície: declarado, a Surface pinta um degradê do início ao fim da linha, de --ui-surface-inverse até ele; sem ele, o fundo continua liso. Todo par de um derivado -on-inverse medido sobre --ui-surface-inverse é medido também sobre a ponta (o @ksmv/ui-checks mede esse par em 17 pontos ao longo do degradê, em sRGB e em Oklab (os dois espaços em que o navegador interpola o degradê da Surface, que não declara espaço: sRGB para cores hex, rgb() ou hsl(), Oklab para as outras), porque um texto que passa nas duas pontas pode reprovar no meio), e o --ui-text-muted-on-inverse ganhou o seu par (inverse-muted-text), porque a descrição do Stat passou a usá-lo.
O Alert e o SectionLabel têm cores por derivados opcionais, todos com o padrão de hoje: --ui-alert-text (de --ui-text-primary) e --ui-alert-text-success, -warning, -danger e -info (do texto de status do tom); --ui-alert-border e as bordas por tom (da borda de status); --ui-alert-dismiss e o X por tom (do texto do aviso no mesmo tom), com o fundo de hover --ui-alert-dismiss-bg-hover (transparent); e --ui-section-label-text (de --ui-text-secondary), -danger (do texto de perigo), -warning (do contorno de aviso) e -strong (do texto principal). Para o desenho da casa, declare o texto principal no corpo dos avisos e a borda do tom translúcida.
O item de menu e o RadioCard também têm derivados opcionais: os tons do item em repouso, --ui-menu-item-text-accent (do destaque do botão de texto), -brand (do botão de texto) e -warning, -danger e -info (do texto principal); o fundo do ícone em círculo, --ui-menu-item-icon-bg (de --ui-surface-subtle); e o fundo do cartão marcado, --ui-radio-card-checked-bg (de --ui-surface), com o de perigo --ui-radio-card-checked-bg-danger (dele) e a borda de perigo --ui-radio-card-checked-border-danger (de --ui-status-danger). Sem declarar, os tons de perigo, alerta e info se parecem com o item comum e o cartão marcado fica no fundo de hoje; o tema da casa declara as cores. No hover e no foco, o item pinta --ui-action-ghost-text-hover, a cor que o contrato mede sobre o fundo do hover, com o --ui-action-ghost-text de reserva.
O Badge, o StatusIndicator e o ProgressBar também têm derivados opcionais: o selo de destaque cheio, --ui-badge-solid-accent-bg e -text (de --ui-surface-inverse e --ui-text-inverse; sem declarar, ele fica na superfície inversa), e o de marca cheio, --ui-badge-solid-brand-bg e -text (do primário); a bolinha apagada, --ui-status-indicator-muted (de --ui-disabled-text), e as bolinhas sobre a superfície inversa, --ui-status-success-on-inverse, -warning-, -danger- e -info-on-inverse (do limite de controle inverso; sem declarar, as quatro ficam nessa cor, e o label diz o estado); e as barras de destaque e de marca, --ui-progress-accent e --ui-progress-brand (do destaque do botão de texto e do botão de texto). O botão pressionado tem --ui-button-pressed-bg, -bg-hover e -text (do primário) e, na superfície inversa, --ui-button-pressed-bg-on-inverse, -bg-hover-on-inverse e -text-on-inverse (de --ui-surface, --ui-surface-subtle e --ui-text-primary: o claro sobre o escuro). O fundo pressionado é medido contra a página e contra o raised; num tema escuro, declare um pressionado claro com texto escuro, como um estado selecionado, porque o primário com texto branco raramente passa nos dois. A linha de lista com tom tem --ui-list-item-success-bg e -border, e o mesmo para warning, danger e info (do fundo e da cor do status); o número à frente do selo neutro tem --ui-badge-leading-text (de --ui-text-secondary). O cabeçalho de página tem --ui-page-header-eyebrow e a métrica tem --ui-stat-label, ambos de --ui-text-secondary. Os passos têm --ui-steps-current (do fundo do primário), --ui-steps-current-text (do texto do primário), --ui-steps-current-bg (de --ui-surface), --ui-steps-current-ring (de --ui-border; o anel é decorativo e aceita transparência) e --ui-steps-complete-bg (de --ui-surface-subtle); a trilha tem --ui-steps-track-filled (do fundo do primário) e --ui-steps-track-empty (de --ui-border-control). O contador da navegação lateral tem --ui-side-nav-count-bg e -text (de --ui-surface-subtle e --ui-text-secondary), os da superfície inversa --ui-side-nav-count-bg-on-inverse (transparente) e -text-on-inverse (de --ui-text-secondary-on-inverse), e a bolinha do recolhido, --ui-side-nav-count-dot (de --ui-status-info). O tema da casa declara as cores.
A conversa e a mídia têm derivados opcionais. A bolha tem, por tom (incoming, assistant e outgoing), o fundo, a borda, o texto e a meta, --ui-message-<tom>-bg, -border, -text e -meta (o incoming da superfície, o assistant do fundo suave de destaque e o outgoing do primário), e a citação, --ui-message-<tom>-quote-bg, -quote-bar e -quote-text (na enviada, o fundo é rgb(0 0 0 / 20%) e a barra e o texto são o texto do primário). A mensagem de sistema tem --ui-system-message-accent-bg, -border e -text (do fundo suave de destaque, da borda e do texto principal) e os mesmos de danger (do status de perigo). A mídia tem o véu, --ui-media-overlay-bg (rgb(0 0 0 / 70%)) com o texto --ui-media-overlay-text (#ffffff); os controles do visualizador, --ui-media-control-bg e -bg-hover (o branco a 20% e a 30%); a legenda, --ui-media-caption-bg (o preto a 50%); a base opaca sob os controles e a legenda, --ui-media-viewer-base (#262626); o fundo do visualizador, --ui-media-viewer-backdrop (o preto a 85%); e a borda do atual do carrossel, --ui-media-strip-current (do primário). A borda da bolha e a barra da citação não são medidas. A citação é medida sobre o fundo da bolha: com a citação e a bolha translúcidas, o par fica descoberto, e o checker o reprova.
A escolha de horário e de dia tem derivados opcionais. O horário livre tem --ui-time-slot-available-bg, -border e -text, e o hover, -bg-hover e -text-hover (do Button soft-accent); o ocupado tem --ui-time-slot-busy-bg, -border e -text (do status de aviso), e o ocupado marcado, --ui-time-slot-busy-selected-bg e -text (do Button warning); o anel do marcado, decorativo, é --ui-time-slot-ring (de --ui-border, e aceita transparência). O dia tem o texto do hoje, --ui-day-today-text (do texto do suave de destaque), o fundo e a borda do marcado na semana, --ui-day-selected-bg (do fundo do suave de destaque) e --ui-day-selected-border (do primário), e as cores do detail, --ui-day-detail-available e -busy (de --ui-text-secondary; o texto já diz a disponibilidade). O marcado do livre e do fora, o hoje e o círculo do dia marcado usam o par do pressionado, que precisa ser opaco.
A grade de calendário tem quatro derivados opcionais: o dia da semana escolhido, --ui-calendar-selected-text (de --ui-text-primary), o contorno do conflito, --ui-calendar-conflict (de --ui-status-danger; decorativo, não medido: diga o conflito em texto), e a faixa livre, --ui-calendar-free-border e -text (da borda e do texto do suave de destaque). Os eventos pintam a cor do tipo pela paleta categórica (colorIndex), sobre uma base opaca de --ui-surface, com o título em --ui-text-primary.
O quadro tem cinco derivados opcionais: o contador da coluna, --ui-board-count-bg e -text (de --ui-surface-subtle e --ui-text-primary), a borda do cartão no hover, --ui-board-card-border-hover (do primário), e o destino do arrastar, --ui-board-drop-bg (do fundo do suave de destaque, que aceita transparência e é composto sobre a sutil) e --ui-board-drop-border (do primário), que também pinta a linha de soltar. No escuro, o primário raramente passa contra a sutil: o tema escuro do catálogo declara as bordas em azul-claro. A borda de tom do cartão é decorativa.
A Table e o ToggleGroup também têm derivados opcionais. Na tabela: o texto do cabeçalho, --ui-table-header-text (de --ui-text-secondary); o fundo do cabeçalho com headerTone="subtle", --ui-table-header-bg (de --ui-surface-subtle); e a divisória entre as linhas, --ui-table-divider (de --ui-border). No chip (appearance="pill"), o marcado: --ui-toggle-group-checked-bg, -bg-hover e -text (do primário) e a borda --ui-toggle-group-checked-border (de --ui-control-checked-bg). No seletor com trilho: --ui-toggle-group-track-bg e -track-text (de --ui-surface-subtle e --ui-text-secondary) e o segmento marcado, --ui-toggle-group-segment-bg, -segment-text e -segment-border (de --ui-surface, --ui-text-primary e --ui-border-control). A borda do segmento marcado é o mínimo do contrato para mostrar o estado, medida a 3:1 contra o segmento; um tema que a tire reprova no @ksmv/ui-checks.
A paleta categórica tem oito categorias, cada uma com seis tokens opcionais: a marca --ui-category-N (de --ui-border-control), o suave --ui-category-N-bg, -text e -border (de --ui-surface-subtle, --ui-text-secondary e --ui-border) e o cheio --ui-category-N-solid e -on-solid (de --ui-surface-inverse e --ui-text-inverse), com N de 1 a 8. Um tema que não declara nenhuma categoria passa e pinta como hoje: o selo com categoria fica igual ao selo neutro, o avatar fica no inverso e a bolinha fica no limite de controle (no tema neutro, a mesma cor da bolinha neutra). themes/neutral.css declara oito matizes medidos.
Desde a 0.19.0, as três variantes têm também um texto para o hover e o pressionado: --ui-action-soft-accent-text-hover, --ui-action-soft-warning-text-hover e --ui-action-warning-outline-text-hover, cada um do texto de repouso da mesma variante, então nada muda sem declarar. Declare quando o hover escurece o fundo e o texto de repouso não passa sobre o fundo mais escuro: por exemplo, o texto principal sobre o fundo do hover.
Forma
Tamanho de fonte, pesos, entrelinha, altura e respiro dos controles, espaçamento, raios e fontes são o desenho da casa, e vêm da biblioteca: o styles.css os declara no início, em :where(:root). Um tema declara só cor (e a cor das sombras). A lista dos tokens de forma está em shape, no theme-contract.json, e a regra themes/no-shape do @ksmv/ui-checks aponta um tema que declare algum deles.
As fontes são Work Sans no corpo e na ênfase, Montserrat nos títulos de destaque (--ui-font-display, no título do Dialog) e JetBrains Mono nos números. O styles.css só declara as pilhas e não baixa nada; sem as fontes carregadas, cai em system-ui e ui-monospace. Para carregá-las, instale os pacotes do Fontsource e importe os pesos que a biblioteca usa:
@import "@fontsource-variable/work-sans";
@import "@fontsource/montserrat/700.css";
@import "@fontsource/jetbrains-mono/400.css";
@import "@fontsource/jetbrains-mono/500.css";A pilha do corpo aceita tanto "Work Sans Variable", o nome que o pacote variável registra, quanto "Work Sans", de um @font-face próprio ou de outra origem.
Um componente nasce com o desenho da casa, sem className de tamanho. Um ajuste pontual continua possível com className, e uma utilidade na camada utilities do Tailwind vence o componente; esse ajuste deve ser declarado com data-ui-override="motivo" no próprio componente, e a regra components/override do @ksmv/ui-checks, quando ativada, aponta o que não for declarado.
Continuam fixos, por serem desenho do componente: a entrelinha do selo, bordas e anel de foco, os indicadores de carregamento, a barra de progresso, o indicador de estado, a área de texto, a largura do diálogo, os indicadores dos controles de escolha (caixa, círculo, trilho e polegar), medidos em em sobre a fonte do controle, a largura mínima da coluna de cartões (16em) e a marca de seleção do ToggleGroup (1em).
O anel de foco é o do navegador (outline: auto, a 2px), que se ajusta sozinho a qualquer fundo, inclusive no botão de perigo e na superfície inversa. Não declare uma regra global de foco (:focus-visible { outline: … }) fora das layers: o CSS fora de layer vence o da biblioteca, e uma cor fixa como currentcolor pinta o anel, que fica do lado de fora do botão, na cor do texto dele: no botão de perigo, o anel branco some sobre a página clara em volta. Para o foco do seu próprio HTML, ponha a regra na layer base, que a biblioteca vence.
Para alvo de toque, mantenha as alturas de controle em pelo menos 24px (WCAG 2.5.8). O critério admite alvo menor quando o espaçamento em volta compensa, o que nenhuma verificação automática desta biblioteca enxerga; por isso a conferência fica com o produto.
Componentes
import { Badge, Button, Dialog } from "@ksmv/ui-react";Exportados: Accordion, AccordionItem, Alert, Avatar, Badge, Board, BoardCard, BoardColumn, Breadcrumb, Button, ButtonGroup, CalendarGrid, Card, Checkbox, Combobox, ConfirmDialog, Day, DayPicker, DescriptionItem, DescriptionList, Dialog, Divider, Drawer, EmptyState, Field, IconButton, Link, List, ListItem, LoadingIndicator, MediaStrip, MediaStripItem, MediaThumbnail, MediaViewer, Menu, MenuCheckboxItem, MenuGroup, MenuItem, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSub, MessageBubble, NavigationMenu, PageHeader, Pagination, Popover, ProgressBar, Radio, RadioCard, RadioGroup, SectionLabel, Select, SideNav, SideNavItem, Skeleton, Stat, StatusIndicator, Step, Steps, Surface, Switch, SystemMessage, Tab, TabList, TabPanel, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Tabs, TextButton, TextInput, Textarea, TimeSlot, TimeSlotPicker, Toast, ToastRegion, ToggleGroup e ToggleGroupItem, com os tipos TypeScript correspondentes, além de LinkRenderProps e RenderLink, os ganchos useFieldControl e useFieldLabelId (com o tipo FieldControlProps) e a função rangeTone (com o tipo RangeStop).
A biblioteca não inventa texto de produto, então a acessibilidade depende de alguns valores que só o consumidor tem:
IconButtonexige a proplabel, que vira o nome acessível do controle;Comboboxexigeoptions,value,onValueChange,noResultsLabeleopenLabel, e um nome: umFieldem volta,aria-labelledbyouaria-label; comloading, exigeloadingLabel;DialogeDrawerexigemtitleecloseLabel; forneçadescriptionquando o título não bastar;ToastRegionexigelabel, eToastexigetitle,closeLabel,openeonOpenChange; umToastfora de umaToastRegionlança erro;ConfirmDialogexige os textos de confirmar e cancelar, emconfirmLabelecancelLabel;Checkbox,Switch,RadioeRadioCardexigemlabel;RadioGroupexigelegendename;ToggleGroupexigelegend,nameetype, e cadaToggleGroupItemexigelabel, mesmo quando mostra só o ícone;CardcomtitleexigeheadingLevel(de 2 a 6), porque só a página sabe a hierarquia dos títulos;Accordiontambém exigeheadingLevel, e cadaAccordionItemexigetitleevalue;PopovereMenuexigemlabel, o nome do conteúdo aberto, erenderTrigger;NavigationMenuexigelabel, o nome da navegação;SideNavexigelabel, e cadaSideNavItemexigeicone um rótulo (comtextValuequando o rótulo não é texto e o item temcountou oSideNavrecolhe);Stepsexigelabel, e cadaStep,titleestatus;ButtonGroupexigelabel;Tableexigecaptioncom texto, o nome acessível da tabela;hideCaptiona esconde só da tela;Avatarexigelabel, o nome da pessoa ou da entidade, oudecorativequando o nome já está ao lado;TabListexigelabel;Breadcrumbexigelabel, o nome da navegação;Paginationexigelabel,previousLabelenextLabel;TimeSlotPickereDayPickerexigemlegendename; cadaTimeSlotexigevalueestatus, e cadaDay,value,labelenumber;BoardexigelabeleonMove; cadaBoardColumnexigeidetitle, e cadaBoardCardexigeidelabel, o nome do cartão no botão de mover e nos anúncios;CalendarGridexigelabel, osdays(comweekdayem texto, que entra no nome dos eventos) e, em cada evento e faixa livre,timeLabele umidsó seu (dois eventos, ou duas faixas, com o mesmoidsão recusados, como dois dias com o mesmovalue); o nome de um evento é o dia da semana, otimeLabele otitle, ou oaccessibleLabel;- a citação da
MessageBubbleexigelabel("Ir para a mensagem citada de ‹nome›"); aMediaThumbnailde imagem exigealtou umacaptionem texto, a de vídeo exigelabele a de documento exigename; oMediaStripexigelabel, e cadaMediaStripItem,nameeremoveLabel; oMediaViewerexigecloseLabelealt(oulabel, comaltvazio), edownloadLabeljunto dodownloadHref; - um indicador visual de estado nunca deve ser o único meio de comunicar significado.
Button, IconButton e TextButton renderizam type="button" quando nenhum type é passado, então um botão de ação dentro de um <form> não envia o formulário. No botão que envia, declare type="submit"; type="reset" também é respeitado.
Field dá o rótulo, a ajuda e o erro a um campo, no desenho da casa: rótulo de 12px, peso médio, na cor secundária, centrado com um ícone a 6px do texto; ajuda e erro de 11px, nessa ordem, abaixo do campo, a 4px. O erro fica numa região role="alert" que está sempre na página, vazia sem erro, para a mensagem ser anunciada quando chega; ela só entra no aria-describedby do campo quando há erro. A coluna do Field encolhe abaixo da largura mínima do campo (um type="date" não transborda) e, numa linha de grade mais alta, a sobra fica embaixo. labelAccessory põe um conteúdo na linha do rótulo, à direita, fora do nome acessível ("60 min" ao lado de "Tipo de compromisso"). TextInput, Select e Textarea têm texto de 12px e 36px de altura; com size="sm", o TextInput e o Select têm 28px, para editar numa linha de lista ou tabela, e o atributo nativo size passa a se chamar htmlSize. numeric (no TextInput e no Textarea) usa a fonte mono com algarismos tabulares, para valores, horários e datas. leadingIcon põe um ícone decorativo dentro do TextInput, no início, que não recebe clique; o ref e a className continuam no <input>. shape="pill" arredonda o TextInput e o Textarea, e resize="none" impede o Textarea de ser redimensionado. O Textarea mede pelas linhas (rows), com 8px acima e abaixo, e o size="sm" tem 6px × 8px. O Select mostra a mãozinha. Desabilitados, os campos são pintados em cor; o foco é o anel do navegador. A ajuda do Field, do Checkbox, do Switch, do Radio e do RadioGroup tem a cor de --ui-field-description (de --ui-text-secondary, medida a 4,5:1 sobre a superfície, o raised e o subtle); a descrição do RadioCard é conteúdo do cartão e não muda.
Select variant="inline" é a escolha dentro de uma frase ("Agenda de ‹Fulana›"): o <select> nativo, sem caixa, sem fundo e sem espaço, com a fonte da frase, em peso 600 e na cor secundária, e uma seta pequena da biblioteca na cor do texto (a seta nativa do WebKit aumentaria a linha da frase). Em cores forçadas, o navegador descarta a seta desenhada, e o select volta à aparência nativa, com a seta do sistema. Ele não aceita size, multiple nem htmlSize: é uma escolha só, numa linha. O texto da frase não é o rótulo dele: dê o nome por aria-label ou por aria-labelledby apontando para o texto da frase. Use-o só dentro de uma frase: o alvo dele é a altura da linha, pela exceção "inline" da WCAG 2.5.8; fora de uma frase, use o Select xs ou sm. Inválido, o texto fica na cor de perigo; desabilitado, na cor de desabilitado, sem fundo.
Um controle seu, de fora da biblioteca (um seletor de cor, uma faixa), se liga ao Field como os da biblioteca, pelo gancho useFieldControl({ id, required, "aria-describedby", "aria-invalid", "aria-required" }): dentro de um Field, ele devolve o id do rótulo (o do <label for>), o aria-describedby com o valor próprio primeiro e depois a ajuda e o erro, o aria-invalid com erro e o required e o aria-required do Field. As props que você passa vencem o Field (aria-invalid={false}, required={false} e um id próprio ficam); fora de um Field, ele devolve o que recebeu. Espalhe o resultado no elemento rotulável. Um controle que o <label for> não rotula (um grupo, um div com papel) usa aria-labelledby={useFieldLabelId()}; nele, required não é atributo válido: passe só o aria-required. Não copie a regra dos ids da ajuda e do erro: ela é interna e muda quando o filho tem id próprio.
Além de primary, secondary, danger e ghost, o Button tem ghost-accent (o fantasma na cor de destaque), danger-outline (contorno de perigo, sem fundo) e warning (a ação que passa por cima de um alerta), e, desde a 0.18.1, soft-accent (um atalho no fundo claro de destaque), soft-warning (o fundo claro de aviso, para assumir algo que outro conduz) e warning-outline (o contorno de aviso, para desabilitar). O ConfirmDialog aceita todas em confirmVariant. O IconButton aceita essas e mais três, só para ícone, porque um glifo é medido a 3:1 e um texto a 4,5:1: quiet-accent e quiet-danger (neutros em repouso, na cor do tom no hover e ao pressionar, para não competir com a ação principal de uma linha) e ghost-warning (para dispensar um aviso). Em repouso, os discretos têm a cor do ícone do fantasma (--ui-icon-button-ghost-text). outlined põe um contorno neutro (--ui-border-control) no ghost e nos discretos, sem sombra, e o contorno fica neutro também no hover: só o fundo e o ícone ganham o tom. O lg do IconButton tem 36px × 36px, a altura de um campo, e o ícone não fica na linha de base em nenhum tamanho. O nome continua sendo o label, e a cor nunca é o único sinal. Com size="xs", o IconButton tem 24px, para a ação dentro de uma linha de lista; o mínimo de 24px não cede a um --ui-space-6 menor. Com shape="circle", ele é redondo em qualquer variante e tamanho, com o diâmetro do tamanho: um md redondo tem a altura de um campo, para ficar ao lado de um campo em pílula. Com outlined, um quiet-accent ou quiet-danger ganha um contorno neutro em repouso, que passa ao tom no hover. Em repouso (e desabilitado), o contornado tem o fundo --ui-icon-button-outlined-bg (sem declarar, transparente), medido com o ícone e com o contorno sobre as superfícies; o hover e o pressionado têm os fundos da variante. Dentro da Surface tone="inverse", o fundo em repouso é sempre transparente.
Desde a 0.22.0, size="xs" vale também no Button e no Select: 24px, raio de 6px e texto de 11px, a medida dos controles do cartão da casa; o IconButton xs com borda (outlined ou uma variante de contorno, como o controle do cartão) tem o raio de 6px também. Desde a 0.22.1, o IconButton xs fantasma sem contorno (ghost, ghost-accent, quiet-accent, quiet-danger e ghost-warning), a ação dentro de uma linha de lista, tem o raio de 8px, o dos outros tamanhos (#391). O mínimo de 24px não cede a um --ui-control-height-xs menor. Não há TextInput xs: o menor campo de texto é o sm, de 28px. Um seletor que só mostra um glifo no lugar do valor não é um Select: use um IconButton xs que abre um Menu com as escolhas (MenuRadioItem). pressed faz do Button ou do IconButton um botão de alternância controlado: a biblioteca põe aria-pressed e pinta o par do ativo, e você troca o valor no onClick. Pressionado, o botão fica no primário (--ui-button-pressed-bg e -text) em qualquer variante, e, dentro da Surface tone="inverse", no claro (-on-inverse); em cores forçadas, na cor de destaque do sistema. O tipo não aceita pressed no primary, que é a variante padrão do Button: ali, pressionado e solto seriam iguais; parta de uma variante sem preenchimento, como o ghost contornado. Só o pressed pinta: um aria-pressed passado à mão não muda a aparência. Num gatilho de menu, o aria-expanded diz se o menu está aberto; um modo que persiste fica no item do menu (MenuCheckboxItem), e o gatilho pode ficar pressionado enquanto o modo está ligado (pressed={modoLigado}).
Desde a 0.19.0, os botões seguem o desenho da casa: o primário, o de perigo, o de aviso e os fantasmas não têm borda, e o fundo é o limite do botão; os preenchidos e os de contorno têm uma sombra leve, cuja cor vem do tema (--ui-button-shadow-color; sem declarar, sem sombra). O texto do fantasma escurece no hover (--ui-action-ghost-text-hover, do texto de repouso), e o IconButton fantasma tem a cor do ícone própria (--ui-icon-button-ghost-text e -hover). O fundo de hover do fantasma pode ser translúcido, como a casa pinta, e o @ksmv/ui-checks mede o texto sobre ele composto em cada superfície. O danger-outline tem fundo --ui-action-danger-outline-bg (sem declarar, transparente).
TextButton é o botão que se lê como texto: um <button>, para agir, onde o Link é um <a>, para navegar. Se muda de endereço, é Link; se faz alguma coisa, é TextButton. Ele usa a fonte do texto em volta (size="inherit", o padrão) ou --ui-font-size-sm e --ui-font-size-xs, com o peso de destaque. O sm tem a entrelinha dos controles (16px em 12px), e o xs, desde a 0.21.1, a do texto da casa, 1,5 (16,5px em 11px). tone é default, accent, muted ou strong (a cor do texto principal, para um nome clicável numa lista); cada tom pode ter uma cor de hover no tema. weight é strong (o padrão), medium ou bold. É sublinhado por padrão (underline="always"), porque a cor sozinha não o distingue do texto; underline="hover" e underline="none" servem só para uma ação fora de uma frase, como um nome numa lista ou uma ação no canto de um cartão, e nunca no meio de um texto corrido. Com iconVisibility="interaction", os ícones aparecem no hover e no foco por teclado, sempre numa tela de toque, e guardam o lugar, para o texto não pular. Os ícones têm 1.15em; uma classe no ícone vence. Com stretched, o alvo de clique cobre o ancestral posicionado mais próximo: ponha position: relative no cartão, e o cartão inteiro aciona o botão, com o anel de foco em volta dele. Uma ação secundária no mesmo cartão precisa de position: relative e z-index: 1 para ficar por cima. O alvo de clique tem pelo menos 24px sem aumentar a linha; entre dois botões de texto lado a lado, deixe pelo menos --ui-space-1.
Checkbox, Switch e RadioGroup com Radio são input nativos: entram em <form> e em FormData sem código extra e respondem a Espaço e, no grupo, às setas. Neles, className vai para o elemento raiz, que alinha o controle e os textos, e é ali que margem e largura fazem sentido; ref, name, checked, onChange, disabled e as demais props nativas vão para o input. No RadioGroup, ref e className vão para o fieldset, e a escolha chega por onValueChange, com value (controlado) ou defaultValue.
O estado misto do Checkbox vem da prop indeterminate. Como no controle nativo, um clique o limpa; para mantê-lo, atualize a prop no onChange. Um Radio só existe dentro de um RadioGroup, que lhe dá o name comum; fora do grupo ele lança erro. Com cores forçadas (forced-colors: active), os três voltam à aparência nativa, desenhada pelo sistema: o Switch passa a parecer um checkbox, e o papel switch continua anunciado.
Desde a 0.21.0, o Checkbox tem a forma do checkbox nativo: borda de 1px, raio de 2px e, marcada ou mista, a caixa inteira na cor marcada, com uma marca grossa de 75% da caixa. As cores são da ponte: --ui-checkbox-border (de --ui-border-control), --ui-checkbox-bg (de --ui-surface) e --ui-checkbox-mark (de --ui-control-checked-fg), medidas a 3:1. Com uma linha de texto, a linha da caixa mede 16px (cresce com o texto que quebra), e o rótulo leva um alvo de 24px × 24px sobre a caixa: o clique um pouco fora da caixa ainda a marca. Desde a 0.21.1, o alvo fica por baixo da caixa: no centro dela, quem recebe o ponteiro é o próprio input, e um teste que marca a caixa (check() do Playwright, elementFromPoint) chega a ele. Deixe 8px entre linhas de caixas: sem espaço, os alvos vizinhos se sobrepõem em até 8px, e a sobreposição, que inclui os 4px de baixo da caixa de cima, fica com a linha de baixo. Um ancestral com overflow: hidden rente à caixa corta o alvo. O rádio e o Switch não mudam: numa lista que mistura caixa e rádio, as linhas têm 16px e 28px. Checkbox size="sm" é a caixa compacta da casa, de 14px, para uma linha densa; o size numérico do input não vale num checkbox e sai do tipo. Desde a 0.20.0, o Switch tem o trilho da casa, de 36×20px, com a bolinha de 16px. labelPosition="start" põe o rótulo e a descrição no início e o controle no fim da linha, como numa lista de preferências, e o rótulo vem antes também na ordem de leitura. hideLabel esconde o rótulo, e a descrição, só da tela: eles continuam dando o nome e a descrição ao interruptor, e o rótulo cobre um alvo de 24px de altura, da largura do controle. O Switch liga com Espaço, como o checkbox nativo; o Enter fica com o formulário. Desde a 0.22.0, o rótulo de uma escolha tem a largura do texto: a linha continua um bloco (as caixas seguem uma embaixo da outra), mas o vazio depois do texto não marca mais, também no rádio e no Switch. Exceções: o Switch com labelPosition="start", cuja linha inteira liga e desliga, e o rótulo oculto. Quem quiser a linha inteira clicável põe justify-self: stretch no .ui-choice__label pela className. shape="circle" dá a caixa redonda da casa, com o anel de 2px, para concluir uma tarefa. hideLabel também existe no Checkbox: o rótulo e a descrição só para o leitor de tela, e a linha vira um quadrado de 24px com a caixa no centro, por baixo da qual fica o rótulo. A caixa oculta não aceita error (o quadrado não tem lugar para a mensagem): mostre o erro ao lado da linha.
RadioCard é um Radio com aparência de cartão: fica dentro de um RadioGroup, ao lado de Radio ou no lugar dele, e o cartão inteiro é a área de clique. O nome acessível é só o label; a description chega ao leitor de tela como descrição. Quando todas as opções do grupo são cartões, elas viram uma grade de colunas de pelo menos 16em, que cai para uma coluna em contêiner estreito; misturados com Radio, ficam empilhados. columns (1, 2 ou 3) no RadioGroup fixa o número de colunas, todas da mesma largura, e hideLegend esconde a legenda só da tela, quando um título visível já nomeia o grupo (o grupo continua nomeado por ela).
O cartão marcado ganha o fundo --ui-radio-card-checked-bg e a borda do controle marcado. tone="danger" usa o fundo e a borda de perigo, para um motivo de perda. hideIndicator tira a bolinha da tela: a borda e o fundo mostram o estado, e o rádio continua no DOM, escondido, para o teclado, o formulário e o leitor de tela; o anel de foco vai no cartão. Com cores forçadas, o cartão marcado sem indicador ganha a cor de destaque do sistema. align="center" centraliza o texto, para opções curtas lado a lado.
ToggleGroup é a seleção por botões, para um efeito imediato: trocar a visualização, filtrar, escolher um período. Com type="single", os itens são rádios: Tab entra no grupo uma vez, as setas movem e marcam, e clicar no item marcado não o desmarca; ofereça um item "Todos" quando "nenhum" for uma escolha. Com type="multiple", os itens são checkboxes, cada um com o seu Tab, e onValueChange recebe os valores na ordem dos itens; um valor do value controlado sem item correspondente sai do array no primeiro clique. No grupo único, cada seta já troca a escolha: se a troca for cara, prefira a escolha múltipla ou um Select. hideLegend e hideLabel escondem o texto só da tela, e o nome acessível continua. size aceita xs (24px), sm, md e lg. No item, className e title vão para o botão visível e ref para o input. Escolha de formulário que precisa de validação, com error e required, usa RadioGroup. Com cores forçadas, cada item mostra o rádio ou o checkbox do sistema colado ao seu botão, e o marcado ganha a borda de destaque do sistema.
Desde a 0.20.0, o ToggleGroup tem as duas formas da casa. O padrão, appearance="segmented", é o seletor de visão: um trilho com o segmento marcado na superfície, com sombra, borda fina e texto forte. appearance="pill" é o chip de filtro: pílulas com 6px entre si, e o marcado cheio na cor de marca. A ✓ do marcado sai do padrão e volta com showMark, na cor do texto do item. stretch estica o grupo e os itens na largura toda. No item, count põe um contador em fonte mono ao lado do texto (ele entra no nome: "Abertos 12"), e category (de 1 a 8) põe uma bolinha de legenda na cor da paleta categórica, decorativa. Para fazer algo ao clicar de novo no item marcado (recarregar a lista), use o onClick do item, que dispara também no marcado, pelo clique e pelo Espaço, sem trocar o valor. Para desmarcar o grupo único, controle o value e passe "" no onClick do item marcado.
Card é um contêiner com cabeçalho (title, description e actions), conteúdo (children) e rodapé (footer). O raiz é uma div; com as="section" ou as="article" e um title, o título dá o nome acessível ao cartão, e uma section nomeada aparece entre os marcos da página. Com as="li", ponha o cartão dentro de um ul ou ol seu: o Card não renderiza a lista. O cartão não é clicável. Para um cartão que leva a uma página, ponha um link no título; cartões clicáveis por inteiro, com seleção ou com composição de domínio ficam no produto, e a escolha por cartão é o RadioCard. O title aqui é o heading, em negrito, e a dica nativa do navegador não passa por ele. O cartão tem o padding da casa: 20px, e 24px a partir de 1024px de largura; padding="sm" dá 16px, e padding="none" leva o corpo às bordas (uma tabela ou uma lista de ponta a ponta), com o cabeçalho e o rodapé mantendo o padding. divider põe uma linha sob o cabeçalho, e bodyClassName vai no contêiner do conteúdo, para o layout dos filhos (flex, space-y-*). O Card é o ancestral posicionado de um TextButton stretched.
Alert é o aviso da casa: um ícone do tom (icon troca, icon={null} tira), um title em negrito, a mensagem (children), actions à direita e, com onDismiss e dismissLabel (o nome do X), um botão de dispensar. O texto tem 12px, e o raio e o padding são da casa (12px, nos tokens de forma --ui-alert-radius e --ui-alert-padding). O papel de announce ("polite" → status, "assertive" → alert) fica só no título e na mensagem: a ação e o X ficam fora da região com o papel de anúncio. Um role="alert" costuma ser lido quando o aviso aparece; um role="status" é lido quando o texto muda com o aviso já na página. Para um aviso passageiro depois de uma ação, use o Toast, que monta a região antes.
EmptyState diz que não há o que mostrar, com title, description, actions e um icon acima do título. frame escolhe a caixa: "box" (padrão), "dashed" (um espaço vazio, "solte aqui") ou "none" (dentro de um cartão que já tem moldura). size="sm" é o compacto, para uma coluna, e size="inline" é uma linha só, sem caixa, à esquerda, para uma frase dentro de uma seção; numa linha curta, a descrição é cortada com reticências.
SectionLabel é o rótulo de seção da casa: 11px, negrito, caixa alta, com um icon opcional e um conteúdo end à direita (uma contagem, uma ação). tone é "muted" (padrão), "danger", "warning" ou "strong", e as faz dele um título (h2 a h6) quando ele abre uma seção. O SectionLabel não tem "use client" nem hooks.
Popover e Menu abrem conteúdo sobre a página, com a Popover API e o CSS anchor positioning do navegador: não há portal, e o conteúdo fica no lugar dele no DOM e na árvore do React. Os dois, e o NavigationMenu, exigem navegadores mais novos que o resto da biblioteca: Chrome e Edge 128, Firefox 147 e Safari 26. Abaixo disso, sem anchor positioning, o conteúdo abre no centro da janela; sem Popover API, ele aparece no fluxo da página. Os demais componentes mantêm o piso de sempre.
O gatilho é seu, por renderTrigger: a função recebe popoverTarget, aria-expanded e o estilo com o anchor-name (no menu, também aria-haspopup e aria-controls), e você os espalha no botão, por exemplo renderTrigger={(props) => <Button {...props}>Ações</Button>}. Escape, clique fora e abrir outro popover fecham o conteúdo aberto. Perto da borda da janela, o conteúdo vira para o outro lado. Popover é não modal e não move o foco; para conteúdo que exige atenção total, use Dialog. Menu segue o padrão de menu do ARIA Authoring Practices: o primeiro item recebe o foco ao abrir, setas, Home e End movem entre os itens, Tab fecha, e escolher um item chama onSelect, fecha o menu e devolve o foco ao gatilho. Um MenuItem com disabled continua focável, com aria-disabled, e ignora a escolha. Um menu aberto dentro de um Dialog aparece acima dele. Desde a 0.20.0, o Menu e o Popover abrem alinhados pelo fim do gatilho, o padrão da casa; align="start" alinha pelo início, como antes. O lado segue a direção do texto do conteúdo, inclusive num trecho com dir="rtl" numa página LTR. O MenuSub e o NavigationMenu não mudam.
O children do Popover pode ser uma função, ({ close }) => ...: o close fecha o popover e devolve o foco ao gatilho, como o Escape, para um conteúdo que fecha por dentro (uma lista de notificações que fecha ao abrir uma). padding="none" tira o padding de 12px, para um cabeçalho de ponta a ponta e uma lista que rola; o conteúdo cuida do próprio espaçamento.
O MenuItem aceita icon, decorativo, de 16px antes do rótulo, ou num círculo de 28px com iconAppearance="circle"; description, uma segunda linha de 11px que chega ao leitor de tela como descrição (o nome é só o rótulo, ou o seu aria-label); e tone ("accent", "brand", "warning", "danger" ou "info"), a cor do texto e do ícone em repouso. No hover e no foco, o item usa a cor do hover, medida pelo contrato. A cor nunca é o único sinal: o rótulo diz o que o item faz.
MenuSub é um item que abre um submenu ao lado, para o lado de fim: label é o texto do item, e os filhos são os itens do submenu, que se nomeia pelo item (ou por menuLabel). Ele abre com clique, Enter, Espaço, a seta do lado de fim (direita em LTR, esquerda em RTL) ou com o mouse parado sobre o item por 200 ms; a seta do lado de início e o Escape fecham só o submenu e devolvem o foco ao item; escolher um item do submenu fecha o menu inteiro e devolve o foco ao gatilho do menu. Perto da borda da janela, o submenu vira para o outro lado. Use um nível só: dois já são difíceis no toque. Em todo menu, digitar leva o foco ao próximo item que começa pelo que foi digitado, sem diferença de caixa nem de acento; letras digitadas em menos de 500 ms formam uma palavra, e o Espaço no meio de uma palavra conta como letra. A busca lê só o rótulo, sem o icon e sem a description. Quando o rótulo não começa pelo que o usuário digitaria (um símbolo no próprio texto), passe textValue ao item.
MenuSeparator separa itens e MenuGroup os agrupa sob um título (label, obrigatório), que dá nome ao grupo; as setas passam pelos dois sem parar. MenuCheckboxItem (checked, onCheckedChange) e MenuRadioGroup com MenuRadioItem (value, onValueChange) são itens marcáveis e controlados: Enter, Espaço e o clique marcam sem fechar o menu, porque quem marca opções costuma marcar várias; Escape e Tab fecham, como sempre. Um MenuItem comum continua fechando ao ser escolhido.
Table e suas peças (TableHead, TableBody, TableRow, TableHeaderCell e TableCell) renderizam a <table> nativa: o leitor de tela anuncia nome, linhas, colunas e os cabeçalhos de cada célula. TableHeaderCell usa scope="col"; no corpo, scope="row" faz o cabeçalho de linha. A ordenação é sua: com onSort, o cabeçalho vira um botão e recebe aria-sort com o valor de sort ("none" quando não informado), e você reordena os dados no onSort. Mantenha um só cabeçalho com sort diferente de "none". align="end" alinha números ao fim, com algarismos de largura fixa, no cabeçalho e nas células. Quando a tabela é mais larga que o espaço, o contêiner dela entra na tabulação, com o nome da legenda, para rolar pelo teclado; quando cabe, não deixa parada de Tab. Não há listras nem realce de linha, que pediriam um par de contraste que o contrato ainda não tem. Desde a 0.20.0, a tabela tem a forma da casa: o cabeçalho de coluna é um rótulo (11px, caixa alta, espaçado, na cor secundária), a primeira célula de cada linha começa rente ao início, alinhada com o título de um Card, e a última linha do último corpo não tem linha embaixo. density="comfortable" dá 12px por 20px a cada célula, para linhas com avatar e ações; headerTone="subtle" pinta o fundo do cabeçalho; e verticalAlign="middle" centraliza as células na linha. O cabeçalho de linha, no corpo, mantém o texto do corpo. Com stickyHeader, o cabeçalho fica fixo enquanto o corpo rola; ele exige maxBlockSize (como "24rem"), a altura dentro da qual a tabela rola, e o contêiner entra na tabulação também quando só a altura transborda. O cabeçalho fixo tem fundo --ui-surface (ou o do headerTone="subtle"), para as linhas não aparecerem por baixo dele: dentro de um Card ou de um Dialog, que usam a superfície elevada, ele se destaca do resto.
Avatar mostra a imagem de src ou, sem ela ou quando ela não carrega, as iniciais do label (a primeira letra das duas primeiras palavras), que initials substitui. As iniciais usam o par de cores do texto inverso, verificado pelo contrato. Para distinguir pessoas, colorIndex pinta o avatar com uma das oito categorias da paleta do tema (abaixo). size aceita sm, md e lg, com as alturas dos controles, e xl (48px) e xxl (52px), fixos. Desde a 0.20.0, as iniciais usam --ui-font-display, em negrito, a tipografia da casa. shape="square" dá o avatar quadrado, para uma equipe ou um usuário, com 8px de raio até 40px e 12px nos tamanhos maiores.
O Badge tem 20px de altura mínima: com o icon (decorativo, de 12px, antes do texto) ou só com texto, ele fica em 20px; um conteúdo mais alto, como outro ícone ou um texto em duas linhas, faz o selo crescer, em vez de cortar. shape="tag" dá a etiqueta, com o raio pequeno, para tipo e categoria; o padrão é a pílula. variant="solid" com tone="accent" ou tone="brand" dá o selo cheio da casa, com o texto claro, e o tipo recusa o cheio nos tons de status. Desde a 0.22.0, size="md" (22px, texto de 12px, a nota de score da casa) e size="lg" (26px, texto de 12px, o selo da fila) se somam ao padrão sm; numeric põe o texto em mono, com algarismos tabulares; e leading põe um conteúdo antes do texto, lido junto com ele (ao contrário do icon), em mono e a 6px: a posição na fila ("1."), uma contagem. No selo neutro suave, o leading tem a cor --ui-badge-leading-text; nos outros, a do texto do selo. Caixa alta fica pelo conteúdo. rangeTone(valor, faixas) devolve o tom de status de um valor por faixas, para pintar uma nota (frio, morno, quente) com os tons que já têm contraste: ganha a primeira faixa cujo below é maior que o valor, e a última, sem below, pega o resto; um valor que não é número finito, ou acima de todas as faixas fechadas, dá "neutral". O número continua escrito: a cor nunca é o único sinal.
List e ListItem são a lista de linhas da casa (a agenda do dia): a List é um <ul> sem marcador, com 8px entre as linhas e o nome por label; cada ListItem tem leading (o controle no início, como a caixa redonda oculta), o título em children, numa linha, cortado com reticências, a description embaixo, em 11px, e actions no fim. A linha tem 8px × 12px, ao menos 40px de altura, raio de 12px e borda de 1px. tone pinta a linha por um status (danger é a atrasada da casa), com o fundo e a borda dos derivados --ui-list-item-<tom>-bg e -border: a ponte declara a cor da casa, inclusive com transparência, e o título e a descrição são medidos sobre esse fundo, composto sobre --ui-surface e sobre a superfície dos sobrepostos (--ui-surface-raised). A linha com tom não é medida sobre --ui-surface-subtle: ponha a lista na página, num Card ou num sobreposto, e não sobre um fundo cinza. O tom nunca é o único sinal: diga o estado também em texto, como um selo. done dá a feita: fundo --ui-surface-subtle e título riscado na cor secundária, também sobre um tom. A linha não é clicável por inteiro, para um controle não ficar dentro de outro: para navegar, ponha um Link no título.
PageHeader é o cabeçalho de página da casa: um <header> com o título (um <h1> por padrão; level troca o nível), o sobretítulo (eyebrow, com eyebrowIcon decorativo de 16px), a description, a media (um ícone ou avatar antes do texto, a 12px) e as actions. O título é Montserrat de 24px; com size="hero", 30px a partir de 640px. De 640px para cima, as ações ficam no fim da linha, a 16px do texto; abaixo, embaixo do texto, na largura toda, dividida entre os botões. A quebra é da janela, como na casa: numa coluna estreita de uma janela larga, o cabeçalho fica em linha. Ele não tem moldura nem margem: ponha-o num Card, e o espaço até o bloco seguinte é da página. hideTitle deixa o título só para o leitor de tela, nas telas sem título visível, e então só as ações aparecem.
Stat é a métrica da casa: o label (12px, com o icon decorativo de 14px), o value em mono (24px, e 30px a partir de 640px), o extra na linha de base do valor (uma unidade, um período) e a description (11px, com a entrelinha de 16,5px desde a 0.22.1). O rótulo e a descrição cortam com reticências. align="end" alinha tudo no fim, só a partir de 640px. É um <div>, lido na ordem: o rótulo antes do valor. Dentro da Surface tone="inverse", ele usa as cores da superfície inversa: o rótulo e o extra em --ui-text-secondary-on-inverse e, desde a 0.22.1, a descrição em --ui-text-muted-on-inverse (sem declarar, a mesma cor do rótulo). Não há seta de tendência: ponha uma no extra ou na description, com o tom pelo rangeTone.
DescriptionList e DescriptionItem são a ficha da casa: um <dl> com o termo (10px, em caixa alta, na cor do rótulo de seção) em cima do valor (12px), a 2px, e 12px entre os pares. columns={2} dá duas colunas a partir de 640px. O valor longo quebra a linha, nunca corta.
SideNav e SideNavItem são a navegação lateral da casa: um <nav> nomeado por label, com uma lista de links. Cada item tem o icon (obrigatório, decorativo, de 16px), o rótulo em children (12px, numa linha, cortado com reticências), o count (um número numa pílula; o link se chama pelo rótulo e pelo número, como em "Conversas, 12", por aria-label; com 0 não aparece) e current, que põe aria-current="page" e pinta o item com o par do botão pressionado. O item tem ao menos 36px e cresce com a fonte do navegador. Ponha o SideNav numa Surface tone="inverse", como a casa: dentro dela, ele usa as cores da superfície inversa; a moldura e a largura são suas. collapsed (controlado) mostra só o ícone, deixa o rótulo para o leitor de tela, dá ao link um title e troca o contador por uma bolinha; com um rótulo que não é texto, passe textValue, que também é pedido num item com count. O botão de recolher e o estado são do app. Os itens são filhos diretos do SideNav: passe-os como lista (itens.map(...)), e não num fragmento ou num componente, que o SideNav recusa. O renderLink é o mesmo RenderLink do NavigationMenu: o adaptador do roteador precisa repassar ao <a> tudo o que recebe (href, className, children, aria-current, aria-label e title).
Steps e Step são os passos da casa: uma <ol> nomeada por label, com o passo atual em aria-current="step" e o estado lido em texto oculto ("concluído", "atual", "a fazer", trocáveis por statusLabels, um objeto parcial). Em cartões (o padrão, o onboarding), cada passo tem o marcador de 36px, o título com o número, a description, o badge ao lado do título e a action no fim; o atual tem borda de 2px e um anel no marcador. Os passos são filhos diretos, como os itens do SideNav. variant="track" é a trilha das fases do lead: um bloco de 6px por passo, em colunas iguais, com o título e a posição no title (passe textValue se o título não for texto); tone="danger" pinta os cheios na cor de perigo, para o lead perdido.
ButtonGroup cola botões (Button) vizinhos, com os cantos só nas pontas e a borda de dentro compartilhada (no xs, os botões ficam lado a lado, para manter o alvo de 24px). É um role="group" nomeado por label, e não muda as props dos botões: um gatilho de Menu continua sendo um gatilho. Para alternâncias independentes, use pressed em cada botão, com pressed={false} explícito quando desligado; para escolher uma entre opções, use o ToggleGroup, que tem o teclado de um grupo de rádios.
MessageBubble é a bolha da conversa da casa: a label em cima (o nome e a hora, do app), a bolha com o texto (que quebra a linha e guarda as quebras) e a meta no canto de baixo (a hora e o estado de entrega, do app), e as actions ao lado, como o Responder. side="start" (padrão) põe a bolha no início, e side="end" no fim; o rabicho é o canto de cima do lado do autor. tone pinta pelo autor: "incoming" (o lead, padrão), "assistant" (a IA) e "outgoing" (a equipe). A bolha ocupa até 75% da lista (88% abaixo de 640px), então a lista precisa ser a largura da conversa. As ações aparecem no hover e no foco, e sempre em toque, mas continuam na ordem de Tab. A quote é a mensagem citada, um botão (onClick) ou um link (href), com author, o texto (cortado em duas linhas) e o label, o nome acessível; não ponha nada interativo dentro dela. Para levar o foco à mensagem citada, dê a ela id e tabIndex={-1} e chame focus() no clique da citação: a linha mostra o anel do navegador.
SystemMessage é o aviso da conversa (layout="center", o padrão: até 448px, centrado, com title, icon, o texto e a time) e a faixa de "não lidas" (layout="band": a largura toda, com o ponto, o texto e as actions, que descem abaixo de 640px). tone é "accent" (padrão) ou "danger". É um role="note" e não anuncia nada sozinho: para anunciar mensagens novas, use a região do app (role="log") ou um Toast.
MediaThumbnail mostra a mídia de uma mensagem por kind: "image" é um botão que abre o visualizador (onOpen), nomeado por openLabel (padrão "Ampliar") e o alt (ou a caption, com alt vazio), com até 340px de altura; "video" é o <video> com os controles nativos, nomeado por label, com a duration num canto; "document" é um cartão com o icon do tipo, o name (cortado), o size e o link de baixar (downloadHref, nomeado por downloadLabel, padrão "Baixar", e o nome). Não ponha nada interativo na miniatura.
MediaStrip e MediaStripItem são o carrossel de anexos antes de enviar: uma lista que rola na horizontal, cada item com o botão de escolher (a miniatura de 56px, nomeada pelo name, com aria-current no current) e o botão de remover, irmãos, com um alvo de 24px no canto. O thumbnail é só a imagem, decorativa. Os itens são filhos diretos do MediaStrip, passados como lista; um fragmento é recusado. Remover é do app, no onRemove; quando o item sai, o foco vai ao próximo, ao anterior se era o último, ou ao emptyFocusRef (o campo da mensagem) se a lista ficou vazia; sem ele, a própria lista, vazia e nomeada, fica com o foco.
MediaViewer é a imagem em tela cheia: um diálogo modal nomeado pelo alt (ou pelo label; com o alt vazio e sem label, ele lança erro, porque o tipo não distingue a string vazia), com o Fechar e o Baixar (downloadHref) no canto de cima, a imagem e a caption centradas, sem passar por baixo dos controles, e a legenda que rola quando é longa. O foco começa no Fechar, o Esc fecha e o foco volta a quem abriu, como no Dialog, de que ele herda o ciclo modal.
TimeSlotPicker e TimeSlot são a escolha de horário da casa: um grupo de rádios (nativos, ocultos) numa grade de columns colunas (7 por padrão; um número inteiro a partir de 1), um chip de 43px por horário, com a hora e o estado (status: "available", "busy" ou "outside", este tracejado), os três escolhíveis. Os textos dos estados vêm de statusLabels (padrões "Livre", "Ocupado" e "Fora"). O reason diz por que o horário está ocupado ou fora: é a descrição do rádio e o title do chip. A grade nunca estreita os chips: se não couber, ela rola na horizontal.
DayPicker e Day são a escolha de dia: variant="strip" (padrão) é a faixa da Agenda, que rola, com chips de 68px × 38px, e variant="week" é a semana do modal, em 7 colunas, com o número num círculo. A biblioteca não calcula datas: passe os dias (value, label com o dia da semana, number e o detail, com detailTone) e o value de hoje em today, cujo rádio fica com aria-current="date". accessibleLabel dá a data por extenso ao leitor de tela. As setas da semana, o "Hoje" e a contagem de cada dia são do app; ao trocar a semana, mova o foco para o botão que a trocou, porque o rádio focado pode sair (o Safari não foca um botão clicado).
Os dois são grupos de rádios: um ponto de Tab, e as setas movem e marcam, uma opção por vez, também na grade. O teclado é o do navegador, com as diferenças dele: num grupo sem marcado, o Tab entra no primeiro rádio e o Shift+Tab, no primeiro ou no último; o Firefox também para na área que rola (a grade ou a faixa estreita), antes dos rádios; e o WebKit não espelha as setas horizontais em RTL. Como em todo grupo de rádios, marcar acontece a cada seta: se marcar for caro (abrir um aviso), espere o fim da escolha. São controlados (value com onValueChange) ou não (defaultValue, que o reset do formulário restaura), nunca os dois; name põe a escolha no formulário, e disabled vale no grupo e em cada opção. Ao focar uma opção, a célula rola para a vista inteira, com o anel.
CalendarGrid é a semana da Agenda: um dia por coluna (days), as horas de startHour a endHour em linhas de 56px, e os events postos pela hora, em minutos desde a meia-noite (start e end), com o timeLabel pronto. A biblioteca não calcula datas nem horas. Os eventos que se cruzam ficam lado a lado, em raias de largura igual, e a coluna cresce para manter 24px por raia; a grade rola na horizontal quando não cabe. Um evento curto tem 24px de altura, um evento que passa do intervalo é cortado com a ponta reta, e um todo fora dele não aparece: escolha as horas da semana. colorIndex dá a cor do tipo, como no Badge; highlighted marca o evento aberto no painel do app (aria-current="true"). Com onSelectDay, cada dia do cabeçalho é um botão pressionado no selectedDay, cuja coluna mostra as freeSlots; today marca o hoje. dayExtra põe uma faixa do app sob cada dia, que cresce com o conteúdo. O Tab segue o tempo: os dias, as faixas do dia e, dia por dia, os eventos e as faixas livres; não há setas. Numa semana muito cheia, ofereça também a lista do dia (List).
Board, BoardColumn e BoardCard são o funil da casa: colunas de cartões, em duas visões (layout="compact", o padrão, divide a largura com 150px no mínimo por coluna; "panoramic" dá 312px por coluna e rola na horizontal). Dê ao Board a altura que ele deve ocupar: o corpo de cada coluna rola. O estado é seu: o Board não guarda a ordem, e onMove(cardId, columnId, index) é um pedido, com o índice final na coluna de destino depois de tirar o cartão da origem. Aplique-o aos filhos. Quando eles mostram o cartão no lugar pedido, o quadro anuncia ("‹cartão› movido para ‹coluna›, posição 2 de 3.") e devolve o foco ao botão de mover do cartão; se não mostram em 1 segundo, ele anuncia que o cartão não foi movido. canDrop(cardId, columnId) devolve true ou o motivo da recusa ("fase final"), que entra no anúncio e no item desabilitado do menu. Cada cartão tem um botão de mover de 24px, que abre um menu com "Mover para ‹coluna›" (vai ao fim dela), "Subir", "Descer", "Para o topo" e "Para o fim": é a operação sem arrastar, pelo clique, pelo toque e pelo teclado. Com o mouse ou o dedo, o mesmo botão também arrasta o cartão, com a linha de soltar, a rolagem automática nas bordas e o Esc para cancelar. Os textos vêm de announcements, um objeto parcial sobre os padrões em português. As colunas e os cartões são filhos diretos, passados como lista, com ids únicos no quadro.
Desde a 0.20.0, o StatusIndicator tem 8px por padrão; size="sm" dá 6px, antes de um texto curto, e size="lg" dá 10px. `animation="
