| name | component-library |
| description | Use when implementing or extending a shared component library — scaffolding new components with CVA variants, writing Storybook stories, wiring visual regression, or auditing existing components for state and a11y coverage. |
| metadata | {"references":["references/composition-patterns.md","references/state-management.md","references/variant-systems.md"]} |
Component Library
Build Order (with validation gates)
- Tokens published → verify with
rg -- '--color-' dist/tokens.css.
- Layout primitives (
Box, Stack, Flex, Grid) → verify token consumption: rg "#[0-9a-fA-F]{6}" src/primitives/ exits empty.
- Typography (
Heading, Text, Label).
- Button → run a11y audit before proceeding:
npx test-storybook --url http://127.0.0.1:6006 zero violations.
- Form atoms (
Input, Select, Checkbox, Radio) → keyboard nav test passes.
- Containers (
Card, Modal, Drawer) → focus-trap test passes for Modal.
- Data (
Table, List) → axe-core audit on rendered story.
- Nav (
Nav, Tabs, Breadcrumbs).
Gate per tier: a11y audit clean + visual regression reviewed before next tier.
Compound Component (Select)
import { createContext, useContext, useState, ReactNode } from "react";
type Ctx = { value: string | null; setValue: (v: string) => void };
const SelectCtx = createContext<Ctx | null>(null);
export function Select({ children, defaultValue = null }:
{ children: ReactNode; defaultValue?: string | null }) {
const [value, setValue] = useState<string | null>(defaultValue);
return (
<SelectCtx.Provider value={{ value, setValue }}>
<div role="listbox" className="rounded-md border">{children}</div>
</SelectCtx.Provider>
);
}
Select.Option = function Option({ value, children }:
{ value: string; children: ReactNode }) {
const ctx = useContext(SelectCtx)!;
const selected = ctx.value === value;
return (
<button role="option" aria-selected={selected}
onClick={() => ctx.setValue(value)}
className={selected ? "bg-[var(--color-brand-500)] text-white" : ""}>
{children}
</button>
);
};
CVA Variant Definition
import { cva } from "class-variance-authority";
export const button = cva("inline-flex items-center font-medium rounded-md", {
variants: {
intent: { primary: "bg-primary text-white",
outline: "border-2 border-primary text-primary",
ghost: "text-primary hover:bg-primary/10" },
size: { sm: "h-8 px-3 text-sm", md: "h-10 px-4", lg: "h-12 px-6 text-lg" }
},
compoundVariants: [
{ intent: "ghost", size: "sm", class: "underline-offset-2" }
],
defaultVariants: { intent: "primary", size: "md" }
});
Storybook Story (Button.stories.tsx)
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
const meta: Meta<typeof Button> = {
component: Button,
args: { children: "Click me" },
argTypes: { intent: { control: "select", options: ["primary","outline","ghost"] },
size: { control: "select", options: ["sm","md","lg"] } },
parameters: { a11y: { config: { rules: [{ id: "color-contrast", enabled: true }] } } }
};
export default meta;
export const Primary: StoryObj<typeof Button> = {};
export const Disabled: StoryObj<typeof Button> = { args: { disabled: true } };
export const Loading: StoryObj<typeof Button> = { args: { children: "Loading…" } };
Playwright Visual Regression (tests/visual/button.spec.ts)
import { test, expect } from "@playwright/test";
const stories = ["primary", "disabled", "loading"];
for (const id of stories) {
test(`Button ${id} visual`, async ({ page }) => {
await page.goto(`http://127.0.0.1:6006/iframe.html?id=button--${id}`);
await expect(page.locator("#storybook-root"))
.toHaveScreenshot(`button-${id}.png`, { maxDiffPixelRatio: 0.01 });
});
}
Run: npx playwright test tests/visual/ --update-snapshots on intentional changes; fail-block on diffs in CI.
Accessibility Audit (per-component gate)
npx test-storybook --url http://127.0.0.1:6006 --maxWorkers=2
npx @axe-core/cli http://127.0.0.1:6006/iframe.html?id=button--primary
Pass: zero serious/critical violations. Track moderate violations in issue tracker.
References
Next Steps