Styling

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.

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"
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:

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:

// 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:

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:

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, kombinerer clsx og tailwind-merge for at give dig både betinget logik og intelligent sammensmeltning:

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) tilbyder et deklarativt API til at definere komponentvarianter.

For eksempel, her er et uddrag fra Button-komponenten fra 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",
    },
  }
)

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

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:

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

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

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

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

const className = useMemo(
  () => cn(
    baseStyles,
    expensiveComputation(props),
    className
  ),
  [props, className]
);
  1. Brug CSS-variabler til dynamiske værdier - I stedet for at generere klasser dynamisk, brug CSS-variabler:
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.