Tailwind CSS Academy · Les

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.

Les 3 van 413 stappen

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>
        &times;
      </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.

Gratis beginnen

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

  1. Het designsysteem plannen
  2. De token- en configuratielaag bouwen
  3. Een componentbibliotheek bouwen
  4. Documentatie en overdracht aan het team
← Terug naar Tailwind CSS Academy