Estilização

Estilização condicional e composável com classes Tailwind.

Bibliotecas de componentes modernas precisam de sistemas de estilização flexíveis que consigam lidar com requisitos complexos sem sacrificar a experiência do desenvolvedor. A combinação do Tailwind CSS com mesclagem inteligente de classes emergiu como um padrão poderoso para construir componentes personalizáveis.

Essa abordagem resolve a tensão fundamental entre fornecer padrões sensatos e permitir a personalização completa — um desafio que atormenta bibliotecas de componentes há anos.

O problema com a estilização tradicional

Abordagens tradicionais de CSS frequentemente levam a guerras de especificidade, conflitos de estilo e sobrescritas imprevisíveis. Quando você passa className="bg-blue-500" para um componente que já tem bg-red-500, qual deles vence?

Sem um tratamento adequado, ambas as classes se aplicam e o resultado depende de muitos fatores — ordem de origem do CSS, especificidade das classes, o algoritmo de mesclagem de classes do bundler, etc.

Mesclando classes inteligentemente

A biblioteca tailwind-merge resolve isso entendendo a estrutura de classes do Tailwind e resolvendo conflitos de forma inteligente. Quando duas classes miram a mesma propriedade CSS, ela mantém apenas a última.

Sem tailwind-merge
// Both bg-red-500 and bg-blue-500 apply - unpredictable result
<Button className="bg-blue-500" />
// Renders: className="bg-red-500 bg-blue-500"
Com tailwind-merge
import { twMerge } from 'tailwind-merge';

// bg-blue-500 wins as it comes last
const className = twMerge('bg-red-500', 'bg-blue-500');
// Returns: "bg-blue-500"

Isto funciona para todas as utilitárias do Tailwind:

twMerge('px-4 py-2', 'px-8'); // Returns: "py-2 px-8"
twMerge('text-sm', 'text-lg'); // Returns: "text-lg"
twMerge('hover:bg-red-500', 'hover:bg-blue-500'); // Returns: "hover:bg-blue-500"

A biblioteca também compreende o sistema de modificadores do Tailwind:

// Modifiers are handled correctly
twMerge('hover:bg-red-500 focus:bg-red-500', 'hover:bg-blue-500');
// Returns: "focus:bg-red-500 hover:bg-blue-500"

Classes condicionais

Frequentemente você precisa aplicar classes condicionalmente com base em props ou estado. A biblioteca clsx fornece uma API limpa para isso:

Usando clsx
import clsx from 'clsx';

// Basic conditionals
clsx('base', isActive && 'active');
// Returns: "base active" (if isActive is true)

// Object syntax
clsx('base', {
  'active': isActive,
  'disabled': isDisabled,
});

// Arrays
clsx(['base', isLarge ? 'text-lg' : 'text-sm']);

// Mixed
clsx(
  'base',
  ['array-item'],
  { 'object-conditional': true },
  isActive && 'conditional'
);

Um padrão comum é mesclar um conjunto padrão de classes com as props recebidas, assim como qualquer lógica personalizada que tenhamos:

component.tsx
const Component = ({ className, ...props }: ComponentProps) => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div
      className={cn(
        "rounded-lg border bg-white shadow-sm",
        isOpen && "bg-blue-500",
        className
      )}
      {...props}
    />
  );
};

A função utilitária cn

A função cn, popularizada por shadcn/ui, combina clsx e tailwind-merge para fornecer tanto lógica condicional quanto mesclagem inteligente:

lib/utils.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

O poder vem da ordem — estilos base primeiro, condicionais em segundo, sobrescritas do usuário por último. Isso garante comportamento previsível enquanto mantém total personalização.

Class Variance Authority (CVA)

Para componentes complexos com muitas variantes, gerenciar manualmente classes condicionais se torna impraticável. Class Variance Authority (CVA) fornece uma API declarativa para definir variantes de componente.

Por exemplo, aqui está um trecho do componente Button do shadcn/ui:

@/components/ui/button.tsx
const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive:
          "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
        outline:
          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost:
          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2 has-[>svg]:px-3",
        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
        icon: "size-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

Melhores práticas

1. A ordem importa

Sempre aplique classes nesta ordem:

  1. Estilos base (sempre aplicados)
  2. Estilos de variante (baseados em props)
  3. Estilos condicionais (baseados em estado)
  4. Sobrescritas do usuário (prop className)
className={cn(
  'base-styles',            // 1. Base
  variant && variantStyles, // 2. Variants
  isActive && 'active',     // 3. Conditionals
  className                 // 4. User overrides
)}

2. Documente suas variantes

Use TypeScript e JSDoc para documentar o que cada variante faz:

type ButtonProps = {
  /**
   * The visual style of the button
   * @default "primary"
   */
  variant?: 'primary' | 'secondary' | 'destructive' | 'ghost';

  /**
   * The size of the button
   * @default "md"
   */
  size?: 'sm' | 'md' | 'lg';
};

3. Extraia padrões repetidos

Se você perceber que está escrevendo a mesma lógica condicional repetidamente, extraia-a:

utils/styles.ts
export const focusRing = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
export const disabled = 'disabled:pointer-events-none disabled:opacity-50';

// Use in components
className={cn(focusRing, disabled, className)}

Guia de migração

Se você está migrando de uma abordagem de estilização diferente, aqui está como adaptar padrões comuns:

Do CSS Modules

Before - CSS Modules
import styles from './Button.module.css';

<button className={`${styles.button} ${styles[variant]} ${className}`} />
Depois - cn + Tailwind
import { cn } from '@/lib/utils';

<button className={cn(
  'px-4 py-2 rounded-lg',
  variant === 'primary' && 'bg-blue-500 text-white',
  className
)} />

Do styled-components

Before - styled-components
const Button = styled.button<{ $primary?: boolean }>`
  padding: 8px 16px;
  background: ${props => props.$primary ? 'blue' : 'gray'};
`;
Depois - cn + Tailwind
function Button({ primary, className, ...props }) {
  return (
    <button
      className={cn(
        'px-4 py-2',
        primary ? 'bg-blue-500' : 'bg-gray-500',
        className
      )}
      {...props}
    />
  );
}

Considerações de desempenho

Tanto clsx quanto tailwind-merge são altamente otimizados, mas tenha estas dicas em mente:

  1. Defina variantes fora dos componentes - as variantes do CVA devem ser definidas fora do componente para evitar recriação em cada renderização.

  2. Memoize computações complexas - se você tiver lógica condicional custosa, considere memoizar:

const className = useMemo(
  () => cn(
    baseStyles,
    expensiveComputation(props),
    className
  ),
  [props, className]
);
  1. Use variáveis CSS para valores dinâmicos - em vez de gerar classes dinamicamente, use variáveis CSS:
Prefer CSS variables
// Good
<div
  className="bg-[var(--color)]"
  style={{ '--color': dynamicColor } as React.CSSProperties}
/>

// Avoid
<div className={`bg-[${dynamicColor}]`} />

A combinação de Tailwind CSS, mesclagem inteligente de classes e APIs de variantes fornece uma base robusta para estilização de componentes. Essa abordagem escala desde botões simples até sistemas de design complexos, mantendo previsibilidade e uma boa experiência do desenvolvedor.