Bygg komponentbiblioteket
Bygg en uppsättning med tio återanvändbara komponenter med hjälp av era tokens, och tillämpa enhetliga variant-API:er med CVA och tillgängliga markupmönster.
Bygg komponentbiblioteket är en gratis lektion i Tailwind CSS Academy på CoddyKit. Detta är lektion 3 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för Tailwind CSS Academy, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Tailwind CSS Academy innehåller totalt 4 lektioner.
Mål för att bygga komponentbiblioteket
Att bygga komponentbiblioteket innebär att översätta token- och konfigurationslagret till en uppsättning användbara, dokumenterade UI-primitiver. Varje komponent ska använda de semantiska tokens som definierades i föregående steg, exponera ett typat variant-API, uppfylla tillgänglighetsstandarden WCAG AA och ha minst en Storybook-story. I den här lektionen går vi igenom hur tio representativa komponenter byggs utifrån dessa principer.
Knappkomponent med CVA
Button är vanligtvis den första komponenten i alla bibliotek. Använd class-variance-authority (CVA) för att definiera typade props för varianter och storlekar. CVA mappar kombinationer av props till klasssträngar, vilket ger rena och förutsägbara klassutdata. Komponenten accepterar propsen variant (primary, secondary, ghost, danger) och size (sm, md, lg), med full TypeScript-inferens.
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-komponent
Badge är en liten inline-etikett som används för statusindikatorer och kategoritaggar. Den använder CVA med färgvarianter som är kopplade till semantisk betydelse: default (grå), success (grön), warning (gul), danger (röd), info (blå). Grundstilen skapar den pillform som är gemensam för alla varianter.
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-komponent
Input-komponenten kapslar in en inbyggd input med enhetlig styling och vidarebefordrade refs för kompatibilitet med formulärbibliotek. Inkludera en label och ett valfritt felmeddelande som en del av komponentens API. Feltillståndet ändrar kantlinjens och ringens färg från grå till röd, vilket ger omedelbar visuell återkoppling utan att extra CSS behövs.
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-komponent
Card är en containerkomponent med valfria underkomponenter som CardHeader, CardBody och CardFooter. Använd mönstret med sammansatta komponenter — exportera relaterade delar från samma fil och namnge dem med punktnotation eller som individuella exporter. Card använder de semantiska tokensen shadow-card och bg-surface som definieras i konfigurationen.
// 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-komponent
Avatar visar ett användarfoto eller använder användarens initialer i en färgad cirkel som reserv. Använd en size-variant för storlekarna liten (8), medium (10) och stor (14). När ingen bild har angetts visas initialer som hämtas från användarens namn i en deterministisk bakgrundsfärg baserad på namnets första tecken. Lägg till en statuspricksvariant för indikatorerna online/offline/upptagen.
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-komponent för laddning
En Spinner visar att något håller på att laddas. Använd Tailwinds animate-spin på en SVG med ett transparent spår och en färgad båge. Storleksvarianterna ska motsvara dem i Button-komponenten, så att en Spinner i en laddande knapp återges i rätt storlek. Inkludera role='status' och en visuellt dold etikett för skärmläsare.
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-komponent
En Tooltip visar kompletterande information när användaren hovrar över eller fokuserar på något. Den enklaste implementationen använder enbart CSS med group och absolut positionering. Utlösaren placeras i en container med group relative, och tooltip-diven använder absolute bottom-full mb-2 hidden group-hover:block för att visas ovanför utlösaren vid hovring. Lägg till role='tooltip' och en aria-describedby-koppling för tillgänglighet.
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-komponent för aviseringar
Toast-aviseringar visas i ett hörn av skärmen och stängs automatiskt. Använd fixed bottom-4 right-4 z-50 flex flex-col gap-2 för containern som innehåller flera toast-aviseringar. Varje toast har en färgad vänsterkant för allvarlighetsvarianten, en ikon, ett meddelande och en stängningsknapp. Animeringen av in- och utgången görs med CSS-övergångar för opacity och 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-stories för komponenter
Varje komponent behöver minst en Storybook-story för varje meningsfull variant. Stories fungerar som levande dokumentation och baslinjer för visuell regressionstestning. Använd CSF3-formatet (Component Story Format) med namngivna exporter för varje story. Inkludera en Default, en story per variant och en AllVariants-story som visar alla varianter sida vid sida för snabb jämförelse.
// 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>
),
};Enhetstestning av komponenter
Testa komponenternas beteende — inte deras styling. Fokusera testerna på att korrekta ARIA-attribut renderas, att klickhanterare körs, att avaktiverat tillstånd förhindrar interaktion och att felmeddelanden visas när error-propen har angetts. Använd @testing-library/react med dess tillgänglighetsfokuserade queries, som getByRole och getByLabelText, i stället för att söka efter klassnamn, eftersom det kopplar testerna till detaljer i stylingimplementationen.
// 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();
});Snabbkontroll
Testa dina kunskaper om koncepten i Tailwind CSS Mastery från den här lektionen.
Sammanfattning av lektionen
I den här lektionen lärde du dig att: bygga typade komponenter med CVA för Button, Badge, Input och Card med hjälp av semantiska designtokens, skriva tillgängliga komponenter med ARIA-attribut, skärmläsaretiketter och tangentbordsstöd samt skapa Storybook-stories och enhetstester för varje komponent. Nästa steg är den sista lektionen: att dokumentera och lämna över designsystemet.
Lär dig HTML med en AI-lärare – gratis
Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.
- Kurser
- 30
- Lektioner
- 120
Vanliga frågor
Är lektionen ”Bygg komponentbiblioteket” gratis?
Ja – hela texten till ”Bygg komponentbiblioteket” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i Tailwind CSS Academy, kan Ni uppgradera till CoddyKit PRO. Kursen i Tailwind CSS Academy innehåller totalt 4 lektioner.
Vad lär jag mig i ”Bygg komponentbiblioteket”?
Bygg en uppsättning med tio återanvändbara komponenter med hjälp av era tokens, och tillämpa enhetliga variant-API:er med CVA och tillgängliga markupmönster. Ni övar på Tailwind CSS Academy med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.
Behöver jag någon erfarenhet för att börja lära mig Tailwind CSS Academy?
Du behöver inga förkunskaper. Utbildningen i Tailwind CSS Academy på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 3 av 4.
Hur lång tid tar lektionen ”Bygg komponentbiblioteket”?
De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.
Kan jag skriva och köra kod i den här Tailwind CSS Academy-lektionen?
Ja. Varje Tailwind CSS Academy-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.
Alla lektioner i den här kursen
- Planera designsystemet
- Bygg token- och konfigurationslagret
- Bygg komponentbiblioteket
- Dokumentation och överlämning till teamet