mirror of
https://github.com/vercel/components.build.git
synced 2026-09-14 20:06:39 +08:00
55fae375a4
* Update Geistdocs * Update geistdocs.tsx * Run translation script * Delete toc.tsx * Update route.ts * Update route.ts
280 lines
9.2 KiB
Plaintext
280 lines
9.2 KiB
Plaintext
---
|
||
title: Styling
|
||
description: 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.
|
||
|
||
```tsx title="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"
|
||
```
|
||
|
||
```tsx title="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:
|
||
|
||
```tsx
|
||
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:
|
||
|
||
```tsx
|
||
// 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:
|
||
|
||
```tsx title="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:
|
||
|
||
```tsx title="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](https://ui.shadcn.com/), kombiniert `clsx` und `tailwind-merge`, um Ihnen sowohl bedingte Logik als auch intelligentes Zusammenführen zu bieten:
|
||
|
||
```tsx title="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)](https://cva.style/docs) bietet eine deklarative API zur Definition von Komponentenvarianten.
|
||
|
||
Zum Beispiel ein Auszug aus der [Button](https://ui.shadcn.com/docs/components/button)-Komponente von shadcn/ui:
|
||
|
||
```tsx title="@/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)
|
||
|
||
```tsx
|
||
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:
|
||
|
||
```tsx
|
||
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:
|
||
|
||
```tsx title="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
|
||
|
||
```tsx title="Vorher - CSS Modules"
|
||
import styles from './Button.module.css';
|
||
|
||
<button className={`${styles.button} ${styles[variant]} ${className}`} />
|
||
```
|
||
|
||
```tsx title="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
|
||
|
||
```tsx title="Vorher - styled-components"
|
||
const Button = styled.button<{ $primary?: boolean }>`
|
||
padding: 8px 16px;
|
||
background: ${props => props.$primary ? 'blue' : 'gray'};
|
||
`;
|
||
```
|
||
|
||
```tsx title="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:
|
||
|
||
```tsx
|
||
const className = useMemo(
|
||
() => cn(
|
||
baseStyles,
|
||
expensiveComputation(props),
|
||
className
|
||
),
|
||
[props, className]
|
||
);
|
||
```
|
||
|
||
3. **Verwenden Sie CSS-Variablen für dynamische Werte** - Statt Klassen dynamisch zu generieren, verwenden Sie CSS-Variablen:
|
||
|
||
```tsx title="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. |