Stijlen
Voorwaardelijke en samenstelbare styling met Tailwind-klassen.
Moderne componentbibliotheken hebben flexibele stylingsystemen nodig die complexe eisen aankunnen zonder de developer experience te schaden. De combinatie van Tailwind CSS met intelligente klasse-samenvoeging is uitgegroeid tot een krachtig patroon voor het bouwen van aanpasbare componenten.
Deze aanpak lost de fundamentele spanning op tussen het bieden van verstandige standaardwaarden en het toestaan van volledige aanpassing — een uitdaging die componentbibliotheken al jaren teistert.
Het probleem met traditionele styling
Traditionele CSS-benaderingen leiden vaak tot specificity-oorlogen, stijlconflicten en onvoorspelbare overrides. Wanneer je className="bg-blue-500" aan een component doorgeeft die al bg-red-500 heeft, welke wint dan?
Zonder juiste afhandeling gelden beide klassen en is het resultaat afhankelijk van veel factoren - de bronvolgorde van CSS, de specificiteit van de klassen, het class-merging-algoritme van de bundler, enz.
Klassen intelligent samenvoegen
De tailwind-merge-bibliotheek lost dit op door Tailwind's klasse-structuur te begrijpen en conflicten intelligent op te lossen. Wanneer twee klassen op dezelfde CSS-eigenschap mikken, houdt het alleen de laatste aan.
// 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"Dit werkt voor alle Tailwind-utilities:
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"De bibliotheek begrijpt ook Tailwind's modifier-systeem:
// 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"Voorwaardelijke klassen
Vaak moet je klassen voorwaardelijk toepassen op basis van props of state. De clsx-bibliotheek biedt een nette API hiervoor:
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'
);Een veelgebruikt patroon is het samenvoegen van een standaardset klassen met binnenkomende props, evenals eventuele eigen logica:
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}
/>
);
};De cn-hulpfunctie
De cn-functie, populair gemaakt door shadcn/ui, combineert clsx en tailwind-merge zodat je zowel voorwaardelijke logica als intelligente samenvoeging krijgt:
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}De kracht zit in de volgorde - basisstijlen eerst, conditionals daarna, gebruiker overrides als laatste. Dit zorgt voor voorspelbaar gedrag terwijl volledige aanpassing behouden blijft.
Class Variance Authority (CVA)
Voor complexe componenten met veel varianten wordt het handmatig beheren van voorwaardelijke klassen onhoudbaar. Class Variance Authority (CVA) biedt een declaratieve API voor het definiëren van componentvarianten.
Bijvoorbeeld, hier is een extract van de Knop-component van 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",
},
}
)Beste praktijken
1. Volgorde is belangrijk
Pas altijd klassen in deze volgorde toe:
- Basisstijlen (altijd toegepast)
- Variantiestijlen (gebaseerd op props)
- Voorwaardelijke stijlen (gebaseerd op state)
- Gebruikersoverschrijvingen (className-prop)
className={cn(
'base-styles', // 1. Basis
variant && variantStyles, // 2. Varianten
isActive && 'active', // 3. Conditionals
className // 4. Gebruikersoverschrijvingen
)}2. Documenteer je varianten
Gebruik TypeScript en JSDoc om te documenteren wat elke variant doet:
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. Haal herhaalde patronen eruit
Als je merkt dat je steeds dezelfde voorwaardelijke logica schrijft, extraheer die:
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)}Migratiegids
Als je migreert van een andere stylingaanpak, zo kun je veelvoorkomende patronen aanpassen:
Van 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
)} />Van 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}
/>
);
}Prestatie-overwegingen
Zowel clsx als tailwind-merge zijn sterk geoptimaliseerd, maar houd deze tips in gedachten:
-
Define variants outside components - CVA-varianten moeten buiten de component worden gedefinieerd om recreatie bij elke render te voorkomen.
-
Memoize complex computations - Als je dure voorwaardelijke logica hebt, overweeg memoization:
const className = useMemo(
() => cn(
baseStyles,
expensiveComputation(props),
className
),
[props, className]
);- Use CSS variables for dynamic values - In plaats van dynamisch classes te genereren, gebruik CSS-variabelen:
// Good
<div
className="bg-[var(--color)]"
style={{ '--color': dynamicColor } as React.CSSProperties}
/>
// Avoid
<div className={`bg-[${dynamicColor}]`} />De combinatie van Tailwind CSS, intelligente klasse-samenvoeging en variant-API's biedt een robuuste basis voor componentstyling. Deze aanpak schaalt van eenvoudige knoppen tot complexe designsystemen terwijl voorspelbaarheid en developer experience behouden blijven.