Komponenttikirjaston rakentaminen
Rakentakaa kymmenen uudelleenkäytettävää komponenttia tokenien avulla ja käyttäkää yhdenmukaisia varianttirajapintoja CVA:lla sekä saavutettavia merkkausmalleja.
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>
×
</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.
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
- Design-järjestelmän suunnittelu
- Token- ja asetustason rakentaminen
- Komponenttikirjaston rakentaminen
- Dokumentointi ja tiimin käyttöönotto