DevToolBoxFREE
BlogAdvertise

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).

Isso foi útil?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Partner Picks

Sponsor this article

Place your product next to this developer topic with tracked clicks.

Ask about article sponsorship

This site uses cookies for analytics and to display ads. By continuing to browse, you agree. Privacy Policy