Een componentbibliotheek bouwen
Bouw een set van tien herbruikbare componenten met uw tokens, pas consistente variant-API's toe met CVA en gebruik toegankelijke markup-patronen.
Een componentbibliotheek bouwen is een gratis Tailwind CSS Academy-les op CoddyKit. Dit is les 3 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Tailwind CSS Academy. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Tailwind CSS Academy bevat in totaal 4 lessen.
Doelen voor het bouwen van de componentbibliotheek
Het bouwen van de componentbibliotheek betekent dat u de token- en configuratielaag vertaalt naar een set bruikbare, gedocumenteerde UI-primitieven. Elke component moet de semantische tokens gebruiken die in de vorige stap zijn gedefinieerd, een getypeerde variant-API bieden, voldoen aan de toegankelijkheidsnormen van WCAG AA en ten minste één Storybook-verhaal hebben. In deze les bouwt u aan de hand van deze principes tien representatieve componenten.
Button-component met CVA
De Button is doorgaans de eerste component in elke bibliotheek. Gebruik class-variance-authority (CVA) om getypeerde eigenschappen voor varianten en afmetingen te definiëren. CVA koppelt combinaties van eigenschappen aan klasse-tekenreeksen, waardoor de uitvoer van klassen schoon en voorspelbaar blijft. De component accepteert de eigenschappen variant (primary, secondary, ghost, danger) en size (sm, md, lg), met volledige TypeScript-inferentie.
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-component
De Badge is een klein inline-label dat wordt gebruikt voor statusindicatoren en categorietags. De component gebruikt CVA met kleurvarianten die aan een semantische betekenis zijn gekoppeld: default (grijs), success (groen), warning (geel), danger (rood), info (blauw). De basisstijl maakt de pilvorm die bij alle varianten hoort.
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-component
De Input-component wikkelt een native input in met consistente opmaak en doorgegeven refs voor compatibiliteit met formulierbibliotheken. Neem een label en optioneel foutbericht op als onderdeel van de API van de component. In de foutstatus verandert de kleur van de rand en ring van grijs naar rood, zodat u direct visuele feedback krijgt zonder extra CSS nodig te hebben.
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-component
De Card is een containercomponent met optionele subcomponenten CardHeader, CardBody en CardFooter. Gebruik het patroon van samengestelde componenten: exporteer verwante onderdelen vanuit hetzelfde bestand en geef ze namen met puntnotatie of gebruik afzonderlijke exports. De Card gebruikt de semantische tokens shadow-card en bg-surface die in de configuratie zijn gedefinieerd.
// 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-component
De Avatar toont een gebruikersfoto of valt terug op initialen in een gekleurde cirkel. Gebruik een size-variant voor kleine (8), middelgrote (10) en grote (14) afmetingen. Wanneer er geen afbeelding is opgegeven, toont u de initialen die uit de naam van de gebruiker zijn gehaald, op een deterministische achtergrondkleur die is gebaseerd op het eerste teken van de naam. Voeg een statuspuntvariant toe voor indicatoren voor online, offline en bezet.
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-laadcomponent
Een Spinner geeft een laadstatus aan. Gebruik Tailwinds animate-spin op een SVG met een transparant spoor en een gekleurde boog. De formaatvarianten komen overeen met die van de Button-component, zodat een Spinner in een laadknop op het juiste formaat wordt weergegeven. Neem role='status' en een visueel verborgen label voor schermlezers op.
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-component
Een Tooltip toont aanvullende informatie wanneer u de aanwijzer erop plaatst of de component de focus krijgt. De eenvoudigste implementatie gebruikt alleen CSS met group en absolute positionering. De trigger wordt omwikkeld door een container met group relative, en het tooltip-div gebruikt absolute bottom-full mb-2 hidden group-hover:block om bij aanwijzen boven de trigger te verschijnen. Voeg voor toegankelijkheid role='tooltip' en een aria-describedby-koppeling toe.
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>
);
}Toastmeldingscomponent
Toastmeldingen verschijnen in een hoek van het scherm en verdwijnen automatisch. Gebruik fixed bottom-4 right-4 z-50 flex flex-col gap-2 voor de container met meerdere toasts. Elke toast heeft een gekleurde linkerrand voor de ernstvariant, een pictogram, een bericht en een sluitknop. Animeer het verschijnen en verdwijnen met CSS-overgangen op opacity en 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>
);
}Storybook-verhalen voor componenten
Elke component heeft ten minste één Storybook-verhaal per betekenisvolle variant nodig. Verhalen dienen als levende documentatie en als basis voor visuele regressietests. Gebruik het CSF3-formaat (Component Story Format) met benoemde exports voor elk verhaal. Neem een Default, een verhaal per variant en een AllVariants-overzichtsverhaal op waarin alle varianten naast elkaar worden weergegeven voor een snelle vergelijking.
// 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>
),
};Eenheidstests voor componenten
Test het gedrag van componenten, niet de opmaak. Richt tests op het volgende: de juiste ARIA-attributen worden weergegeven, klikhandlers worden uitgevoerd, de uitgeschakelde status voorkomt interactie en foutberichten verschijnen wanneer de foutproperty is ingesteld. Gebruik @testing-library/react met de op toegankelijkheid gerichte zoekopdrachten zoals getByRole en getByLabelText in plaats van te zoeken op klassenamen, omdat tests daarmee gekoppeld raken aan implementatiedetails van de opmaak.
// 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();
});Snelle controle
Test uw begrip van de concepten van Tailwind CSS Mastery uit deze les.
Samenvatting van de les
In deze les hebt u geleerd hoe u: getypeerde componenten met CVA bouwt voor Button, Badge, Input en Card met semantische ontwerptokens, toegankelijke componenten schrijft met ARIA-attributen, schermlezerlabels en toetsenbordondersteuning, en Storybook-verhalen en eenheidstests maakt voor elke component. Hierna volgt de laatste les: het documenteren en overdragen van het ontwerpsysteem.
Leer HTML met een AI-tutor — gratis
Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.
- Cursussen
- 30
- Lessen
- 120
Veelgestelde vragen
Is de les “Een componentbibliotheek bouwen” gratis?
Ja — de volledige tekst van “Een componentbibliotheek bouwen” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus Tailwind CSS Academy wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus Tailwind CSS Academy bevat in totaal 4 lessen.
Wat leer ik in “Een componentbibliotheek bouwen”?
Bouw een set van tien herbruikbare componenten met uw tokens, pas consistente variant-API's toe met CVA en gebruik toegankelijke markup-patronen. Je oefent met Tailwind CSS Academy door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.
Heb ik ervaring nodig om met Tailwind CSS Academy te beginnen?
Ervaring vooraf is niet nodig. Tailwind CSS Academy op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 3 van 4.
Hoe lang duurt de les “Een componentbibliotheek bouwen”?
De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.
Kan ik code schrijven en uitvoeren in deze les over Tailwind CSS Academy?
Ja. Elke les over Tailwind CSS Academy bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.
Alle lessen in deze cursus
- Het designsysteem plannen
- De token- en configuratielaag bouwen
- Een componentbibliotheek bouwen
- Documentatie en overdracht aan het team