Voltar para o blog
UI/UX Design

Design System Moderno com Tailwind CSS v4

Construa design systems escaláveis e consistentes usando a nova versão do Tailwind

Foto de Vinicius Mendes
Especialista em IA Aplicada | Full Stack Engineer | UX/UI Designer
18 min de leitura
Imagem de capa do artigo: Design System Moderno com Tailwind CSS v4

Design System Moderno com Tailwind CSS v4

Construir um Design System consistente e escalável é fundamental para qualquer projeto sério de frontend. Com o lançamento do Tailwind CSS v4, temos novas ferramentas poderosas para criar sistemas de design que são ao mesmo tempo flexíveis e fáceis de manter. Neste guia completo, vamos explorar como construir um Design System profissional do zero.

Design System - Componentes e tokens de design

O que mudou no Tailwind CSS v4?

O Tailwind v4 representa uma reescrita completa do framework, trazendo mudanças significativas:

Principais Novidades

Feature Tailwind v3 Tailwind v4
Engine PostCSS Oxide (Rust)
Config tailwind.config.js CSS @theme
Performance Rápido 10x mais rápido
CSS Variables Manual Automático
Container Queries Plugin Nativo

"Tailwind v4 não é apenas mais rápido, é uma nova forma de pensar sobre configuração de estilos." - Adam Wathan, criador do Tailwind

Configuração com @theme

A grande mudança do v4 é a configuração via CSS usando a diretiva @theme:

Estrutura Básica

css
/* globals.css */
@import "tailwindcss";

@theme {
  /* Colors - usando formato RGB sem vírgulas */
  --color-brand-50: 240 253 244;
  --color-brand-100: 220 252 231;
  --color-brand-500: 34 197 94;
  --color-brand-600: 22 163 74;
  --color-brand-900: 20 83 45;

  /* Secondary palette */
  --color-accent-400: 250 204 21;
  --color-accent-500: 234 179 8;

  /* Neutral scale */
  --color-neutral-50: 250 250 250;
  --color-neutral-900: 23 23 23;

  /* Spacing scale */
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
  --spacing-2xl: 3rem;

  /* Typography */
  --font-sans: "Inter", system-ui, sans-serif;
  --font-mono: "JetBrains Mono", monospace;

  /* Border radius */
  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;
  --radius-xl: 1rem;
  --radius-full: 9999px;

  /* Shadows */
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}

Tokens Semânticos

Além dos tokens de base, crie tokens semânticos para contextos específicos:

css
@theme {
  /* Tokens semânticos para temas */
  --color-background: var(--color-neutral-50);
  --color-foreground: var(--color-neutral-900);
  --color-primary: var(--color-brand-500);
  --color-primary-hover: var(--color-brand-600);
  --color-muted: var(--color-neutral-100);
  --color-border: var(--color-neutral-200);

  /* Estados */
  --color-success: 34 197 94;
  --color-warning: 234 179 8;
  --color-error: 239 68 68;
  --color-info: 59 130 246;
}

/* Dark mode */
@media (prefers-color-scheme: dark) {
  @theme {
    --color-background: var(--color-neutral-900);
    --color-foreground: var(--color-neutral-50);
    --color-muted: var(--color-neutral-800);
    --color-border: var(--color-neutral-700);
  }
}

Usando os Tokens

html
<!-- Os tokens viram classes automaticamente -->
<div class="bg-background text-foreground">
  <button class="bg-primary hover:bg-primary-hover text-white rounded-lg px-md py-sm">
    Botão Primary
  </button>
</div>

Componentes Base

Com os tokens definidos, vamos criar componentes reutilizáveis:

Button Component

typescript
// components/ui/button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const buttonVariants = cva(
  // Base styles
  'inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default: 'bg-primary text-white hover:bg-primary-hover shadow-sm',
        secondary: 'bg-muted text-foreground hover:bg-muted/80',
        outline: 'border border-border bg-transparent hover:bg-muted',
        ghost: 'hover:bg-muted',
        destructive: 'bg-error text-white hover:bg-error/90',
      },
      size: {
        sm: 'h-8 px-3 text-sm rounded-md',
        md: 'h-10 px-4 text-sm rounded-lg',
        lg: 'h-12 px-6 text-base rounded-lg',
        icon: 'h-10 w-10 rounded-lg',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'md',
    },
  }
);

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  isLoading?: boolean;
}

export function Button({
  className,
  variant,
  size,
  isLoading,
  children,
  ...props
}: ButtonProps) {
  return (
    <button
      className={cn(buttonVariants({ variant, size }), className)}
      disabled={isLoading}
      {...props}
    >
      {isLoading ? (
        <svg className="animate-spin h-4 w-4 mr-2" viewBox="0 0 24 24">
          <circle
            className="opacity-25"
            cx="12"
            cy="12"
            r="10"
            stroke="currentColor"
            strokeWidth="4"
            fill="none"
          />
          <path
            className="opacity-75"
            fill="currentColor"
            d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
          />
        </svg>
      ) : null}
      {children}
    </button>
  );
}

Card Component

typescript
// components/ui/card.tsx
import { cn } from '@/lib/utils';

interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
  variant?: 'default' | 'bordered' | 'elevated';
}

export function Card({
  className,
  variant = 'default',
  children,
  ...props
}: CardProps) {
  return (
    <div
      className={cn(
        'rounded-xl bg-background p-6',
        {
          'border border-border': variant === 'default' || variant === 'bordered',
          'shadow-lg': variant === 'elevated',
        },
        className
      )}
      {...props}
    >
      {children}
    </div>
  );
}

export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return <div className={cn('mb-4', className)} {...props} />;
}

export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
  return <h3 className={cn('text-xl font-semibold text-foreground', className)} {...props} />;
}

export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return <div className={cn('text-muted-foreground', className)} {...props} />;
}

Container Queries (Novo!)

O Tailwind v4 traz suporte nativo a Container Queries:

html
<!-- Definir container -->
<div class="@container">
  <!-- Estilos baseados no tamanho do container, não da viewport -->
  <div class="@sm:flex @md:grid @md:grid-cols-2 @lg:grid-cols-3">
    <div class="@sm:w-1/2 @md:w-full">
      Card responsivo ao container
    </div>
  </div>
</div>

Exemplo Prático

typescript
// components/responsive-card.tsx
export function ResponsiveCard({ title, description, image }: Props) {
  return (
    <article className="@container">
      <div className="flex flex-col @md:flex-row gap-4 p-4 rounded-xl border border-border">
        {/* Imagem adapta ao container */}
        <div className="@md:w-1/3">
          <img
            src={image}
            alt={title}
            className="w-full aspect-video @md:aspect-square object-cover rounded-lg"
          />
        </div>

        {/* Conteúdo */}
        <div className="@md:w-2/3 flex flex-col justify-center">
          <h3 className="text-lg @lg:text-xl font-semibold">{title}</h3>
          <p className="text-muted-foreground @lg:text-lg mt-2">{description}</p>
        </div>
      </div>
    </article>
  );
}

Integrando com Figma

Exportando Tokens do Figma

json
{
  "colors": {
    "brand": {
      "50": { "value": "#f0fdf4", "type": "color" },
      "500": { "value": "#22c55e", "type": "color" },
      "900": { "value": "#14532d", "type": "color" }
    }
  },
  "spacing": {
    "xs": { "value": "4px", "type": "spacing" },
    "sm": { "value": "8px", "type": "spacing" },
    "md": { "value": "16px", "type": "spacing" }
  }
}

Script de Conversão

javascript
// scripts/tokens-to-css.js
const tokens = require('./figma-tokens.json');

function hexToRgb(hex) {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result
    ? `${parseInt(result[1], 16)} ${parseInt(result[2], 16)} ${parseInt(result[3], 16)}`
    : null;
}

let css = '@theme {\n';

// Colors
Object.entries(tokens.colors).forEach(([name, shades]) => {
  Object.entries(shades).forEach(([shade, { value }]) => {
    css += `  --color-${name}-${shade}: ${hexToRgb(value)};\n`;
  });
});

// Spacing
Object.entries(tokens.spacing).forEach(([name, { value }]) => {
  css += `  --spacing-${name}: ${value};\n`;
});

css += '}';

console.log(css);

Dark Mode

Implementação com CSS

css
/* Suporte a preferência do sistema e classe manual */
@theme {
  --color-background: 255 255 255;
  --color-foreground: 23 23 23;
}

.dark {
  --color-background: 23 23 23;
  --color-foreground: 250 250 250;
}

@media (prefers-color-scheme: dark) {
  :root:not(.light) {
    --color-background: 23 23 23;
    --color-foreground: 250 250 250;
  }
}

Theme Toggle Component

typescript
// components/theme-toggle.tsx
'use client';

import { useTheme } from 'next-themes';
import { Button } from './ui/button';
import { FiSun, FiMoon } from 'react-icons/fi';

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      aria-label="Alternar tema"
    >
      <FiSun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <FiMoon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
    </Button>
  );
}

Documentação do Design System

Storybook Setup

typescript
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './button';

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: 'select',
      options: ['default', 'secondary', 'outline', 'ghost', 'destructive'],
    },
    size: {
      control: 'select',
      options: ['sm', 'md', 'lg', 'icon'],
    },
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

export const Default: Story = {
  args: {
    children: 'Button',
    variant: 'default',
    size: 'md',
  },
};

export const AllVariants: Story = {
  render: () => (
    <div className="flex gap-4 flex-wrap">
      <Button variant="default">Default</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="destructive">Destructive</Button>
    </div>
  ),
};

Checklist de Design System

Fundações

  • Definir paleta de cores (brand, neutral, semantic)
  • Definir escala de espaçamento
  • Definir tipografia (fonts, sizes, weights)
  • Definir border radius e shadows
  • Definir breakpoints e container queries

Componentes

  • Button (variantes e tamanhos)
  • Card (header, content, footer)
  • Input e Form fields
  • Modal/Dialog
  • Navigation components
  • Feedback components (Toast, Alert)

Infraestrutura

  • Setup Storybook para documentação
  • Testes de acessibilidade
  • Testes visuais (Chromatic)
  • CI/CD para publicação

Conclusão

O Tailwind CSS v4 traz uma nova era para construção de Design Systems. A configuração via CSS com @theme simplifica a integração com ferramentas de design e torna o sistema mais fácil de manter.

Recursos Adicionais

Documentação:

Ferramentas:

Este guia será atualizado conforme novas features do Tailwind v4 forem lançadas.

Tags

  • Tailwind CSS
  • Design System
  • CSS
  • UI/UX
  • Frontend
  • React
  • Tokens
  • Dark Mode

Continue explorando outros conteúdos que podem te interessar