Stilhantering

Villkorlig och komponerbar stilhantering med Tailwind-klasser.

Moderna komponentbibliotek behöver flexibla stilssystem som kan hantera komplexa krav utan att offra utvecklarupplevelsen. Kombinationen av Tailwind CSS med intelligent klasssammanslagning har framstått som ett kraftfullt mönster för att bygga anpassningsbara komponenter.

Denna metod löser den grundläggande spänningen mellan att erbjuda rimliga standardinställningar och att tillåta fullständig anpassning - en utmaning som har plågat komponentbibliotek i åratal.

Problemet med traditionell styling

Traditionella CSS-metoder leder ofta till specificitetskrig, stilkonflikter och oförutsägbara överskrivningar. När du skickar className="bg-blue-500" till en komponent som redan har bg-red-500, vilken vinner?

Utan korrekt hantering tillämpas båda klasserna och resultatet beror på många faktorer - CSS-källordning, klassernas specificitet, bundlerns algoritm för klasssammanslagning osv.

Intelligent sammanslagning av klasser

Biblioteket tailwind-merge löser detta genom att förstå Tailwinds klassstruktur och intelligent lösa konflikter. När två klasser riktar in sig på samma CSS-egenskap behåller det endast den sista.

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"

Detta fungerar för alla 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 förstår även Tailwinds modifierarsystem:

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

Villkorliga klasser

Ofta behöver du tillämpa klasser villkorligt baserat på props eller state. Biblioteket clsx erbjuder ett rent API för detta:

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

Ett vanligt mönster är att slå ihop en uppsättning standardklasser med inkommande props, samt eventuell egen logik:

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

cn-hjälpfunktionen

Funktionen cn, populärgjord av shadcn/ui, kombinerar clsx och tailwind-merge för att ge både villkorslogik och intelligent sammanslagning:

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 från ordningen - basstilar först, villkor andra, användarens överskrivningar sist. Detta säkerställer förutsägbart beteende samtidigt som full anpassning bibehålls.

Class Variance Authority (CVA)

För komplexa komponenter med många varianter blir manuell hantering av villkorliga klasser ohanterligt. Class Variance Authority (CVA) erbjuder ett deklarativt API för att definiera komponentvarianter.

Till exempel, här är ett utdrag från Button-komponenten från 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",
    },
  }
)

Bästa praxis

1. Ordning spelar roll

Tillämpa alltid klasser i denna ordning:

  1. Basstilar (alltid tillämpade)
  2. Variantstilar (baserat på props)
  3. Villkorliga stilar (baserat på state)
  4. Användaröverskrivningar (className-prop)
className={cn(
  'base-styles',            // 1. Bas
  variant && variantStyles, // 2. Varianter
  isActive && 'active',     // 3. Villkorliga
  className                 // 4. Användaröverskrivningar
)}

2. Dokumentera dina varianter

Använd TypeScript och JSDoc för att dokumentera vad varje 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. Extrahera upprepade mönster

Om du märker att du skriver samma villkorliga logik upprepade gånger, extrahera 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)}

Migreringsguide

Om du migrerar från en annan stilmetod, här är hur du anpassar vanliga mönster:

Från 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
)} />

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

Prestandaöverväganden

Både clsx och tailwind-merge är mycket optimerade, men tänk på följande tips:

  1. Definiera varianter utanför komponenter - CVA-varianter bör definieras utanför komponenten för att undvika att de skapas om vid varje render.

  2. Memoisera komplexa beräkningar - Om du har kostsamma villkorliga beräkningar, överväg att memoizera:

const className = useMemo(
  () => cn(
    baseStyles,
    expensiveComputation(props),
    className
  ),
  [props, className]
);
  1. Använd CSS-variabler för dynamiska värden - Istället för att generera klasser dynamiskt, använd CSS-variabler:
Prefer CSS variables
// Good
<div
  className="bg-[var(--color)]"
  style={{ '--color': dynamicColor } as React.CSSProperties}
/>

// Avoid
<div className={`bg-[${dynamicColor}]`} />

Kombinationen av Tailwind CSS, intelligent klasssammanslagning och variant-API:er ger en robust grund för komponentstyling. Denna metod skalar från enkla knappar till komplexa designsystem samtidigt som förutsägbarhet och utvecklarupplevelse bibehålls.