Executando verificação de segurança...
1

Como Criar um Design System com Radix UI e Tailwind CSS

O problema real: componentes bonitos que quebram no leitor de tela

A maioria dos design systems internos nasce de um diretório components/ que cresce sem contrato. Um Button aceita 14 props, um Modal não trapa foco, um Select customizado ignora navegação por teclado. O time corrige acessibilidade caso a caso, e cada correção introduz lógica imperativa frágil.

Radix UI resolve a parte difícil: comportamento, acessibilidade (WAI-ARIA) e gerenciamento de foco. Tailwind CSS resolve a parte visual sem CSS-in-JS em runtime. O trabalho que sobra é criar uma camada de variantes tipada que conecte os dois e exponha uma API previsível para quem consome.

Este post monta essa camada do zero, com código funcional em React e TypeScript.

Anatomia da stack: quem faz o quê

ResponsabilidadeRadix UITailwind CSSSeu código
Semântica HTML e ARIASimNãoNão
Gerenciamento de foco (trap, restore)SimNãoNão
Animações de entrada/saídaParcial (data attributes)Sim (classes utilitárias)Conecta os dois
Tokens visuais (cor, espaçamento, tipografia)NãoSim (theme config)Define os tokens
API de variantes tipadaNãoNãoSim (com cva ou equivalente)
Composição de componentesSim (compound components)Não se aplicaEncapsula o padrão

A divisão é clara: Radix não opina sobre visual, Tailwind não opina sobre comportamento. O seu design system é a cola tipada entre os dois.

Configuração inicial do projeto

# Cria o projeto com Vite e React + TypeScript
npm create vite@latest ds-demo -- --template react-ts
cd ds-demo

# Radix UI: instala apenas os primitivos que você vai usar
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-slot

# Tailwind CSS v4 (se estiver no v3, o fluxo de config muda)
npm install -D tailwindcss @tailwindcss/vite

# CVA para variantes tipadas + tailwind-merge para resolver conflitos de classe
npm install class-variance-authority tailwind-merge clsx
// src/lib/cn.ts
// Utilitário que combina clsx (condicionais) com twMerge (resolve conflitos de especificidade do Tailwind)
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  // twMerge garante que "px-4 px-6" resulte em "px-6", não em ambas as classes
  return twMerge(clsx(inputs));
}

Esse cn é a função mais usada do design system inteiro. Sem tailwind-merge, quem consome o componente e passa className extra não consegue sobrescrever padding ou cor de forma previsível.

Tokens de design no Tailwind

Antes de criar componentes, defina os tokens. No Tailwind v4, a configuração migrou para CSS. Se você usa v3, o equivalente fica em tailwind.config.ts.

/* src/app.css — Tailwind v4 com tokens customizados */
@import "tailwindcss";

@theme {
  /* Cores semânticas: nomeie pela função, não pelo valor visual */
  --color-surface: #ffffff;
  --color-surface-raised: #f8f9fa;
  --color-border-default: #e2e4e9;
  --color-border-strong: #c1c4cc;

  --color-primary: #2563eb;
  --color-primary-hover: #1d4ed8;
  --color-primary-foreground: #ffffff;

  --color-destructive: #dc2626;
  --color-destructive-hover: #b91c1c;
  --color-destructive-foreground: #ffffff;

  --color-muted: #6b7280;
  --color-muted-foreground: #374151;

  /* Espaçamentos extras além dos defaults do Tailwind */
  --spacing-4_5: 1.125rem;

  /* Raios */
  --radius-component: 0.5rem;
  --radius-pill: 9999px;
}

Nomear tokens por função (primary, destructive, surface) e não por cor (blue-600, red-600) permite trocar o tema inteiro sem tocar em componentes.

Componente Button: variantes tipadas com CVA

// src/components/ui/button.tsx
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { forwardRef, type ButtonHTMLAttributes } from "react";
import { cn } from "@/lib/cn";

// CVA de

---

Leia o artigo completo em [https://www.vivodecodigo.com.br/react/design-system-radix-ui-tailwind-css](https://www.vivodecodigo.com.br/react/design-system-radix-ui-tailwind-css)
Carregando publicação patrocinada...