alchemy-ui component library
ui-design-system/ui/alchemy-ui-component-librarylive
| Kind | frontend |
|---|---|
| Surface | SPA |
| Maturity | prod |
| Reusable | extract |
| Design system | alchemy-ui |
| Stack | react-19, typescript, tailwind-3.4, lucide-react, headlessui-2 |
| Depends on | _shared/design-system-and-tokens, _shared/react-app-skeleton |
| Renders | no backend module: a design-system row |
| Entry | alchemy_labs/frontend/src/pages/DesignSystemDemo.tsx |
| Reference implementation |
|
| Measured on | alchemy_labs d1a-token-source fd8558f 2026-09-11 |
| Version | 1 |
Purpose
foundations server rendered, hydrated in view spacing
the 4 px scale, as Tailwind emits it
borderRadius
the steps, plus `pill` from the config
sm
md
lg
xl
2xl
3xl
pill
colors
brand values read from @prima-modules/tokens
brand.purple
#6844C4
brand.orange
#BA573D
brand.cream
#FFF6F1
brand.beige
#FBF6F0
Shell tokens
ink
ink-60
ink-40
paper
line
purple-100
Semantic
success
warning
error
info
Heading
level 1 to 4
Heading 1
Heading 2
Heading 3
Heading 4
Text
size, weight, muted
Large text, for introductions
Regular text, default body copy
Small text, secondary information
Extra small, captions and labels
Normal weight (400)
Medium weight (500)
Semibold weight (600)
Bold weight (700)
Default text colour
Muted text colour
Primary colour text
Divider
variant, label, orientation, contrast
One React component library that every SPA page in the kit is assembled from, so a form on the commerce page and a form on the settings page are the same Input, the same Button and the same Card. The invariant that makes it trustworthy is that the atoms carry no visual value of their own: every colour, radius, shadow and font they render is a Tailwind class emitted from tailwind.config.js (S1, decision D1a), so a token change repaints the whole app and the gallery on the docs page, built from the same token file, repaints with it.
It makes no backend module visible; it is the vocabulary the other frontend recipes render with, and the /design-system page is its own rendering.
When to use
Use it for every surface: SPA rendering (decision D1b, 2026-09-11): a page inside the app shell composes atoms and molecules from the barrel src/components/alchemy-ui/index.ts and writes no button, input, badge or card of its own. Trigger phrases: "add a field to the settings page", "a table of orders", "a status chip", "a loading skeleton", "the toast after saving".
Do not use it on a Prima product surface (chat, portal, prima.li), which render in the prima system, or on an Astro client site (tokens-css, S3). Do not reach for src/components/design-system/: it was deleted on 2026-09-11 (commit fd105ee) and nothing imported it.
Stack assumptions
S1 for the token file, S2 for the shell the atoms render inside. Pinned as measured in alchemy_labs/frontend/package.json at fd8558f (2026-09-11): react ^19.1.1, typescript ^4.9.5, tailwindcss ^3.4.17, lucide-react ^0.563.0 (icons), @headlessui/react ^2.2.9 (the overlays the atoms do not cover), framer-motion ^12.23.24 (used by a few pages, not by the atoms). Class strings only; no CSS-in-JS, no CSS modules.
Data-fetch style: none. The atoms are presentational; DataTable takes rows and columns, Toast takes a message. Copy language of the library page is English (it is an internal showcase); the atoms render whatever copy the page passes, du-Form German on merchant pages.
Data model
No server state. Component props are the contract; the barrel exports them as types:
- Atoms (
atoms/index.ts, 19 modules):Button(variant: primary | secondary | outline | ghost,size: sm | md | lg,fullWidth,loading),Input,Heading(level1 to 4),Text(size,muted),Textarea,Select(SelectOption),MultiSelect,SmartSelect,RadioandRadioGroup,CheckboxandCheckboxGroup,Toggle,BadgeandStatusBadge,SelectCardsandMultiSelectCards(SelectableOption),AvatarandAvatarGroup,SpinnerplusSkeleton,SkeletonCard,SkeletonListItem,SkeletonProfile,SkeletonTable,SkeletonForm,SkeletonArticle,Divider,AlchemyLoader,AlchemyPageLoader,AlchemyInlineLoader,GlobalBackground,AZSymbol. - Molecules (
molecules/index.ts, 5 modules):CardwithCardHeader,CardContent,CardFooter;InputField(label, help, error aroundInput);TablewithTableHeader,TableBody,TableRow,TableHeadCell,TableCellandDataTable(Column);ScrollNavandFloatingScrollNav(ScrollNavItem);ToastProvideranduseToast(ToastType,ToastOptions). - The barrel also re-exports
ThemeProvideranduseThemefromcontexts/ThemeContext(index.tsline 15);useTheme().isDarkis alwaysfalse.
Client state: the demo page holds form state for its own specimens (useState at DesignSystemDemo.tsx lines 48 to 56) and a copiedCode id for the copy buttons. Browser storage: alchemy-theme (always light, written by ThemeProvider); nothing else.
API surface
None consumed. The page and the atoms make no request. The one build-time contract is virtual:tailwind-brand (DesignSystemDemo.tsx line 43 imports brand to print the swatch values), generated from tailwind.config.js by vite.config.ts lines 6 to 32.
Application logic
buttons server rendered, hydrated in view Button
variant x size x state; radius is a fixed ratio of height
Variants
Sizes (40, 48, 64 px high; 16, 20, 26 px radius)
States
fullWidth
- A page imports what it needs from
'../components/alchemy-ui'(145 importing files at fd8558f). - It composes:
Cardaround a block,InputFieldfor a labelled input,Buttonfor the action,DataTablefor rows,useToast()for the outcome of a write (ToastProvideris mounted once inApp.tsx; callinguseToastoutside it throws). - Visual variation is a prop (
variant,size,status), never aclassNamethat overrides colour.classNameexists on every atom for layout (w-full,mt-4), not for identity. - Loading, empty and error are rendered with the atoms:
Spinneror aSkeleton*while fetching, a sentence plus the primaryButtonwhen empty,StatusBadgeand the envelope's message when failed. /design-systemrenders every atom in every state on one public page so a change can be seen in one place.FloatingScrollNavbuilds its dots from the section ids.
Invariants
- No hex literal in an atom. At fd8558f
grep -rli "6844c4" src/components/alchemy-uiprints nothing; the primaryButtonisbg-brand-purpleandfocus:ring-brand-purple(Button.tsxlines 42 to 44). - Button radius is a fixed ratio of height, about 0.4085 (brand spec: 24 px radius at 58.75 px height):
smh-10 rounded-[16px],mdh-12 rounded-[20px],lgh-16 rounded-[26px](Button.tsxlines 31 to 38). A new size keeps the ratio; a new component that is a pill usesrounded-pillfrom the config. - The library is light-only.
dark:variants in the atoms never apply (ThemeContext.tsxline 29 removes.dark); no new ones are written. - One library.
components/alchemy-ui/is the only component set; a page that needs a component the library lacks adds an atom there, with a specimen on/design-system, rather than a page-local copy. - The
/design-systemroute stays public and data-free.
Frontend
The component, rebuilt from the demo in the kit with the design system the recipe names, one frame per state. What is in the frames is what the recipe describes and what Primacode builds from; it is not a picture of the running application.
inputs server rendered, hydrated in view Input
inputSize, error, disabled
This field is required
InputField
label, helperText, required, error around Input
Never shared
Already taken
Textarea
resize, inputSize
selection server rendered, hydrated in view Select
single value; the list is a portal
Pick one
MultiSelect
value: string[]
SmartSelect
searchable, clearable, loading
Radio, RadioGroup
RadioGroup takes options; Radio is the single control
Checkbox, CheckboxGroup
indeterminate, description, error
Required
Toggle
size, description
SelectCards, MultiSelectCards
variant: cards | chips | list
badges-avatars server rendered, hydrated in view Badge
variant, dot, size
Variants
With dot
Sizes
StatusBadge
one per status
Avatar, AvatarGroup
size, status, shape, max
Sizes
Status and shape
AvatarGroup, max 4 of 6
loaders server rendered, hydrated in view Spinner
size, color, label
AlchemyLoader, AlchemyInlineLoader
AlchemyPageLoader is the same loader as a fixed full-screen overlay and is not rendered here
Skeleton
variant text | circular | rectangular | rounded; lines
SkeletonCard, SkeletonProfile, SkeletonListItem, SkeletonForm, SkeletonTable, SkeletonArticle
SkeletonCard
SkeletonProfile
SkeletonListItem
SkeletonForm
SkeletonTable
SkeletonArticle
tables-cards server rendered, hydrated in view DataTable
columns with render, striped, hoverable, getRowKey, emptyMessage
Name | E-mail | Status | Role |
|---|---|---|---|
| Mira Keller | [email protected] | Active | Admin |
| Jonas Weber | [email protected] | Pending | Member |
| Lea Frei | [email protected] | Inactive | Member |
| Noah Brunner | [email protected] | Active | Editor |
Name |
|---|
| No people yet |
Table, TableHeader, TableBody, TableRow, TableHeadCell, TableCell
the composable half; size sm, selected row, sortable head
Name | Role | Status |
|---|---|---|
| Jonas Weber | Member | Pending |
| Lea Frei | Member | Inactive |
| Mira Keller | Admin | Active |
| Noah Brunner | Editor | Active |
Card, CardHeader, CardContent, CardFooter
variant default | glass; padding none to xl
Welcome back
Sign in to your account
Default variant
Solid card without the glass finish. Good for high-contrast content.
padding none
the content brings its own
ui-design-system/ui/alchemy-ui-component-library/demo.tsx
/**
* Live demo for ui-design-system/ui/alchemy-ui-component-library.
*
* The gallery: every atom and molecule the barrel exports, with its variants
* and states, grouped into eight frames and titled with its export name. A
* port of the sections of Labs' DesignSystemDemo.tsx (fd8558f, lines 199 to
* 832) without the page chrome (sticky header, theme toggle, FloatingScrollNav,
* the radial gradient). Swatch values are read from @prima-modules/tokens, the
* way the page reads `brand` from virtual:tailwind-brand.
*
* Not rendered: AZSymbol and GlobalBackground (agency brand atoms; a customer
* deletes the two export lines), AlchemyPageLoader (a fixed full-screen
* overlay) and FloatingScrollNav (fixed to the viewport edge); each is named in
* the frame that would hold it.
*/
import React, { useState } from 'react';
import { Rocket, Shield, Sparkles } from 'lucide-react';
import { defineStates } from '@prima-modules/demo';
import { brand } from '@prima-modules/tokens';
import {
Button,
Input,
Heading,
Text,
Textarea,
Select,
MultiSelect,
SmartSelect,
Radio,
RadioGroup,
Checkbox,
CheckboxGroup,
Toggle,
Badge,
StatusBadge,
SelectCards,
MultiSelectCards,
Avatar,
AvatarGroup,
Spinner,
Skeleton,
SkeletonCard,
SkeletonListItem,
SkeletonProfile,
SkeletonTable,
SkeletonForm,
SkeletonArticle,
Divider,
AlchemyLoader,
AlchemyInlineLoader,
Card,
CardHeader,
CardContent,
CardFooter,
InputField,
Table,
TableHeader,
TableBody,
TableRow,
TableHeadCell,
TableCell,
DataTable,
ScrollNav,
ToastProvider,
useToast,
ThemeProvider,
} from '@prima-modules/alchemy-ui';
/* ---- sample data (literal, nothing real) ---------------------------- */
const people = [
{ id: 1, name: 'Mira Keller', email: '[email protected]', status: 'active', role: 'Admin' },
{ id: 2, name: 'Jonas Weber', email: '[email protected]', status: 'pending', role: 'Member' },
{ id: 3, name: 'Lea Frei', email: '[email protected]', status: 'inactive', role: 'Member' },
{ id: 4, name: 'Noah Brunner', email: '[email protected]', status: 'active', role: 'Editor' },
];
type Person = (typeof people)[number];
const frameworks = [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
{ value: 'astro', label: 'Astro' },
{ value: 'svelte', label: 'Svelte' },
];
const countries = [
{ value: 'ch', label: 'Switzerland', description: 'Europe' },
{ value: 'de', label: 'Germany', description: 'Europe' },
{ value: 'at', label: 'Austria', description: 'Europe' },
{ value: 'jp', label: 'Japan', description: 'Asia' },
{ value: 'ca', label: 'Canada', description: 'North America' },
];
const channels = [
{ value: 'email', label: 'E-mail notifications' },
{ value: 'sms', label: 'SMS notifications' },
{ value: 'push', label: 'Push notifications' },
];
/* ---- the contract ------------------------------------------------- */
export type Section =
| 'foundations'
| 'buttons'
| 'inputs'
| 'selection'
| 'badges-avatars'
| 'loaders'
| 'tables-cards'
| 'navigation-toasts';
export interface Props {
section: Section;
}
export const states = defineStates<Props>({
foundations: { label: 'Foundations: spacing, radius, colours, Heading, Text, Divider', props: { section: 'foundations' }, height: 1500 },
buttons: { label: 'Button', props: { section: 'buttons' }, height: 520 },
inputs: { label: 'Input, InputField, Textarea', props: { section: 'inputs' }, height: 760 },
selection: { label: 'Select, MultiSelect, SmartSelect, Radio, Checkbox, Toggle, SelectCards', props: { section: 'selection' }, height: 1500 },
'badges-avatars': { label: 'Badge, StatusBadge, Avatar, AvatarGroup', props: { section: 'badges-avatars' }, height: 760 },
loaders: { label: 'Spinner, Skeleton, AlchemyLoader', props: { section: 'loaders' }, height: 1500 },
'tables-cards': { label: 'Table, DataTable, Card', props: { section: 'tables-cards' }, height: 1100 },
'navigation-toasts': { label: 'ScrollNav, ToastProvider and useToast', props: { section: 'navigation-toasts' }, height: 520 },
});
/* ---- scaffolding ---------------------------------------------------- */
/** One specimen block, titled with the export name it shows. */
function Specimen({ name, note, children }: { name: string; note?: string; children: React.ReactNode }) {
return (
<section className="space-y-4">
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<Heading level={3} className="!tracking-normal">
<code className="font-mono text-[0.9em]">{name}</code>
</Heading>
{note && (
<Text size="sm" muted>
{note}
</Text>
)}
</div>
<Card variant="glass" padding="lg">
{children}
</Card>
</section>
);
}
function Row({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<Text weight="semibold" className="mb-3">
{label}
</Text>
<div className="flex flex-wrap items-center gap-4">{children}</div>
</div>
);
}
/* ---- the eight frames ---------------------------------------------- */
function Foundations() {
return (
<>
<Specimen name="spacing" note="the 4 px scale, as Tailwind emits it">
<div className="space-y-3">
{[
{ key: '1', value: '0.25rem', px: '4px' },
{ key: '2', value: '0.5rem', px: '8px' },
{ key: '3', value: '0.75rem', px: '12px' },
{ key: '4', value: '1rem', px: '16px' },
{ key: '6', value: '1.5rem', px: '24px' },
{ key: '8', value: '2rem', px: '32px' },
{ key: '12', value: '3rem', px: '48px' },
{ key: '16', value: '4rem', px: '64px' },
].map((item) => (
<div key={item.key} className="flex items-center gap-4">
<div className="w-12 text-sm font-mono text-stone-500">{item.key}</div>
<div className="h-6 bg-brand-purple rounded" style={{ width: item.value }} />
<div className="text-sm font-mono text-zinc-700">
{item.value} <span className="text-stone-500">({item.px})</span>
</div>
</div>
))}
</div>
</Specimen>
<Specimen name="borderRadius" note="the steps, plus `pill` from the config">
<div className="flex flex-wrap gap-6">
{[
{ key: 'sm', value: '0.125rem' },
{ key: 'md', value: '0.375rem' },
{ key: 'lg', value: '0.5rem' },
{ key: 'xl', value: '0.75rem' },
{ key: '2xl', value: '1rem' },
{ key: '3xl', value: '1.5rem' },
{ key: 'pill', value: '9999px' },
].map((item) => (
<div key={item.key} className="flex flex-col items-center gap-2">
<div className="w-16 h-16 bg-brand-purple" style={{ borderRadius: item.value }} />
<Text size="xs" muted>
{item.key}
</Text>
</div>
))}
</div>
</Specimen>
<Specimen name="colors" note="brand values read from @prima-modules/tokens">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{(
[
['brand.purple', 'bg-brand-purple', brand.purple],
['brand.orange', 'bg-brand-orange', brand.orange],
['brand.cream', 'bg-brand-cream border border-line', brand.cream],
['brand.beige', 'bg-brand-beige border border-line', brand.beige],
] as const
).map(([name, cls, hex]) => (
<div key={name} className="space-y-2">
<div className={`h-20 rounded-xl ${cls}`} />
<Text size="sm" weight="medium">
{name}
</Text>
<Text size="xs" muted>
{hex}
</Text>
</div>
))}
</div>
<Divider className="my-6" />
<Text weight="semibold" className="mb-4">
Shell tokens
</Text>
<div className="grid grid-cols-3 md:grid-cols-6 gap-4">
{(
[
['ink', 'bg-ink'],
['ink-60', 'bg-ink-60'],
['ink-40', 'bg-ink-40'],
['paper', 'bg-paper border border-line'],
['line', 'bg-line'],
['purple-100', 'bg-purple-100'],
] as const
).map(([name, cls]) => (
<div key={name} className="space-y-2">
<div className={`h-12 rounded-lg ${cls}`} />
<Text size="xs" muted>
{name}
</Text>
</div>
))}
</div>
<Divider className="my-6" />
<Text weight="semibold" className="mb-4">
Semantic
</Text>
<div className="grid grid-cols-4 gap-4">
{(
[
['success', 'bg-success'],
['warning', 'bg-warning'],
['error', 'bg-error'],
['info', 'bg-info'],
] as const
).map(([name, cls]) => (
<div key={name} className="space-y-2">
<div className={`h-12 rounded-lg ${cls}`} />
<Text size="xs" muted>
{name}
</Text>
</div>
))}
</div>
</Specimen>
<Specimen name="Heading" note="level 1 to 4">
<div className="space-y-2">
<Heading level={1}>Heading 1</Heading>
<Heading level={2}>Heading 2</Heading>
<Heading level={3}>Heading 3</Heading>
<Heading level={4}>Heading 4</Heading>
</div>
</Specimen>
<Specimen name="Text" note="size, weight, muted">
<div className="space-y-3">
<Text size="lg">Large text, for introductions</Text>
<Text>Regular text, default body copy</Text>
<Text size="sm">Small text, secondary information</Text>
<Text size="xs">Extra small, captions and labels</Text>
<Divider />
<Text weight="normal">Normal weight (400)</Text>
<Text weight="medium">Medium weight (500)</Text>
<Text weight="semibold">Semibold weight (600)</Text>
<Text weight="bold">Bold weight (700)</Text>
<Divider />
<Text>Default text colour</Text>
<Text muted>Muted text colour</Text>
<Text className="text-brand-purple">Primary colour text</Text>
</div>
</Specimen>
<Specimen name="Divider" note="variant, label, orientation, contrast">
<div className="space-y-4">
<Divider />
<Divider variant="dashed" />
<Divider variant="dotted" />
<Divider label="OR" />
<Divider contrast="high" />
<div className="flex items-center gap-4 h-12">
<span>Left</span>
<Divider orientation="vertical" />
<span>Right</span>
</div>
</div>
</Specimen>
</>
);
}
function Buttons() {
return (
<Specimen name="Button" note="variant x size x state; radius is a fixed ratio of height">
<div className="space-y-8">
<Row label="Variants">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</Row>
<Row label="Sizes (40, 48, 64 px high; 16, 20, 26 px radius)">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</Row>
<Row label="States">
<Button>Default</Button>
<Button loading>Loading</Button>
<Button disabled>Disabled</Button>
</Row>
<Row label="fullWidth">
<Button fullWidth>Continue</Button>
</Row>
</div>
</Specimen>
);
}
function Inputs() {
return (
<div className="max-w-md space-y-10">
<Specimen name="Input" note="inputSize, error, disabled">
<div className="space-y-4">
<Input placeholder="Small input" inputSize="sm" />
<Input placeholder="Medium input" inputSize="md" />
<Input placeholder="Large input" inputSize="lg" />
<Input placeholder="Invalid input" error="This field is required" />
<Input placeholder="Disabled input" disabled />
</div>
</Specimen>
<Specimen name="InputField" note="label, helperText, required, error around Input">
<div className="space-y-4">
<InputField label="E-mail address" placeholder="[email protected]" type="email" helperText="Never shared" required />
<InputField label="Workspace name" placeholder="My workspace" error="Already taken" />
</div>
</Specimen>
<Specimen name="Textarea" note="resize, inputSize">
<div className="space-y-4">
<Textarea placeholder="Write your message here" />
<Textarea placeholder="Non-resizable" resize="none" inputSize="sm" />
</div>
</Specimen>
</div>
);
}
function Selection() {
const [selectValue, setSelectValue] = useState('');
const [multiValue, setMultiValue] = useState<string[]>(['react']);
const [smartValue, setSmartValue] = useState('');
const [radioValue, setRadioValue] = useState('option1');
const [checkboxValues, setCheckboxValues] = useState<string[]>(['email']);
const [toggleValue, setToggleValue] = useState(true);
const [cardValue, setCardValue] = useState('pro');
const [chipValue, setChipValue] = useState<string[]>(['react', 'astro']);
return (
<>
<Specimen name="Select" note="single value; the list is a portal">
<div className="max-w-md space-y-4">
<Select options={frameworks} value={selectValue} onChange={setSelectValue} placeholder="Choose a framework" />
<Select options={frameworks} value="vue" placeholder="Disabled" disabled />
<Select options={frameworks} value="" placeholder="With error" error="Pick one" />
</div>
</Specimen>
<Specimen name="MultiSelect" note="value: string[]">
<div className="max-w-md">
<MultiSelect options={frameworks} value={multiValue} onChange={setMultiValue} placeholder="Select frameworks" />
</div>
</Specimen>
<Specimen name="SmartSelect" note="searchable, clearable, loading">
<div className="max-w-md space-y-4">
<SmartSelect
options={countries}
value={smartValue}
onChange={(v) => setSmartValue(v as string)}
placeholder="Search for a country"
searchable
clearable
/>
<SmartSelect options={countries} value="" onChange={() => {}} placeholder="Loading" loading />
</div>
</Specimen>
<Specimen name="Radio, RadioGroup" note="RadioGroup takes options; Radio is the single control">
<div className="grid md:grid-cols-2 gap-8">
<RadioGroup
name="demo-radio"
options={[
{ value: 'option1', label: 'Option 1', description: 'The first option' },
{ value: 'option2', label: 'Option 2', description: 'The second option' },
{ value: 'option3', label: 'Option 3', description: 'Disabled', disabled: true },
]}
value={radioValue}
onChange={setRadioValue}
/>
<div className="space-y-3">
<Radio name="single" label="Small" size="sm" defaultChecked />
<Radio name="single" label="Medium" size="md" />
<Radio name="single" label="Large" size="lg" />
</div>
</div>
</Specimen>
<Specimen name="Checkbox, CheckboxGroup" note="indeterminate, description, error">
<div className="grid md:grid-cols-2 gap-8">
<CheckboxGroup options={channels} value={checkboxValues} onChange={setCheckboxValues} />
<div className="space-y-4">
<Checkbox label="I agree to the terms" description="By ticking this box you accept the terms" />
<Checkbox
label="Select all"
indeterminate={checkboxValues.length > 0 && checkboxValues.length < channels.length}
checked={checkboxValues.length === channels.length}
onChange={(checked) => setCheckboxValues(checked ? channels.map((o) => o.value) : [])}
/>
<Checkbox label="With error" error="Required" />
</div>
</div>
</Specimen>
<Specimen name="Toggle" note="size, description">
<div className="flex flex-wrap gap-8">
<Toggle label="Notifications" description="Receive e-mail notifications" checked={toggleValue} onChange={setToggleValue} />
<Toggle label="Small" size="sm" />
<Toggle label="Large" size="lg" defaultChecked />
<Toggle label="Disabled" disabled />
</div>
</Specimen>
<Specimen name="SelectCards, MultiSelectCards" note="variant: cards | chips | list">
<div className="space-y-10">
<SelectCards
options={[
{ value: 'starter', label: 'Starter', description: 'For small projects', icon: <Sparkles className="w-5 h-5" /> },
{ value: 'pro', label: 'Professional', description: 'For growing businesses', icon: <Rocket className="w-5 h-5" /> },
{ value: 'enterprise', label: 'Enterprise', description: 'Advanced features', icon: <Shield className="w-5 h-5" /> },
]}
value={cardValue}
onChange={setCardValue}
variant="cards"
columns={3}
/>
<MultiSelectCards options={[...frameworks, { value: 'next', label: 'Next.js' }]} value={chipValue} onChange={setChipValue} variant="chips" />
<div className="max-w-md">
<SelectCards
options={[
{ value: 'email', label: 'E-mail', description: 'Get updates by e-mail' },
{ value: 'sms', label: 'SMS', description: 'Receive text messages' },
{ value: 'push', label: 'Push', description: 'Browser notifications' },
]}
value="sms"
onChange={() => {}}
variant="list"
/>
</div>
</div>
</Specimen>
</>
);
}
function BadgesAvatars() {
return (
<>
<Specimen name="Badge" note="variant, dot, size">
<div className="space-y-6">
<Row label="Variants">
<Badge>Default</Badge>
<Badge variant="primary">Primary</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="error">Error</Badge>
<Badge variant="info">Info</Badge>
<Badge variant="outline">Outline</Badge>
</Row>
<Row label="With dot">
<Badge variant="success" dot>
Online
</Badge>
<Badge variant="warning" dot>
Away
</Badge>
<Badge variant="error" dot>
Offline
</Badge>
</Row>
<Row label="Sizes">
<Badge size="sm">Small</Badge>
<Badge size="md">Medium</Badge>
<Badge size="lg">Large</Badge>
</Row>
</div>
</Specimen>
<Specimen name="StatusBadge" note="one per status">
<div className="flex flex-wrap items-center gap-3">
{(['active', 'inactive', 'pending', 'completed', 'failed', 'draft', 'published', 'archived'] as const).map((s) => (
<StatusBadge key={s} status={s} />
))}
</div>
</Specimen>
<Specimen name="Avatar, AvatarGroup" note="size, status, shape, max">
<div className="space-y-6">
<Row label="Sizes">
{(['xs', 'sm', 'md', 'lg', 'xl', '2xl'] as const).map((s) => (
<Avatar key={s} name="Mira Keller" size={s} />
))}
</Row>
<Row label="Status and shape">
<Avatar name="Online" status="online" size="lg" />
<Avatar name="Away" status="away" size="lg" />
<Avatar name="Busy" status="busy" size="lg" />
<Avatar name="Offline" status="offline" size="lg" />
<Avatar name="Square" shape="square" size="lg" />
</Row>
<Row label="AvatarGroup, max 4 of 6">
<AvatarGroup avatars={people.map((p) => ({ name: p.name })).concat([{ name: 'Sara Meier' }, { name: 'Tim Huber' }])} max={4} size="md" />
</Row>
</div>
</Specimen>
</>
);
}
function Loaders() {
return (
<>
<Specimen name="Spinner" note="size, color, label">
<div className="flex items-center gap-6">
<Spinner size="xs" />
<Spinner size="sm" />
<Spinner size="md" />
<Spinner size="lg" />
<Spinner size="xl" />
<Spinner size="md" label="Loading" />
<span className="bg-ink rounded-lg p-3">
<Spinner size="md" color="white" />
</span>
</div>
</Specimen>
<Specimen name="AlchemyLoader, AlchemyInlineLoader" note="AlchemyPageLoader is the same loader as a fixed full-screen overlay and is not rendered here">
<div className="space-y-8">
<div className="flex items-end gap-8">
<AlchemyLoader size="sm" label="Small" />
<AlchemyLoader size="md" label="Medium" />
<AlchemyLoader size="lg" label="Large" />
<AlchemyLoader size="xl" label="Extra large" />
</div>
<Divider contrast="high" />
<div className="flex items-end gap-8">
<AlchemyLoader size="md" variant="bubble" label="Bubble" />
<AlchemyLoader size="md" variant="pulse" label="Pulse" />
<AlchemyLoader size="md" variant="bounce" label="Bounce" />
<AlchemyLoader size="md" variant="spin" label="Spin" />
</div>
<Divider contrast="high" />
<div className="flex items-center gap-6">
<AlchemyInlineLoader size="xs" />
<AlchemyInlineLoader size="sm" />
<AlchemyInlineLoader size="md" />
</div>
</div>
</Specimen>
<Specimen name="Skeleton" note="variant text | circular | rectangular | rounded; lines">
<div className="space-y-4 max-w-md">
<div className="flex items-center gap-4">
<Skeleton variant="circular" width={48} height={48} />
<div className="flex-1 space-y-2">
<Skeleton variant="text" width="60%" />
<Skeleton variant="text" width="40%" />
</div>
</div>
<Skeleton variant="rectangular" height={60} />
<Skeleton variant="rounded" height={100} />
<Skeleton variant="text" lines={3} />
</div>
</Specimen>
<Specimen name="SkeletonCard, SkeletonProfile, SkeletonListItem, SkeletonForm, SkeletonTable, SkeletonArticle">
<div className="grid md:grid-cols-2 gap-6">
<div>
<Text size="sm" muted className="mb-2">
SkeletonCard
</Text>
<SkeletonCard />
</div>
<div>
<Text size="sm" muted className="mb-2">
SkeletonProfile
</Text>
<SkeletonProfile />
</div>
<div>
<Text size="sm" muted className="mb-2">
SkeletonListItem
</Text>
<div className="border border-stone-200 rounded-xl">
<SkeletonListItem />
<SkeletonListItem />
<SkeletonListItem />
</div>
</div>
<div>
<Text size="sm" muted className="mb-2">
SkeletonForm
</Text>
<SkeletonForm fields={2} />
</div>
<div className="md:col-span-2">
<Text size="sm" muted className="mb-2">
SkeletonTable
</Text>
<SkeletonTable rows={3} />
</div>
<div className="md:col-span-2">
<Text size="sm" muted className="mb-2">
SkeletonArticle
</Text>
<SkeletonArticle />
</div>
</div>
</Specimen>
</>
);
}
function TablesCards() {
const [sort, setSort] = useState<'asc' | 'desc' | null>('asc');
const rows = [...people].sort((a, b) => (sort === 'desc' ? b.name.localeCompare(a.name) : a.name.localeCompare(b.name)));
return (
<>
<Specimen name="DataTable" note="columns with render, striped, hoverable, getRowKey, emptyMessage">
<div className="space-y-6" data-gallery-focus="true">
<DataTable<Person>
data={people}
columns={[
{ key: 'name', header: 'Name', sortable: true },
{ key: 'email', header: 'E-mail', sortable: true },
{ key: 'status', header: 'Status', render: (p) => <StatusBadge status={p.status as 'active' | 'pending' | 'inactive'} /> },
{ key: 'role', header: 'Role', align: 'right' },
]}
striped
hoverable
getRowKey={(p) => p.id}
/>
<DataTable<Person> data={[]} columns={[{ key: 'name', header: 'Name' }]} emptyMessage="No people yet" />
</div>
</Specimen>
<Specimen name="Table, TableHeader, TableBody, TableRow, TableHeadCell, TableCell" note="the composable half; size sm, selected row, sortable head">
<Table size="sm" hoverable>
<TableHeader>
<TableRow>
<TableHeadCell sortable sortDirection={sort} onSort={() => setSort(sort === 'asc' ? 'desc' : 'asc')}>
Name
</TableHeadCell>
<TableHeadCell>Role</TableHeadCell>
<TableHeadCell align="right" width={120}>
Status
</TableHeadCell>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((p, i) => (
<TableRow key={p.id} selected={i === 0}>
<TableCell>{p.name}</TableCell>
<TableCell>{p.role}</TableCell>
<TableCell align="right">
<StatusBadge status={p.status as 'active' | 'pending' | 'inactive'} size="sm" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Specimen>
<Specimen name="Card, CardHeader, CardContent, CardFooter" note="variant default | glass; padding none to xl">
<div className="grid md:grid-cols-2 gap-6">
<Card variant="glass" padding="xl">
<CardHeader title="Welcome back" subtitle="Sign in to your account" />
<CardContent className="mt-6">
<div className="space-y-4">
<Input placeholder="E-mail" type="email" />
<Input placeholder="Password" type="password" />
</div>
</CardContent>
<CardFooter className="mt-6">
<Button fullWidth>Continue</Button>
</CardFooter>
</Card>
<Card variant="default" padding="lg">
<Heading level={4} className="mb-4">
Default variant
</Heading>
<Text muted className="mb-6">
Solid card without the glass finish. Good for high-contrast content.
</Text>
<Button variant="outline">Learn more</Button>
</Card>
<Card variant="default" padding="none" className="md:col-span-2">
<CardHeader title="padding none" subtitle="the content brings its own" centered className="p-4" />
</Card>
</div>
</Specimen>
</>
);
}
function ToastButtons() {
const toast = useToast();
return (
<div className="flex flex-wrap gap-3">
<Button size="sm" onClick={() => toast.success('Saved')}>
success
</Button>
<Button size="sm" variant="outline" onClick={() => toast.error('Could not save', { duration: 6000 })}>
error
</Button>
<Button size="sm" variant="ghost" onClick={() => toast.info('Three items exported')}>
info
</Button>
<Button size="sm" variant="secondary" onClick={() => toast.showToast('Sticky until dismissed', 'info', { duration: 0 })}>
sticky
</Button>
</div>
);
}
function NavigationToasts() {
return (
<>
<Specimen name="ScrollNav" note="FloatingScrollNav is the same list fixed to the viewport edge and is not rendered here">
<div className="space-y-4">
<ScrollNav
items={[
{ id: 'demo-a', label: 'Overview' },
{ id: 'demo-b', label: 'Details' },
{ id: 'demo-c', label: 'History' },
]}
/>
<div className="grid grid-cols-3 gap-3">
{['demo-a', 'demo-b', 'demo-c'].map((id) => (
<div key={id} id={id} className="rounded-xl border border-line bg-white p-4">
<Text size="sm" muted>
#{id}
</Text>
</div>
))}
</div>
</div>
</Specimen>
<Specimen name="ToastProvider, useToast" note="the toasts stack at the bottom right of the viewport; error toasts stay 6 s, the rest 4 s, 0 is sticky">
<ToastProvider>
<ToastButtons />
</ToastProvider>
</Specimen>
</>
);
}
/* ---- the component -------------------------------------------------- */
export default function Demo({ section }: Props) {
return (
<ThemeProvider>
<div className="min-h-full bg-stone-100 text-ink font-sans">
<div className="max-w-5xl mx-auto px-6 py-8 space-y-12">
{section === 'foundations' && <Foundations />}
{section === 'buttons' && <Buttons />}
{section === 'inputs' && <Inputs />}
{section === 'selection' && <Selection />}
{section === 'badges-avatars' && <BadgesAvatars />}
{section === 'loaders' && <Loaders />}
{section === 'tables-cards' && <TablesCards />}
{section === 'navigation-toasts' && <NavigationToasts />}
</div>
</div>
</ThemeProvider>
);
}
Component tree, from the recipe
<Route /design-system> (public, no PrivateRoute, App.tsx line 216)
DesignSystemDemo
div.min-h-screen.bg-stone-100 (fixed radial-gradient overlay in brand purple)
FloatingScrollNav (position=right, one dot per section id)
header.sticky (Heading level=3 "Alchemy UI", Text "Design System", theme toggle: no-op)
main.max-w-5xl
section#hero Heading level=1, Text, Button primary + outline
section#spacing the 4 px scale, one bar per step
section#radius the radius steps
section#colors swatches read from `brand` (virtual:tailwind-brand)
section#typography Heading levels 1 to 4, Text sizes
section#buttons Button x variants (primary, secondary, outline, ghost) x sizes (sm, md, lg) x loading
section#inputs Input, InputField (label, help, error)
section#textarea Textarea
section#selects Select, MultiSelect, SmartSelect
section#radio-checkbox RadioGroup, Checkbox, CheckboxGroup
section#toggle Toggle
section#select-cards SelectCards, MultiSelectCards
section#badges Badge, StatusBadge
section#avatars Avatar, AvatarGroup
section#loaders Spinner, Skeleton x 6 variants, AlchemyLoader
section#dividers Divider
section#tables DataTable
section#cards Card > CardHeader, CardContent, CardFooter The rendering is the live gallery alchemy-ui-component-library/demo.tsx, built from @prima-modules/alchemy-ui (the atoms and molecules extracted verbatim from fd8558f) and @prima-modules/tokens (the preset generated from tailwind.config.js at the same commit), rendered by the docs host as one frame per state (RENDERING.md, section 3). The tree in the frontmatter describes the Labs page at /design-system; the demo is that page's sections without its chrome (no sticky header, no theme toggle, no FloatingScrollNav, no radial gradient), one section per state, so the docs page shows eight labelled frames instead of one picture whose fold hid most of them.
Every export of the barrel appears under its export name: foundations (spacing, radius, the colour swatches read from @prima-modules/tokens, Heading, Text, Divider), buttons (Button in every variant, size and state), inputs (Input, InputField, Textarea), selection (Select, MultiSelect, SmartSelect, Radio and RadioGroup, Checkbox and CheckboxGroup, Toggle, SelectCards and MultiSelectCards), badges-avatars (Badge, StatusBadge in all eight statuses, Avatar, AvatarGroup), loaders (Spinner, Skeleton and the six composed skeletons, AlchemyLoader, AlchemyInlineLoader), tables-cards (DataTable populated and empty, the composable Table* set, Card with CardHeader, CardContent, CardFooter) and navigation-toasts (ScrollNav, ToastProvider with useToast behind four buttons). Not rendered: AZSymbol and GlobalBackground (agency brand atoms, named in the demo's header; a customer deletes the two export lines), AlchemyPageLoader and FloatingScrollNav (fixed to the viewport, they would escape the frame; each is named in the frame that would hold it). ThemeProvider wraps every frame as it wraps the Labs page; isDark is false. Sample data is fictional; nothing is fetched.
Tokens the page uses, all from tailwind.config.js: brand.purple (buttons, active dots, swatches), stone-100 ground with a radial gradient in the purple's rgb, ink and stone-* for text, shadow-lift on cards, rounded-[16px|20px|26px] on buttons, rounded-pill on chips, font-sans (ES Rebond Grotesque). At 390 px each frame is one column; buttons keep their height and radius.
States rendered: every Button variant in default, loading and disabled; Input in three sizes, with a value, in error and disabled; Skeleton in four shapes plus the six composed patterns; Spinner in five sizes; StatusBadge in each status. Hover, focus, the open menus of the selects and the toasts are live in the frames (hydrate: visible).
Security & tenancy
Nothing here holds data. The route is public by design (App.tsx line 216); it renders no user, no workspace, no key. The theme toggle in the page header calls toggleTheme, which is a no-op, so the button is decorative (see Gotchas). Surface gating does not apply; the page renders for every build surface.
Tests
- No colocated unit test for the atoms at fd8558f (measured:
ls src/components/alchemy-ui/**/__tests__finds nothing). The render check is the page itself plusnpm run build(tsc --noEmit && vite build), which passed on fd105ee and fd8558f with vitest at 160 passed and the one pre-existingApp.test.tsxfailure. - What a new app adds: one vitest file per atom that renders each variant with
renderWithProviders(S2src/test-utils/index.tsx) and asserts the class the variant maps to (bg-brand-purpleforprimary), and one jsdom test foruseToastasserting it throws outsideToastProvider. - The Playwright gate does not visit
/design-system(measured: none of the fivefrontend/e2e/*.spec.tsnames it). In this kit the render smoke (node tooling/verify/render-smoke.mjs) renders every state of the demo withrenderToStringand asserts no throw and non-empty output;tooling/verify/compare.mjsagainst/design-systemis the visual check, run by the verifier and never committed.
Deploy & ops
Ships inside the Labs bundle (S2 Deploy & ops): vite build with the Tailwind content globs purging unused classes. Nothing to configure; no env. A token used only inside a computed class string is purged by Tailwind and renders unstyled: name it in safelist or write the full class in source. Rollback is the bundle's rollback.
Gotchas
navigation-toasts server rendered, hydrated in view ScrollNav
FloatingScrollNav is the same list fixed to the viewport edge and is not rendered here
#demo-a
#demo-b
#demo-c
ToastProvider, useToast
the toasts stack at the bottom right of the viewport; error toasts stay 6 s, the rest 4 s, 0 is sticky
- 2026-09-11 (measured at b1089b6, fixed at fd8558f): the atoms hard-coded the brand purple as arbitrary values (
bg-[#6844C4]) in 130 files;tokens.tsin the library folder had zero consumers and a different shade scale. D1a deletedtokens.ts(and theindex.tsline 5 re-export that made "zero consumers" true only of the values) and replaced the literals withbg-brand-purpleand siblings. Nothing visibly changed (built CSS diffed rule by rule). - 2026-09-11 (measured at fd8558f):
Button.tsxprimary still hovers tobg-[#6359c7](line 43) and its deaddark:hover to#8a7de8; neither is a brand token. Name abrand.purple-hoverin the config before touching it, or accept the literal as the dated exception it is. - 2026-09-11 (measured): the page header's theme toggle renders a moon icon and calls a no-op;
isDarkis alwaysfalse. The dark specimens the code path describes (bg-zinc-900, the darker gradient) are unreachable. A customer copying the page removes the toggle. - 2026-09-11 (measured):
AZSymbolandGlobalBackgroundare agency brand atoms (the mark and the ambient background). A customer build swaps them for its own mark or deletes the specimen; they are in the barrel so the delete is one export line. - 2026-09-11 (measured,
DesignSystemDemo.tsxline 154): the page ground isbg-stone-100, not the shell'sbg-paper; the showcase predates the shell tokens. Inside the shell every page usespaper. - 2026-09-11 (measured): the sidecar of the first harness capture of this page said
authenticated: truebecause the harness counted the consent entry as a session;shot.mjswas corrected the same day. The captures themselves were deleted on 2026-09-11 when the rendering became the live demo (RENDERING.md); the harness stays as the verifier. - 2026-09-11 (rendering trap):
ScrollNavreadswindow.scrollYanddocument.getElementByIdin an effect,SelectandSmartSelectportal their menus todocument.body, andToastProviderstacks its toasts fixed at the bottom right of whatever page hosts it. All three are fine underhydrate: visible(the effects run after hydration) and none may run at module top level, which the gate checks by importing the demo in Node.
Reference implementation
Branch that ships: alchemy_labs feat/service-desk at b1089b6 with the D1a commits on top on d1a-token-source (fd105ee, fd8558f, 2026-09-11); the branch is the one this recipe was measured on and is expected to merge to the shipping line. Paths relative to frontend/:
src/components/alchemy-ui/index.ts(barrel,ThemeProviderre-export at 15),atoms/index.ts(19 exports),molecules/index.ts(5 exports).src/components/alchemy-ui/atoms/Button.tsx: props (4 to 10), base styles (23 to 29), the size and radius table (31 to 38), variants (40 to 70).src/components/alchemy-ui/molecules/Toast.tsx:ToastProvider,useToast;molecules/Table.tsx:Table*andDataTable.src/pages/DesignSystemDemo.tsx(843 lines): imports (4 to 43), state (48 to 56), page chrome (153 to 196), the sections (199 to 832).src/App.tsxline 216:<Route path="/design-system" element={<DesignSystemDemo />} />, outsidePrivateRoute.tailwind.config.js: the token source (S1).
Deviations of the reference from this recipe, dated 2026-09-11: the two non-token hover literals in Button.tsx; dead dark: variants throughout the atoms; the decorative theme toggle; no unit tests for the atoms; stone-100 ground on the showcase page; one pixel-literal font size in the atoms (text-[10px] for the xs avatar, Avatar.tsx line 123) that S1 rule 1 forbids.
Acceptance checks
From alchemy_labs/frontend at fd8558f, or from a new app built to this recipe:
grep -rli "6844c4" src/components/alchemy-ui | wc -lprints0;grep -c "bg-brand-purple" src/components/alchemy-ui/atoms/Button.tsxprints2(the light and the dead dark variant).grep -c "export" src/components/alchemy-ui/atoms/index.tsprints19;grep -c "^export" src/components/alchemy-ui/molecules/index.tsprints5.npm run buildexits 0.- Browser walk-through: open
/design-systemsigned out; the page renders with no redirect to/login; the floating dots on the right jump to each section; in the inspector the three buttons of the "Buttons" section measure 40, 48 and 64 px high with radii 16, 20 and 26 px; the header's moon button changes nothing;document.documentElement.classList.contains('dark')isfalse. - Demo gate:
npm run checkprints no line for this recipe (the demo exists, exports the eight states in the frontmatter's order, and carries noscreenshotkey);node tooling/verify/render-smoke.mjs ui-design-system/ui/alchemy-ui-component-libraryprintsokfor all eight states and0 failures. - Verifier: with the docs index build and the Labs dev server running,
node tooling/verify/compare.mjs ui-design-system/ui/alchemy-ui-component-library --state buttons --docs http://127.0.0.1:4321 --app http://127.0.0.1:3020 --session-file tooling/verify/.labs-local/anon.jsonwritestooling/verify/out/ui-design-system--alchemy-ui-component-library/buttons/report.jsonwithpage_errors: []; themismatch_ratiois read by the verifier for the Buttons section only (the app page's animated gradient and the sections above the fold are excluded by construction, since the frame is one section), and the report is never committed.
Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14. The recipe file is the source; this page is a rendering of it.