Tyylittely

Ehdollinen ja yhdisteltävä tyylitys Tailwind-luokkien avulla.

Nykyaikaiset komponenttikirjastot tarvitsevat joustavia tyylijärjestelmiä, jotka pystyvät käsittelemään monimutkaisia vaatimuksia tinkimättä kehittäjäkokemuksesta. Tailwind CSS:n yhdistäminen älykkääseen luokkien yhdistelyyn on muodostunut tehokkaaksi malliksi räätälöitävien komponenttien rakentamiseen.

Tämä lähestymistapa ratkaisee perustavanlaatuisen jännitteen järkevistä oletusarvoista ja täydellisestä mukautettavuudesta - haaste, joka on vaivannut komponenttikirjastoja vuosia.

Ongelma perinteisen tyylityksen kanssa

Perinteiset CSS-lähestymistavat johtavat usein spesifisyyssotiin, tyylikonflikteihin ja ennakoimattomiin ylikirjoituksiin. Kun välität komponentille className="bg-blue-500", mutta komponentissa on jo bg-red-500, kumpi voittaa?

Ilman asianmukaista käsittelyä molemmat luokat voivat päteä ja lopputulos riippuu monista tekijöistä – CSS:n lähdejärjestyksestä, luokkien spesifisyydestä, bundlerin luokkien yhdistelyalgoritmista jne.

Luokkien älykäs yhdistäminen

tailwind-merge-kirjasto ratkaisee tämän ymmärtämällä Tailwindin luokkarakenteen ja ratkaisemalla konfliktit älykkäästi. Kun kaksi luokkaa kohdistuvat samaan CSS-ominaisuuteen, se pitää vain viimeisen.

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"

Tämä toimii kaikkien Tailwind-apuohjelmien kanssa:

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"

Kirjasto ymmärtää myös Tailwindin modifikaattorijärjestelmän:

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

Ehdolliset luokat

Usein tarvitsee soveltaa luokkia ehdollisesti propsien tai tilan perusteella. clsx-kirjasto tarjoaa selkeän rajapinnan tähän:

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

Yleinen käytäntö on yhdistää oletusarvoinen luokkajoukko saapuviin props-arvoihin sekä mahdolliseen omaan logiikkaamme:

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-apuohjelma

cn-funktio, jonka tunnetuksi teki shadcn/ui, yhdistää clsx- ja tailwind-merge-kirjastot tarjoten sekä ehdollisen logiikan että älykkään yhdistelyn:

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

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

Teho syntyy järjestyksestä - perusmuotoilu ensin, ehdolliset tyylit toisena, käyttäjän ylikirjoitukset viimeisenä. Tämä varmistaa ennakoitavan käyttäytymisen samalla kun säilytetään täysi mukautettavuus.

Class Variance Authority (CVA)

Monimutkaisille komponenteille, joilla on paljon variantteja, ehdollisten luokkien manuaalinen hallinta käy nopeasti työlääksi. Class Variance Authority (CVA) tarjoaa deklaratiivisen rajapinnan komponenttivarianttien määrittelyyn.

Esimerkiksi tässä on ote shadcn/ui:n Button-komponentista:

@/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",
    },
  }
)

Parhaat käytännöt

1. Järjestyksellä on merkitystä

Sovella luokkia aina tässä järjestyksessä:

  1. Perusmuotoilut (aina käytössä)
  2. Varianttien tyylit (propsien perusteella)
  3. Ehdolliset tyylit (tilan perusteella)
  4. Käyttäjän ylikirjoitukset (className-prop)
className={cn(
  'base-styles',            // 1. Base
  variant && variantStyles, // 2. Variants
  isActive && 'active',     // 3. Conditionals
  className                 // 4. User overrides
)}

2. Dokumentoi varianttisi

Käytä TypeScriptiä ja JSDocia kuvaamaan, mitä kukin variantti tekee:

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. Ota toistuvat mallit talteen

Jos huomaat kirjoittavasi samaa ehdollista logiikkaa toistuvasti, erottele se:

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

Migraatio-opas

Jos siirryt eri tyylityslähestymistavasta, tässä on miten sopeuttaa yleisiä malleja:

CSS Modulesista

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

styled-componentsista

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

Suorituskykylähtökohdat

Sekä clsx että tailwind-merge ovat erittäin optimoituja, mutta pidä nämä vinkit mielessä:

  1. Määrittele variantit komponentin ulkopuolella - CVA-variantit tulisi määritellä komponentin ulkopuolelle, jotta niitä ei luoda uudelleen jokaisella renderöinnillä.

  2. Muistimuistioi (memoize) monimutkaiset laskelmat - Jos sinulla on raskasta ehdollista logiikkaa, harkitse muistimuistion käyttöä:

const className = useMemo(
  () => cn(
    baseStyles,
    expensiveComputation(props),
    className
  ),
  [props, className]
);
  1. Käytä CSS-muuttujia dynaamisille arvoille - Dynaamisten arvojen luokkien generoimisen sijaan käytä CSS-muuttujia:
Prefer CSS variables
// Good
<div
  className="bg-[var(--color)]"
  style={{ '--color': dynamicColor } as React.CSSProperties}
/>

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

Tailwind CSS:n, älykkään luokkien yhdistelyn ja variantti-rajapintojen yhdistelmä tarjoaa vankan perustan komponenttien tyylittämiselle. Tämä lähestymistapa skaalautuu yksinkertaisista painikkeista monimutkaisiin design-järjestelmiin säilyttäen ennakoitavuuden ja hyvän kehittäjäkokemuksen.