React tutorial
Typed function components, one file each, controlled with the standard value + onChange pattern — everything works
the way React already does.
What you'll build
A small contact form — labelled input, submit button, controlled state, real keyboard and screen-reader behaviour — first bare, then styled, in about 10 minutes.
Before you start
You need git, Node 22 or later, and pnpm 10 or later. Any React 18+ project works — Next.js, Vite, or plain React.
Step 1 — Get the code
git clone https://github.com/LilyDesignSystem/lily-design-system
cd lily-design-system/lily-design-system-react-headless
pnpm install
pnpm test # vitest + @testing-library/react
pnpm run storybook # browse all 571 components Components are flat files in components/ — Button.tsx, TextInput.tsx — each with its test,
story, and docs alongside. Copy the files you need into your app, or
vendor the repo and import from it.
Step 2 — Your first component
import Button from "lily-design-system-react-headless/components/Button";
export function SaveBar() {
return <Button onClick={save}>Save</Button>;
} Renders <button class="button" type="button">. Props: type, disabled, pressed (renders aria-pressed), label (an aria-label override), className, and any extra
attribute spreads onto the root element.
Step 3 — Compose a small form
import { useState } from "react";
import Form from ".../components/Form";
import Field from ".../components/Field";
import TextInput from ".../components/TextInput";
import Button from ".../components/Button";
export function ContactForm() {
const [name, setName] = useState("");
return (
<Form label="Contact form" onSubmit={handleSubmit}>
<Field label="Full name" required>
<TextInput id="name" label="Full name" value={name} onChange={setName} required />
</Field>
<Button type="submit">Save</Button>
</Form>
);
} Field takes the visible label as its own prop
rather than a nested label component — pass it once, on Field, and TextInput's matching label supplies the accessible name.
Verified 2026-08-29: an earlier draft nested a
separate Label inside Field, which renders two labels (one empty) because Field already
renders its own from the label prop — caught by
compiling the exact snippet. The contact-form composed route is the same pattern, exercised by real e2e and axe tests.
Step 4 — Style it
Everything above renders as pure structure, ready for your styles.
Style the hooks — .button, .text-input, .label, .field — in
your stylesheet, layer Tailwind utilities via className, or
link a ready-made theme:
<link rel="stylesheet" href="/themes/united-states-web-design-system.css" /> Step 5 — See it all working
The Next.js example app is the full catalog styled and demoed, with Playwright e2e and an axe-core baseline:
git clone https://github.com/LilyDesignSystem/lily-design-system
cd lily-design-system/lily-design-system-react-next-examples
pnpm install && pnpm run dev Next steps
- Theming — runtime switching with the React theme-picker helper.
- Preference helpers — language and text size.
- Component catalog — props, ARIA, and keyboard per component.
Questions along the way? The help page is full of answers — and if a step could be clearer, tell us and we'll gladly improve the tutorial.