Stilizacija

Uslovno i kompozabilno stilizovanje pomoću Tailwind klasa.

Moderni biblioteki komponenti zahtevaju fleksibilne sisteme stilizovanja koji mogu da podnesu kompleksne zahteve bez žrtvovanja iskustva programera. Kombinacija Tailwind CSS-a sa inteligentnim spajanjem klasa pojavila se kao moćan obrazac za izgradnju prilagodljivih komponenti.

Ovaj pristup rešava fundamentalni konflikt između pružanja smislenih podrazumevanih vrednosti i omogućavanja potpune prilagodbe - izazov koji je mučio biblioteke komponenti dugi niz godina.

Problem sa tradicionalnim stilizovanjem

Tradicionalni pristupi CSS-u često vode do ratova specifčnosti, konflikata stilova i nepredvidivih nadjačavanja. Kada prosledite className="bg-blue-500" komponenti koja već ima bg-red-500, koja će pobediti?

Bez pravilnog rukovanja, obe klase se primenjuju i rezultat zavisi od mnogo faktora - CSS izvornog reda, specifčnosti klasa, algoritma bundlera za spajanje klasa, itd.

Inteligentno spajanje klasa

Biblioteka tailwind-merge rešava ovo razumevanjem strukture Tailwind klasa i inteligentnim rešavanjem konflikata. Kada dve klase ciljaju isto CSS svojstvo, zadržava samo poslednju.

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"

Ovo funkcioniše za sve Tailwind utilitete:

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"

Biblioteka takođe razume Tailwindov sistem modifikatora:

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

Uslovno dodavanje klasa

Često je potrebno primeniti klase uslovno, na osnovu props-a ili stanja. Biblioteka clsx pruža čist API za ovo:

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

Uobičajeni obrazac je spajanje skupa podrazumevanih klasa sa dolazećim props-ima, kao i bilo kojom prilagođenom logikom koju imamo:

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 pomoćna funkcija

Funkcija cn, popularizovana od strane shadcn/ui, kombinuje clsx i tailwind-merge kako bi vam omogućila i uslovnu logiku i inteligentno spajanje:

lib/utils.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

Snaga dolazi iz redosleda - osnovni stilovi prvi, uslovi drugi, korisnička nadjačavanja poslednja. Ovo obezbeđuje predvidivo ponašanje uz održavanje potpune prilagodljivosti.

Class Variance Authority (CVA)

Za kompleksne komponente sa mnogobrojnim varijantama, ručno upravljanje uslovnim klasama postaje nepregledno. Class Variance Authority (CVA) pruža deklarativni API za definisanje varijanti komponenti.

Na primer, evo isječka iz Button komponente iz 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",
    },
  }
)

Najbolje prakse

1. Poredak je bitan

Uvek primenjujte klase u ovom redosledu:

  1. Osnovni stilovi (uvek primenjeni)
  2. Stilovi varijanti (na osnovu props-a)
  3. Uslovni stilovi (na osnovu stanja)
  4. Korisnička nadjačavanja (prop className)
className={cn(
  'base-styles',            // 1. Base
  variant && variantStyles, // 2. Variants
  isActive && 'active',     // 3. Conditionals
  className                 // 4. User overrides
)}

2. Dokumentujte svoje varijante

Koristite TypeScript i JSDoc da dokumentujete šta svaka varijanta radi:

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. Ekstrahujte ponovljene obrasce

Ako primetite da stalno pišete istu uslovnu logiku, izvucite je:

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

Vodič za migraciju

Ako migrirate sa drugog pristupa stilizovanju, evo kako da prilagodite uobičajene obrasce:

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

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

Razmatranja performansi

I clsx i tailwind-merge su visoko optimizovani, ali imajte na umu sledeće savete:

  1. Definišite varijante van komponenti - CVA varijante bi trebalo definisati van komponente kako biste izbegli rekreiranje pri svakom renderovanju.

  2. Memorizujte kompleksne izračune - Ako imate skupu uslovnu logiku, razmotrite memorizaciju:

const className = useMemo(
  () => cn(
    baseStyles,
    expensiveComputation(props),
    className
  ),
  [props, className]
);
  1. Koristite CSS promenljive za dinamičke vrednosti - Umesto generisanja klasa dinamički, koristite CSS promenljive:
Prefer CSS variables
// Good
<div
  className="bg-[var(--color)]"
  style={{ '--color': dynamicColor } as React.CSSProperties}
/>

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

Kombinacija Tailwind CSS-a, inteligentnog spajanja klasa i varijantnih API-ja pruža robusnu osnovu za stilizovanje komponenti. Ovaj pristup skalira od jednostavnih dugmadi do kompleksnih dizajn sistema, uz održavanje predvidivosti i dobrog iskustva za programere.