Styling
Bedingtes und komponierbares Styling mit Tailwind-Klassen.
Moderne Komponentenbibliotheken benötigen flexible Styling-Systeme, die komplexe Anforderungen bewältigen können, ohne die Entwicklererfahrung zu beeinträchtigen. Die Kombination von Tailwind CSS mit intelligenter Zusammenführung von Klassen hat sich als leistungsfähiges Muster zum Erstellen anpassbarer Komponenten etabliert.
Dieser Ansatz löst die grundlegende Spannung zwischen dem Bereitstellen sinnvoller Voreinstellungen und der Ermöglichung vollständiger Anpassung – eine Herausforderung, die Komponentenbibliotheken seit Jahren belastet.
Das Problem mit traditionellem Styling
Traditionelle CSS-Ansätze führen häufig zu Spezifitätskonflikten, Stilüberschreibungen und unvorhersehbaren Ergebnissen. Wenn Sie className="bg-blue-500" an eine Komponente übergeben, die bereits bg-red-500 enthält, welche Klasse gewinnt?
Ohne richtige Handhabung treffen beide Klassen zu und das Ergebnis hängt von vielen Faktoren ab – CSS-Quellreihenfolge, der Spezifität der Klassen, dem Klassenzusammenführungsalgorithmus des Bundlers usw.
Intelligentes Zusammenführen von Klassen
Die Bibliothek tailwind-merge löst dieses Problem, indem sie Tailwinds Klassenstruktur versteht und Konflikte intelligent auflöst. Wenn zwei Klassen dieselbe CSS-Eigenschaft ansprechen, behält sie nur die zuletzt auftretende bei.
// 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"Das funktioniert für 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"Die Bibliothek versteht auch Tailwinds Modifier-System:
// 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"Bedingte Klassen
Oft müssen Klassen bedingt auf Basis von Props oder State angewendet werden. Die Bibliothek clsx bietet dafür eine saubere API:
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'
);Ein gängiges Muster ist es, eine Standardmenge an Klassen mit eingehenden Props sowie beliebiger eigener Logik zu verschmelzen:
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}
/>
);
};Die cn Utility-Funktion
Die cn-Funktion, popularisiert durch shadcn/ui, kombiniert clsx und tailwind-merge, um Ihnen sowohl bedingte Logik als auch intelligentes Zusammenführen zu bieten:
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Die Stärke ergibt sich aus der Reihenfolge – Basisstile zuerst, Bedingungsklassen zweit, Benutzerüberschreibungen zuletzt. Das sorgt für vorhersehbares Verhalten bei gleichzeitiger vollständiger Anpassbarkeit.
Class Variance Authority (CVA)
Bei komplexen Komponenten mit vielen Varianten wird die manuelle Verwaltung bedingter Klassen schnell unübersichtlich. Class Variance Authority (CVA) bietet eine deklarative API zur Definition von Komponentenvarianten.
Zum Beispiel ein Auszug aus der Button-Komponente von 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",
},
}
)Bewährte Verfahren
1. Reihenfolge ist wichtig
Wenden Sie Klassen immer in dieser Reihenfolge an:
- Basisstile (immer angewendet)
- Variantenstile (basierend auf Props)
- Bedingte Stile (basierend auf State)
- Benutzerüberschreibungen (className-Prop)
className={cn(
'base-styles', // 1. Base
variant && variantStyles, // 2. Variants
isActive && 'active', // 3. Conditionals
className // 4. User overrides
)}2. Dokumentieren Sie Ihre Varianten
Verwenden Sie TypeScript und JSDoc, um zu dokumentieren, was jede Variante bewirkt:
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. Wiederholte Muster auslagern
Wenn Sie feststellen, dass Sie dieselbe bedingte Logik wiederholt schreiben, lagern Sie sie aus:
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)}Migrationsleitfaden
Wenn Sie von einem anderen Styling-Ansatz migrieren, hier einige Hinweise zur Anpassung gängiger Muster:
Von 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
)} />Von 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}
/>
);
}Leistungsaspekte
Sowohl clsx als auch tailwind-merge sind stark optimiert, aber beachten Sie diese Tipps:
-
Definieren Sie Varianten außerhalb von Komponenten - CVA-Varianten sollten außerhalb der Komponente definiert werden, um eine Neuerstellung bei jedem Rendern zu vermeiden.
-
Memoisieren Sie komplexe Berechnungen - Wenn Sie teure bedingte Logik haben, überlegen Sie, diese zu memoisieren:
const className = useMemo(
() => cn(
baseStyles,
expensiveComputation(props),
className
),
[props, className]
);- Verwenden Sie CSS-Variablen für dynamische Werte - Statt Klassen dynamisch zu generieren, verwenden Sie CSS-Variablen:
// Good
<div
className="bg-[var(--color)]"
style={{ '--color': dynamicColor } as React.CSSProperties}
/>
// Avoid
<div className={`bg-[${dynamicColor}]`} />Die Kombination aus Tailwind CSS, intelligenter Klassen-Zusammenführung und Varianten-APIs bietet eine robuste Grundlage für das Styling von Komponenten. Dieser Ansatz skaliert von einfachen Buttons bis hin zu komplexen Designsystemen, während er Vorhersagbarkeit und eine gute Entwicklererfahrung beibehält.