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.

Ohne 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"
Mit 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"

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:

Verwendung von 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'
);

Ein gängiges Muster ist es, eine Standardmenge an Klassen mit eingehenden Props sowie beliebiger eigener Logik zu verschmelzen:

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}
    />
  );
};

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:

lib/utils.ts
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:

@/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",
    },
  }
)

Bewährte Verfahren

1. Reihenfolge ist wichtig

Wenden Sie Klassen immer in dieser Reihenfolge an:

  1. Basisstile (immer angewendet)
  2. Variantenstile (basierend auf Props)
  3. Bedingte Stile (basierend auf State)
  4. 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:

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

Migrationsleitfaden

Wenn Sie von einem anderen Styling-Ansatz migrieren, hier einige Hinweise zur Anpassung gängiger Muster:

Von CSS Modules

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

<button className={`${styles.button} ${styles[variant]} ${className}`} />
Nachher - cn + Tailwind
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

Vorher - styled-components
const Button = styled.button<{ $primary?: boolean }>`
  padding: 8px 16px;
  background: ${props => props.$primary ? 'blue' : 'gray'};
`;
Nachher - cn + Tailwind
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:

  1. Definieren Sie Varianten außerhalb von Komponenten - CVA-Varianten sollten außerhalb der Komponente definiert werden, um eine Neuerstellung bei jedem Rendern zu vermeiden.

  2. 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]
);
  1. Verwenden Sie CSS-Variablen für dynamische Werte - Statt Klassen dynamisch zu generieren, verwenden Sie CSS-Variablen:
CSS-Variablen bevorzugen
// 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.