DevToolBoxGRÁTIS
Blog

Padrões de Componentes Tailwind CSS: Construindo UI Reutilizável em 2026

14 minby DevToolBox

A abordagem utility-first do Tailwind CSS pode levar a JSX inchado se não for organizado corretamente.

class-variance-authority (cva): variantes com tipos

cva é a forma padrão de gerenciar variantes de componentes no Tailwind.

// Using class-variance-authority (cva) for type-safe variants
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const buttonVariants = cva(
    // Base classes applied to every variant
    'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
    {
        variants: {
            variant: {
                default: 'bg-primary text-primary-foreground hover:bg-primary/90',
                destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
                outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
                ghost: 'hover:bg-accent hover:text-accent-foreground',
                link: 'text-primary underline-offset-4 hover:underline',
            },
            size: {
                default: 'h-10 px-4 py-2',
                sm: 'h-9 rounded-md px-3',
                lg: 'h-11 rounded-md px-8',
                icon: 'h-10 w-10',
            },
        },
        defaultVariants: {
            variant: 'default',
            size: 'default',
        },
    }
);

interface ButtonProps
    extends React.ButtonHTMLAttributes<HTMLButtonElement>,
        VariantProps<typeof buttonVariants> {}

export function Button({ className, variant, size, ...props }: ButtonProps) {
    return (
        <button
            className={cn(buttonVariants({ variant, size, className }))}
            {...props}
        />
    );
}

// Usage:
// <Button variant="outline" size="sm">Click me</Button>
// <Button variant="destructive">Delete</Button>

Quando usar @apply

@apply extrai combinações de classes Tailwind repetidas em classes CSS.

/* globals.css — using @apply to extract reusable patterns */
@layer components {
    /* Card component pattern */
    .card {
        @apply rounded-lg border bg-card text-card-foreground shadow-sm;
    }

    .card-header {
        @apply flex flex-col space-y-1.5 p-6;
    }

    .card-title {
        @apply text-2xl font-semibold leading-none tracking-tight;
    }

    .card-content {
        @apply p-6 pt-0;
    }

    /* Form field pattern */
    .form-label {
        @apply text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70;
    }

    .form-input {
        @apply flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm
               ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium
               placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2
               focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
    }

    /* Badge pattern */
    .badge {
        @apply inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold
               transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2;
    }
}

/* Use sparingly — prefer utility classes in JSX for maintainability */

O utilitário cn(): mesclar classes com segurança

clsx lida com classes condicionais, tailwind-merge resolve conflitos do Tailwind.

// lib/utils.ts — the cn() helper (used everywhere in shadcn/ui)
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

// clsx: conditionally join class names
// twMerge: intelligently merge Tailwind classes (handles conflicts)
export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs));
}

// Examples:
cn('px-4 py-2', 'px-6')           // 'py-2 px-6' (px-4 overridden)
cn('text-red-500', isActive && 'text-blue-500')  // conditional
cn({ 'opacity-50': disabled, 'cursor-not-allowed': disabled })

Headless UI: componentes interativos acessíveis

Headless UI fornece componentes interativos acessíveis sem estilo.

// Using Headless UI (Tailwind Labs) for accessible components
import { Listbox, Transition } from '@headlessui/react';
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid';
import { Fragment, useState } from 'react';

const people = [
    { id: 1, name: 'Durward Reynolds' },
    { id: 2, name: 'Kenton Towne' },
    { id: 3, name: 'Therese Wunsch' },
];

export function Select() {
    const [selected, setSelected] = useState(people[0]);

    return (
        <Listbox value={selected} onChange={setSelected}>
            <Listbox.Button className="relative w-full cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm">
                <span className="block truncate">{selected.name}</span>
                <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
                    <ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
                </span>
            </Listbox.Button>
            <Transition as={Fragment} leave="transition ease-in duration-100" leaveFrom="opacity-100" leaveTo="opacity-0">
                <Listbox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
                    {people.map((person) => (
                        <Listbox.Option key={person.id} value={person} className={({ active }) => cn('relative cursor-default select-none py-2 pl-10 pr-4', active ? 'bg-amber-100 text-amber-900' : 'text-gray-900')}>
                            {({ selected }) => (
                                <>
                                    <span className={cn('block truncate', selected ? 'font-medium' : 'font-normal')}>{person.name}</span>
                                    {selected ? <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600"><CheckIcon className="h-5 w-5" /></span> : null}
                                </>
                            )}
                        </Listbox.Option>
                    ))}
                </Listbox.Options>
            </Transition>
        </Listbox>
    );
}

Configuração Tailwind: design tokens e temas

Estender Tailwind com variáveis CSS permite troca de temas sem JavaScript.

// tailwind.config.ts — extend with design tokens
import type { Config } from 'tailwindcss';

const config: Config = {
    darkMode: ['class'],
    content: ['./src/**/*.{ts,tsx}'],
    theme: {
        extend: {
            colors: {
                // CSS variable-based colors for theming
                border: 'hsl(var(--border))',
                input: 'hsl(var(--input))',
                ring: 'hsl(var(--ring))',
                background: 'hsl(var(--background))',
                foreground: 'hsl(var(--foreground))',
                primary: {
                    DEFAULT: 'hsl(var(--primary))',
                    foreground: 'hsl(var(--primary-foreground))',
                },
                destructive: {
                    DEFAULT: 'hsl(var(--destructive))',
                    foreground: 'hsl(var(--destructive-foreground))',
                },
            },
            borderRadius: {
                lg: 'var(--radius)',
                md: 'calc(var(--radius) - 2px)',
                sm: 'calc(var(--radius) - 4px)',
            },
            keyframes: {
                'accordion-down': {
                    from: { height: '0' },
                    to: { height: 'var(--radix-accordion-content-height)' },
                },
            },
            animation: {
                'accordion-down': 'accordion-down 0.2s ease-out',
            },
        },
    },
    plugins: [require('tailwindcss-animate')],
};

export default config;

Abordagens de bibliotecas de componentes

LibraryApproachCode OwnershipThemingAccessibility
shadcn/uiCopy-paste + Radix + CVAYou own the codeCSS variablesRadix primitives
Chakra UIComponent librarynpm dependencyTheme objectBuilt-in
MUI (Material)Component librarynpm dependencyTheme providerBuilt-in
MantineComponent librarynpm dependencyCSS variablesBuilt-in
DaisyUITailwind pluginConfig onlyCSS variablesManual

Boas práticas

  • Usar cva para qualquer componente com 2+ variantes.
  • Sempre usar cn() para mesclar classes.
  • Evitar @apply para componentes.
  • Design tokens via variáveis CSS para dark mode.
  • Para dropdowns, modais — usar Headless UI ou Radix UI.

Perguntas frequentes

Tailwind ou CSS-in-JS?

Em 2026, Tailwind + CVA + shadcn/ui é o padrão dominante para novos projetos React.

O que é shadcn/ui?

shadcn/ui é uma coleção de componentes copy-paste construídos com Radix UI + Tailwind + CVA.

Como lidar com dark mode no Tailwind?

Adicionar darkMode: "class" no tailwind.config.ts e usar o prefixo dark:.

Como prevenir a purga de classes Tailwind em produção?

Configurar o array content no tailwind.config.ts para incluir todos os arquivos.

O Tailwind é bom para sistemas de design?

Sim, mas requer disciplina. Usar design tokens (variáveis CSS).

Ferramentas relacionadas

𝕏 Twitterin LinkedIn
Isso foi útil?

Fique atualizado

Receba dicas de dev e novos ferramentas semanalmente.

Sem spam. Cancele a qualquer momento.

Try These Related Tools

🌈CSS Gradient GeneratorCSS Triangle Generator🎨Color Picker Online

Related Articles

CSS Grid Mastery: Guia Completo com Exemplos Reais 2026

Dominar CSS Grid Layout 2026: grid-template areas, auto-placement, subgrid e layouts responsivos.

Padrões React Query 2026: Data Fetching, Cache e Mutations com TanStack Query

Dominar padrões React Query (TanStack Query) 2026: useQuery, useMutation, atualizações otimistas.