Patterns

Padrões — shadcn/ui

Labeled Form Field + Error/Disabled State Pairing

Quando usar: input/select/textarea/switch com label, ajuda e/ou erro/disabled de validação. Como: envolver em Field, FieldLabel htmlFor, FieldDescription para ajuda, FieldError para erro (agrupar em FieldGroup; seções em FieldSet+FieldLegend). Estado: aria-invalid/disabled no controle e data-invalid/data-disabled no Field pai simultaneamente. Trade-offs: esquecer um dos dois atributos de estado deixa o estilo inconsistente com a funcionalidade.

Choice Card

Quando usar: seleção de plano/preferência onde o card inteiro deve ser clicável. Como: FieldLabel envolvendo o Field inteiro, controle (RadioGroupItem/Switch) como filho direto, orientation="horizontal".

Link Styled as Button

Quando usar: <a> que precisa parecer Button (navegação, não ação). Como: buttonVariants({variant, size}) como className em <a> puro, nunca Button render={<a/>}. Trade-offs: Button real força role="button", quebrando semântica de link.

Addon Inside Input

Quando usar: ícone, texto fixo, botão ou contador dentro da borda visual do Input/Textarea. Como: InputGroupInput/InputGroupTextarea dentro de InputGroup, com InputGroupAddon align="inline-start|inline-end|block-start|block-end" depois do controle no DOM. Trade-offs: mais boilerplate que um Input simples.

Three-Path Framework Installation

Quando usar: instalar shadcn/ui em Next/Vite/Astro/React Router/TanStack Start. Como: (1) shadcn/create gerando init --preset [CODE] --template <fw>, (2) init -t <fw> scaffoldando projeto novo, (3) projeto existente: Tailwind+alias manual, depois shadcn init. Trade-offs: (1)/(2) só para projeto novo; (3) único caminho para projeto existente.

New Theme Token

Quando usar: cor semântica nova além do tema default (esquecer .dark deixa o token quebrado no modo escuro). Como: definir em :root E .dark, expor em @theme inline como --color-<nome>: var(--<nome>).

Date Picker via Popover+Calendar

Quando usar: seletor de data compacto (não existe <DatePicker> pronto); evitar offset de um dia por timezone. Como: Popover > PopoverTrigger (render Button) > PopoverContent > Calendar; date-fns pra formatar; data-empty={!date} no trigger; setOpen(false) no onSelect pra fechar; detectar timezone com Intl.DateTimeFormat().resolvedOptions().timeZone dentro de useEffect (nunca no render) e passar via prop timeZone. Trade-offs: mais verboso que componente único; detectar timezone no render quebra hidratação SSR.

Combobox: multi-select com chips ou estilo "select popup"

Quando usar: seleção múltipla com tags removíveis, ou combobox que deve se comportar como Select fechado. Como: multi-select troca ComboboxInput por ComboboxChips > ComboboxValue + ComboboxChipsInput, value array, multiple na raiz. Select-popup usa ComboboxTrigger render={<Button/>} com ComboboxValue, e ComboboxInput showTrigger={false} dentro de ComboboxContent. Trade-offs: multi-select muda a estrutura inteira; select-popup perde busca sempre visível em troca de visual compacto.

Responsive Dialog (Dialog desktop / Drawer mobile)

Quando usar: formulário/painel com UX diferente por tamanho de tela. Como: useMediaQuery("(min-width: 768px)") decide Dialog vs Drawer; mesmo componente de form e state open/onOpenChange compartilhados. Trade-offs: duplica árvore de trigger/header, evita duplicar lógica do form.

Tooltip em elemento desabilitado

Quando usar: Button disabled precisa explicar o motivo via hover. Como: Button disabled não dispara pointer events; envolver com <span className="inline-block w-fit"> via TooltipTrigger render={<span/>}. Workaround padrão documentado.

Confirmação destrutiva (Alert Dialog)

Quando usar: ação irreversível que exige resposta explícita. Não fecha ao clicar fora, reservar pra decisões que precisam ser forçadas. Como: AlertDialogTrigger render={<Button variant="destructive">}, AlertDialogContent size="sm", AlertDialogAction variant="destructive".

Menu family reuse (Dropdown/Context/Menubar) + Command Palette

Quando usar: menu de ações (API idêntica nos três) ou busca rápida via teclado (Cmd+K). Como: menus: *Group+*Label seções; *CheckboxItem toggles; *RadioGroup+*RadioItem exclusiva; *Sub+*SubTrigger+*SubContent submenu; variant="destructive" ação perigosa (Dropdown exige *Portal ao redor de SubContent, Context Menu não). Command Palette: Command (base cmdk, não Base UI) dentro de CommandDialog open/onOpenChange controlado; CommandGroup heading, CommandEmpty.

Toast Imperativo com Undo

Quando usar: ação reversível (deletar, arquivar) sem bloquear a UI. Fluxo assíncrono longo prefira toast.promise. Como: capturar id de toast.add({actionProps: {children: "Undo", onClick(){toast.close(id)}}}).

Feature Opt-in Tree-Shakeable (TanStack Table v9)

Quando usar: data table só com os recursos necessários (mais setup que v8, mas bundle menor). Como: tableFeatures({...}) centralizado, compartilhado entre columns.tsx/data-table.tsx/page.tsx.

Chart Theming via CSS Variables

Quando usar: gráficos consistentes entre light/dark (Recharts v3 não usa mais hsl(var(...)), código antigo precisa ajuste). Como: --chart-1...--chart-5 em :root/.dark; chartConfig referencia "var(--chart-1)"; fill="var(--color-KEY)" (chave do config vira --color-KEY dentro do ChartContainer).

Controller Pattern Universal (React Hook Form)

Quando usar: conectar componente shadcn/ui controlado ao RHF (verboso por campo, mas controle total de markup). Como: <Controller render={({field, fieldState}) => <Field data-invalid={fieldState.invalid}><Control {...field} aria-invalid={fieldState.invalid}/><FieldError errors={[fieldState.error]}/></Field>}/>. Select/Checkbox/RadioGroup/Switch: mapear field.valuevalue/checked, field.onChangeonValueChange/onCheckedChange.

Dynamic Array Fields (3 variantes)

Quando usar: lista de itens adicionável/removível em formulário (nenhuma lib compartilha sintaxe, migrar entre elas reescreve a seção). Como: RHF useFieldArrayfields/append/remove; TanStack Form mode="array" no form.FieldpushValue/removeValue; Formisch <FieldArray>+funções insert/remove top-level.

Dark Mode via Classe CSS

Quando usar: toggle manual light/dark/system. Como: persistir preferência (localStorage Vite, cookie via remix-themes, next-themes no Next, script inline+MutationObserver no Astro), aplicar/remover classe dark no <html>. Trade-offs: estratégia difere por framework (SSR/hidratação); Next/Remix evitam flash via mecanismo próprio.

RTL via CLI Transform

Quando usar: suporte a árabe/hebraico/persa. Como: shadcn create --template <fw> --rtl (rtl:true), <DirectionProvider direction="rtl">, dir="rtl"/lang no <html>, fonte Noto. Trade-offs: automático só em estilos novos; Calendar/Pagination/Sidebar exigem migração manual; projeto existente roda shadcn migrate rtl.

Server Action Form (useActionState)

Quando usar: formulário Next.js com validação client+server sem RHF. Como: schema Zod + FormState → server action "use server" com safeParse retornando {values, errors, success} → client useActionState(action, initialState) ligando pending/data-invalid/aria-invalid. Trade-offs: roundtrip ao servidor a cada submit, mas elimina duplicação de schema.

Registry Item Override + Target Placeholders

Quando usar: customizar item de registry externo sem fork; ou fazer o registry próprio funcionar com aliases diferentes entre consumidores. Como: override cria item próprio com registryDependencies: ["@vendor/button"], sobrepondo cssVars/files com mesmo target (último vence). Portabilidade: files[].target usa @components/, @ui/, @lib/, @hooks/ em vez de caminho hardcoded. Trade-offs: override só vale pra mudanças pequenas (cor/token); sem placeholder pra utils, e paths no meio da string não são resolvidos.

Registry com Content Negotiation + Autenticação

Quando usar: URL raiz precisa servir landing/docs a humanos e JSON ao CLI/MCP sem subpath; e/ou registry não deve ser público. Como: negociação por headers Accept: application/vnd.shadcn.v1+json/User-Agent: shadcn (JSON se presentes, senão HTML, com Vary correto); autenticação via components.json#registries.@nome.headers.Authorization = "Bearer ${TOKEN}", servidor valida e retorna 401/403 customizado. Trade-offs: Open in v0 só aceita auth via query param ?token=, não headers.