Tailwind CSS Academy · Oppitunti

Komponenttikirjaston rakentaminen

Rakentakaa kymmenen uudelleenkäytettävää komponenttia tokenien avulla ja käyttäkää yhdenmukaisia varianttirajapintoja CVA:lla sekä saavutettavia merkkausmalleja.

Oppitunti 3/413 vaihetta

Komponenttikirjaston rakentaminen on ilmainen Tailwind CSS Academy-oppitunti CoddyKitissä. Tämä on oppitunti 3/4. Voit lukea koko oppitunnin alta ilmaiseksi ja harjoitella sen jälkeen käytännössä selaimessa sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla. Oppitunti kuuluu Tailwind CSS Academy-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. Tailwind CSS Academy-kurssilla on yhteensä 4 oppituntia.

Komponenttikirjaston rakentamisen tavoitteet

Komponenttikirjaston rakentaminen tarkoittaa token- ja konfiguraatiokerroksen muuntamista joukoksi käytettäviä ja dokumentoituja käyttöliittymän peruskomponentteja. Jokaisen komponentin tulee käyttää edellisessä vaiheessa määriteltyjä semanttisia tokeneita, tarjota tyypitetty variantti-API, täyttää WCAG AA -saavutettavuusstandardit ja sisältää vähintään yksi Storybook-story. Tässä oppitunnissa rakennetaan näiden periaatteiden mukaisesti kymmenen edustavaa komponenttia.

Button-komponentti CVA:lla

Button on yleensä jokaisen kirjaston ensimmäinen komponentti. Määrittele tyypitetyt variantti- ja kokopropsit käyttämällä class-variance-authoritya (CVA). CVA yhdistää propsien yhdistelmät luokkamerkkijonoihin ja tuottaa selkeän ja ennakoitavan luokkien tulosteen. Komponentti hyväksyy variant-propsin (primary, secondary, ghost, danger) ja size-propsin (sm, md, lg), ja TypeScript päättelee tyypit kattavasti.

import { cva, type VariantProps } from 'class-variance-authority';

const buttonVariants = cva(
  // Base classes shared by all variants
  'inline-flex items-center justify-center rounded-button font-semibold transition focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
  {
    variants: {
      variant: {
        primary:   'bg-primary text-white hover:bg-primary-hover focus:ring-primary',
        secondary: 'bg-surface-elevated text-text-primary border border-border hover:bg-gray-100',
        ghost:     'text-primary hover:bg-primary-light',
        danger:    'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
      },
      size: {
        sm: 'px-3 py-1.5 text-sm',
        md: 'px-4 py-2 text-sm',
        lg: 'px-6 py-3 text-base',
      },
    },
    defaultVariants: { variant: 'primary', size: 'md' },
  }
);

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
  VariantProps<typeof buttonVariants>;

export function Button({ variant, size, className, ...props }: ButtonProps) {
  return <button className={buttonVariants({ variant, size, className })} {...props} />;
}

Badge-komponentti

Badge on pieni tekstin yhteydessä näytettävä tunniste, jota käytetään tilailmaisimiin ja luokittelutägeihin. Se käyttää CVA:ta, jossa värivariantit on yhdistetty semanttisiin merkityksiin: default (harmaa), success (vihreä), warning (keltainen), danger (punainen), info (sininen). Perustyyli luo kaikille varianteille yhteisen pillerimuodon.

import { cva, type VariantProps } from 'class-variance-authority';

const badgeVariants = cva(
  'inline-flex items-center rounded-badge px-2.5 py-0.5 text-xs font-semibold',
  {
    variants: {
      variant: {
        default: 'bg-gray-100 text-gray-800',
        success: 'bg-green-100 text-green-800',
        warning: 'bg-yellow-100 text-yellow-800',
        danger:  'bg-red-100 text-red-800',
        info:    'bg-blue-100 text-blue-800',
      },
    },
    defaultVariants: { variant: 'default' },
  }
);

type BadgeProps = React.HTMLAttributes<HTMLSpanElement> &
  VariantProps<typeof badgeVariants>;

export function Badge({ variant, className, ...props }: BadgeProps) {
  return <span className={badgeVariants({ variant, className })} {...props} />;
}

Input-komponentti

Input-komponentti käärii natiivin input-elementin yhtenäiseen tyylittelyyn ja välittää ref-viittaukset eteenpäin lomakekirjastojen yhteensopivuutta varten. Sisällytä komponentin API:in label ja valinnainen virheilmoitus. Virhetila vaihtaa reunan ja renkaan värin harmaasta punaiseksi, mikä antaa välittömän visuaalisen palautteen ilman ylimääräistä CSS:ää.

import { forwardRef } from 'react';
import { clsx } from 'clsx';

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  error?: string;
}

export const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, className, id, ...props }, ref) => (
    <div className='flex flex-col gap-1'>
      {label && (
        <label htmlFor={id} className='text-sm font-medium text-text-primary'>
          {label}
        </label>
      )}
      <input
        ref={ref}
        id={id}
        aria-invalid={Boolean(error)}
        aria-describedby={error ? `${id}-error` : undefined}
        className={clsx(
          'w-full rounded-input border px-3 py-2 text-sm text-text-primary transition',
          'focus:outline-none focus:ring-2 focus:ring-offset-0',
          error
            ? 'border-red-500 focus:ring-red-500'
            : 'border-border focus:border-primary focus:ring-primary',
          className
        )}
        {...props}
      />
      {error && (
        <p id={`${id}-error`} role='alert' className='text-xs text-red-600'>
          {error}
        </p>
      )}
    </div>
  )
);

Card-komponentti

Card on säilökomponentti, jolla on valinnaiset CardHeader-, CardBody- ja CardFooter-alakomponentit. Käytä yhdistelmäkomponenttimallia — vie toisiinsa liittyvät osat samasta tiedostosta ja nimeä ne piste-merkinnällä tai erillisinä vientikohteina. Card käyttää konfiguraatiossa määriteltyjä semanttisia tokeneita shadow-card ja bg-surface.

// Card.tsx
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={clsx('rounded-card bg-surface shadow-card overflow-hidden', className)}
      {...props}
    />
  );
}

export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={clsx('border-b border-border px-6 py-4', className)}
      {...props}
    />
  );
}

export function CardBody({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return <div className={clsx('px-6 py-4', className)} {...props} />;
}

export function CardFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={clsx('border-t border-border bg-surface-elevated px-6 py-3', className)}
      {...props}
    />
  );
}

Avatar-komponentti

Avatar näyttää käyttäjän valokuvan tai käyttää sen sijaan nimikirjaimia värillisessä ympyrässä. Käytä size-varianttia pienelle (8), keskikokoiselle (10) ja suurelle (14) koolle. Jos kuvaa ei ole annettu, näytä käyttäjän nimestä poimitut nimikirjaimet deterministisesti määräytyvässä taustavärissä, joka perustuu nimen ensimmäiseen merkkiin. Lisää statuspistevariantti online-, offline- ja busy-tilojen ilmaisemista varten.

const avatarSize = cva('rounded-full overflow-hidden flex-shrink-0', {
  variants: {
    size: {
      sm: 'h-8 w-8 text-xs',
      md: 'h-10 w-10 text-sm',
      lg: 'h-14 w-14 text-base',
    },
  },
  defaultVariants: { size: 'md' },
});

export function Avatar({ src, name, size }: AvatarProps) {
  const initials = name?.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase();

  return (
    <div className={avatarSize({ size })}>
      {src ? (
        <img src={src} alt={name} className='h-full w-full object-cover' />
      ) : (
        <div className='flex h-full w-full items-center justify-center
                        bg-primary-light font-semibold text-primary'>
          {initials}
        </div>
      )}
    </div>
  );
}

Spinner-latauskomponentti

Spinner ilmaisee lataustilan. Käytä Tailwindin animate-spin-luokkaa SVG-elementissä, jossa on läpinäkyvä rata ja värillinen kaari. Kokovariantit vastaavat Button-komponentin kokoja, jotta latauspainikkeen sisällä oleva Spinner näkyy oikean kokoisena. Sisällytä role='status' ja ruudunlukijoille visuaalisesti piilotettu selite.

const spinnerSize = cva('animate-spin', {
  variants: {
    size: { sm: 'h-4 w-4', md: 'h-5 w-5', lg: 'h-6 w-6' },
  },
  defaultVariants: { size: 'md' },
});

export function Spinner({ size }: { size?: 'sm' | 'md' | 'lg' }) {
  return (
    <svg
      className={spinnerSize({ size })}
      xmlns='http://www.w3.org/2000/svg'
      fill='none'
      viewBox='0 0 24 24'
      role='status'
      aria-label='Loading'
    >
      <circle className='opacity-25' cx='12' cy='12' r='10' stroke='currentColor' strokeWidth='4' />
      <path
        className='opacity-75'
        fill='currentColor'
        d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z'
      />
    </svg>
  );
}

Tooltip-komponentti

Tooltip näyttää täydentäviä tietoja osoittimen ollessa kohteen päällä tai kohteen ollessa fokusoituna. Yksinkertaisin toteutus käyttää pelkkää CSS:ää, group-luokkaa ja absoluuttista asemointia. Laukaisin sijoitetaan group relative -säilöön, ja tooltip-divissä käytetään absolute bottom-full mb-2 hidden group-hover:block -määritystä, jotta se näkyy laukaisimen yläpuolella osoittimen ollessa sen päällä. Lisää saavutettavuutta varten role='tooltip' ja aria-describedby-linkitys.

export function Tooltip({ content, children }: TooltipProps) {
  return (
    <div className='group relative inline-block'>
      {children}
      <div
        role='tooltip'
        className='pointer-events-none absolute bottom-full left-1/2 z-10
                   mb-2 -translate-x-1/2 whitespace-nowrap rounded-lg
                   bg-gray-900 px-3 py-1.5 text-xs text-white opacity-0
                   transition-opacity group-hover:opacity-100'
      >
        {content}
        {/* Arrow */}
        <div className='absolute left-1/2 top-full -translate-x-1/2
                        border-4 border-transparent border-t-gray-900' />
      </div>
    </div>
  );
}

Toast-ilmoituskomponentti

Toast-ilmoitukset näkyvät näytön kulmassa ja sulkeutuvat automaattisesti. Käytä useita toasteja sisältävässä säilössä määritystä fixed bottom-4 right-4 z-50 flex flex-col gap-2. Jokaisessa toastissa on vakavuusvarianttia ilmaiseva värillinen vasen reuna, kuvake, viesti ja sulkemispainike. Animoi sisään- ja poistumisen CSS-siirtymillä ominaisuuksille opacity ja translateY.

const toastVariants = cva(
  'flex items-start gap-3 rounded-card bg-surface shadow-elevated border-l-4 p-4 min-w-[300px]',
  {
    variants: {
      severity: {
        success: 'border-green-500',
        error:   'border-red-500',
        warning: 'border-yellow-500',
        info:    'border-blue-500',
      },
    },
    defaultVariants: { severity: 'info' },
  }
);

export function Toast({ severity, message, onClose }: ToastProps) {
  return (
    <div className={toastVariants({ severity })} role='alert'>
      <div className='flex-1'>
        <p className='text-sm font-medium text-text-primary'>{message}</p>
      </div>
      <button onClick={onClose} className='text-text-secondary hover:text-text-primary'>
        <span className='sr-only'>Dismiss</span>
        &times;
      </button>
    </div>
  );
}

Komponenttien Storybook-storyt

Jokaiselle komponentille tarvitaan vähintään yksi Storybook-story kutakin merkityksellistä varianttia kohti. Storyt toimivat elävänä dokumentaationa ja visuaalisen regressiotestauksen vertailukohtina. Käytä CSF3-muotoa (Component Story Format) ja nimettyjä vientikohteita kutakin storytä varten. Sisällytä Default, yksi story kutakin varianttia kohti sekä AllVariants-esittelystory, joka näyttää kaikki variantit rinnakkain nopeaa vertailua varten.

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  component: Button,
  title: 'Primitives/Button',
};
export default meta;
type Story = StoryObj<typeof Button>;

export const Default: Story = {
  args: { children: 'Click me', variant: 'primary', size: 'md' },
};

export const Secondary: Story = {
  args: { children: 'Cancel', variant: 'secondary' },
};

export const AllVariants: Story = {
  render: () => (
    <div className='flex flex-wrap gap-4'>
      <Button variant='primary'>Primary</Button>
      <Button variant='secondary'>Secondary</Button>
      <Button variant='ghost'>Ghost</Button>
      <Button variant='danger'>Danger</Button>
    </div>
  ),
};

Komponenttien yksikkötestaus

Testaa komponentin toimintaa — älä tyylittelyä. Keskity testeissä seuraaviin asioihin: oikeat ARIA-attribuutit tulostuvat, napsautusten käsittelijät suoritetaan, käytöstä poistettu tila estää vuorovaikutuksen ja virheilmoitukset näkyvät, kun error-prop on asetettu. Käytä @testing-library/react-kirjastoa ja sen saavutettavuuteen keskittyviä kyselyitä, kuten getByRole ja getByLabelText, sen sijaan että hakisit elementtejä luokkanimien perusteella, sillä se sitoo testit tyylittelyn toteutuksen yksityiskohtiin.

// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

test('calls onClick when clicked', () => {
  const handleClick = jest.fn();
  render(<Button onClick={handleClick}>Save</Button>);
  fireEvent.click(screen.getByRole('button', { name: 'Save' }));
  expect(handleClick).toHaveBeenCalledTimes(1);
});

test('does not call onClick when disabled', () => {
  const handleClick = jest.fn();
  render(<Button disabled onClick={handleClick}>Save</Button>);
  fireEvent.click(screen.getByRole('button', { name: 'Save' }));
  expect(handleClick).not.toHaveBeenCalled();
});

Pikatarkistus

Testaa, miten hyvin ymmärrät tämän oppitunnin Tailwind CSS Mastery -käsitteet.

Oppitunnin kertaus

Tässä oppitunnissa opit rakentamaan tyypitettyjä komponentteja CVA:lla Buttonia, Badgea, Inputia ja Cardia varten semanttisia design-tokeneita käyttäen, kirjoittamaan saavutettavia komponentteja ARIA-attribuuttien, ruudunlukijaselitteiden ja näppäimistötuen avulla sekä luomaan Storybook-storyja ja yksikkötestejä jokaista komponenttia varten. Seuraavaksi vuorossa on viimeinen oppitunti: design-järjestelmän dokumentointi ja luovuttaminen.

Aloita maksutta

Opi HTML tekoälytuutorin avulla — ilmaiseksi

Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.

Kurssit
30
Oppitunnit
120

Usein kysytyt kysymykset

Onko oppitunti ”Komponenttikirjaston rakentaminen” ilmainen?

Kyllä – oppitunnin ”Komponenttikirjaston rakentaminen” koko tekstin voi lukea täällä verkossa ilmaiseksi. Jos haluat harjoitella interaktiivisesti sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla sekä avata koko Tailwind CSS Academy-kurssin, päivitä CoddyKit PROhon. Tailwind CSS Academy-kurssilla on yhteensä 4 oppituntia.

Mitä opin oppitunnilla ”Komponenttikirjaston rakentaminen”?

Rakentakaa kymmenen uudelleenkäytettävää komponenttia tokenien avulla ja käyttäkää yhdenmukaisia varianttirajapintoja CVA:lla sekä saavutettavia merkkausmalleja. Harjoittelet Tailwind CSS Academy-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.

Tarvitsenko kokemusta aloittaakseni Tailwind CSS Academy-opiskelun?

Aiempi kokemus ei ole tarpeen. CoddyKitin Tailwind CSS Academy-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 3/4.

Kuinka kauan ”Komponenttikirjaston rakentaminen”-oppitunnin suorittaminen kestää?

Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.

Voinko kirjoittaa ja suorittaa koodia tällä Tailwind CSS Academy-oppitunnilla?

Kyllä. Jokainen Tailwind CSS Academy-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.

Kaikki tämän kurssin oppitunnit

  1. Design-järjestelmän suunnittelu
  2. Token- ja asetustason rakentaminen
  3. Komponenttikirjaston rakentaminen
  4. Dokumentointi ja tiimin käyttöönotto
← Takaisin: Tailwind CSS Academy