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
8.8 KiB
Plaintext
280 lines
8.8 KiB
Plaintext
---
|
|
title: Styling
|
|
description: Betinget og komponerbar styling med Tailwind-klasser.
|
|
---
|
|
|
|
Moderne komponentbiblioteker har brug for fleksible styling-systemer, der kan håndtere komplekse krav uden at gå på kompromis med udvikleroplevelsen. Kombinationen af Tailwind CSS og intelligent sammensmeltning af klasser er dukket op som et kraftfuldt mønster til at bygge tilpasselige komponenter.
|
|
|
|
Denne tilgang løser den grundlæggende spænding mellem at tilbyde fornuftige standarder og samtidig tillade fuld tilpasning - en udfordring, der har besværet komponentbiblioteker i mange år.
|
|
|
|
## Problemet med traditionel styling
|
|
|
|
Traditionelle CSS-tilgange fører ofte til specificitetskrige, stilkonflikter og uforudsigelige overrides. Når du sender `className="bg-blue-500"` til en komponent, der allerede har `bg-red-500`, hvilken vinder så?
|
|
|
|
Uden korrekt håndtering anvendes begge klasser, og resultatet afhænger af mange faktorer - CSS-kildens rækkefølge, klassernes specificitet, bundlerens klasse-sammensmeltingsalgoritme osv.
|
|
|
|
## Intelligent sammensmeltning af klasser
|
|
|
|
Biblioteket `tailwind-merge` løser dette ved at forstå Tailwinds klasseopbygning og intelligent løse konflikter. Når to klasser målretter den samme CSS-egenskab, beholdes kun den sidste.
|
|
|
|
```tsx title="Without 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="With 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"
|
|
```
|
|
|
|
Dette virker for 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"
|
|
```
|
|
|
|
Biblioteket forstår også 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"
|
|
```
|
|
|
|
## Betingede klasser
|
|
|
|
Ofte skal du anvende klasser betinget baseret på props eller state. Biblioteket `clsx` tilbyder et rent API til dette:
|
|
|
|
```tsx title="Using 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'
|
|
);
|
|
```
|
|
|
|
Et almindeligt mønster er at flette et standardsæt af klasser med indkommende props samt enhver brugerdefineret logik, vi måtte have:
|
|
|
|
```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}
|
|
/>
|
|
);
|
|
};
|
|
```
|
|
|
|
## Hjælpefunktionen `cn`
|
|
|
|
Funktionen `cn`, populariseret af [shadcn/ui](https://ui.shadcn.com/), kombinerer `clsx` og `tailwind-merge` for at give dig både betinget logik og intelligent sammensmeltning:
|
|
|
|
```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));
|
|
}
|
|
```
|
|
|
|
Kraften kommer fra rækkefølgen - basisstilarter først, betingelser anden, bruger-overrides sidst. Dette sikrer forudsigelig adfærd samtidig med fuld tilpasning.
|
|
|
|
## Class Variance Authority (CVA)
|
|
|
|
For komplekse komponenter med mange varianter bliver manuel håndtering af betingede klasser uhåndterligt. [Class Variance Authority (CVA)](https://cva.style/docs) tilbyder et deklarativt API til at definere komponentvarianter.
|
|
|
|
For eksempel, her er et uddrag fra [Button](https://ui.shadcn.com/docs/components/button)-komponenten fra 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",
|
|
},
|
|
}
|
|
)
|
|
```
|
|
|
|
## Bedste praksis
|
|
|
|
### 1. Rækkefølgen betyder noget
|
|
|
|
Anvend altid klasser i denne rækkefølge:
|
|
1. Basisstilarter (altid anvendt)
|
|
2. Variantstilarter (baseret på props)
|
|
3. Betingede stilarter (baseret på state)
|
|
4. Bruger-overrides (className-prop)
|
|
|
|
```tsx
|
|
className={cn(
|
|
'base-styles', // 1. Base
|
|
variant && variantStyles, // 2. Variants
|
|
isActive && 'active', // 3. Conditionals
|
|
className // 4. User overrides
|
|
)}
|
|
```
|
|
|
|
### 2. Dokumentér dine varianter
|
|
|
|
Brug TypeScript og JSDoc til at dokumentere, hvad hver variant gør:
|
|
|
|
```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. Ekstraher gentagne mønstre
|
|
|
|
Hvis du oplever, at du gentagne gange skriver den samme betingede logik, så ekstraher den:
|
|
|
|
```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)}
|
|
```
|
|
|
|
## Migreringsvejledning
|
|
|
|
Hvis du migrerer fra en anden styling-tilgang, her er hvordan du tilpasser almindelige mønstre:
|
|
|
|
### Fra CSS Modules
|
|
|
|
```tsx title="Before - CSS Modules"
|
|
import styles from './Button.module.css';
|
|
|
|
<button className={`${styles.button} ${styles[variant]} ${className}`} />
|
|
```
|
|
|
|
```tsx title="After - cn + Tailwind"
|
|
import { cn } from '@/lib/utils';
|
|
|
|
<button className={cn(
|
|
'px-4 py-2 rounded-lg',
|
|
variant === 'primary' && 'bg-blue-500 text-white',
|
|
className
|
|
)} />
|
|
```
|
|
|
|
### Fra styled-components
|
|
|
|
```tsx title="Before - styled-components"
|
|
const Button = styled.button<{ $primary?: boolean }>`
|
|
padding: 8px 16px;
|
|
background: ${props => props.$primary ? 'blue' : 'gray'};
|
|
`;
|
|
```
|
|
|
|
```tsx title="After - cn + Tailwind"
|
|
function Button({ primary, className, ...props }) {
|
|
return (
|
|
<button
|
|
className={cn(
|
|
'px-4 py-2',
|
|
primary ? 'bg-blue-500' : 'bg-gray-500',
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Ydelsesovervejelser
|
|
|
|
Både `clsx` og `tailwind-merge` er højt optimerede, men husk disse tips:
|
|
|
|
1. **Definér varianter uden for komponenter** - CVA-varianter bør defineres uden for komponentet for at undgå genoprettelse ved hver render.
|
|
|
|
2. **Memoisér komplekse beregninger** - Hvis du har dyre betingede beregninger, overvej at memoisisere:
|
|
|
|
```tsx
|
|
const className = useMemo(
|
|
() => cn(
|
|
baseStyles,
|
|
expensiveComputation(props),
|
|
className
|
|
),
|
|
[props, className]
|
|
);
|
|
```
|
|
|
|
3. **Brug CSS-variabler til dynamiske værdier** - I stedet for at generere klasser dynamisk, brug CSS-variabler:
|
|
|
|
```tsx title="Prefer CSS variables"
|
|
// Good
|
|
<div
|
|
className="bg-[var(--color)]"
|
|
style={{ '--color': dynamicColor } as React.CSSProperties}
|
|
/>
|
|
|
|
// Avoid
|
|
<div className={`bg-[${dynamicColor}]`} />
|
|
```
|
|
|
|
Kombinationen af Tailwind CSS, intelligent klasse-sammensmeltning og variant-API'er giver et robust fundament for komponentstyling. Denne tilgang skalerer fra simple knapper til komplekse designsystemer, samtidig med at den bevarer forudsigelighed og en god udvikleroplevelse. |