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.
// Both bg-red-500 and bg-blue-500 apply - unpredictable result
<Button className="bg-blue-500" />
// Renders: className="bg-red-500 bg-blue-500"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:
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:
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:
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:
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:
- Estilos base (sempre aplicados)
- Estilos de variante (baseados em props)
- Estilos condicionais (baseados em estado)
- 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:
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
import styles from './Button.module.css';
<button className={`${styles.button} ${styles[variant]} ${className}`} />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
const Button = styled.button<{ $primary?: boolean }>`
padding: 8px 16px;
background: ${props => props.$primary ? 'blue' : 'gray'};
`;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:
-
Defina variantes fora dos componentes - as variantes do CVA devem ser definidas fora do componente para evitar recriação em cada renderização.
-
Memoize computações complexas - se você tiver lógica condicional custosa, considere memoizar:
const className = useMemo(
() => cn(
baseStyles,
expensiveComputation(props),
className
),
[props, className]
);- Use variáveis CSS para valores dinâmicos - em vez de gerar classes dinamicamente, use variáveis CSS:
// 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.