A React component library where the consumer's config is the design system.
The library ships mechanisms. Your config file makes the decisions: which colors, sizes, radii, variants, spacing, text styles and densities exist. Every key you define is at the same time
- a valid prop value:
<Button size="talla-xl" />compiles only iftalla-xlis in your config, and - a rendered style: the plugin generates CSS for it at build time.
No codegen, no module augmentation, no provider. One file.
// src/yarcl.config.ts
import { defineConfig } from '@yarcl/react/define';
import defaults from '@yarcl/react/defaults';
export default defineConfig({
...defaults,
colors: {
ink: { light: '#1c1917', dark: '#f5f5f4' },
clay: { light: '#a4441f', dark: '#f0a07a' },
},
sizes: {
'talla-s': { height: '2.25rem', paddingX: '0.875rem', fontSize: '0.75rem', iconSize: '0.875rem' },
'talla-m': { height: '2.75rem', paddingX: '1.25rem', fontSize: '0.8125rem', iconSize: '1rem' },
},
defaults: { ...defaults.defaults, size: 'talla-m', color: 'ink' },
});<Button size="talla-m" color="clay" /> // ✓
<Button size="md" /> // ✗ Type '"md"' is not assignable to type '"talla-s" | "talla-m"'as const+keyof typeof:defineConfigkeeps literal types, and the library derivesSize,Color,Variant, … from them.- A module alias: the library imports
@yarcl/config. The build plugin points it at your config file at runtime, and a matchingpathsentry in yourtsconfig.jsondoes the same for types. If your file doesn't exist, both fall back to the library defaults. - Build-time CSS: the plugin loads your config and serves
@yarcl/react/styles.css: CSS variables on:rootplus oneyarcl-{group}-{key}class per key. Components only set class names; no inline styles, nothing computed at runtime. Editing the config hot-reloads the CSS.
Why an alias instead of module augmentation or codegen: see ADR 0001.
From an existing React project using Vite, webpack, Rspack, Rollup or esbuild:
npx @yarcl/react initThis installs the package, detects the build tool, connects it and TypeScript to the same config, and creates src/yarcl.config.ts. For manual Vite setup:
npm install @yarcl/react// vite.config.ts
import react from '@vitejs/plugin-react';
import yarcl from '@yarcl/react/vite';
export default defineConfig({ plugins: [react(), yarcl({ config: 'src/yarcl.config.ts' })] });Use the matching @yarcl/react/webpack, /rspack, /rollup or /esbuild entry point for another build tool. See the installation guide for complete examples.
| Group | Keys | Used by |
|---|---|---|
colors |
open, { light, dark, on? } → light-dark() |
color prop everywhere |
neutrals |
bg, surface, text, muted, border + any |
surfaces, text, borders |
sizes |
open, { height, paddingX, fontSize, iconSize } |
every control; same size = same height |
radii |
open; conventionally sm, md, lg, xl plus exceptions (square, rounded) |
radius prop; defaults.radius (e.g. md) applies to every component |
variants |
open recipes: background / border / text |
variant prop (Button, Badge, Alert, …) |
spacing |
open | gap, padding |
shadows |
open | shadow prop, floating panels |
density |
open, { paddingX, paddingY, fontSize } |
Table |
modalSizes |
open, widths | Dialog and Drawer size |
typography |
fontFaces, families, fonts, styles (open), headings h1–h6 |
Text, Heading, labels |
zIndex, motion, borders |
required keys + any | layering, transitions |
focusRing, defaults |
references to keys above | focus outline, omitted props |
components |
per-component defaults, e.g. { Button: { radius: 'square' } } |
omitted props, before defaults |
A prop resolves as: the prop → an enclosing group (ButtonGroup, RadioGroup, ToggleGroup) → components.<Name> → defaults.
radii: { square: '0', sm: '0.25rem', md: '0.375rem', lg: '0.5rem', rounded: '9999px' },
defaults: { radius: 'md', … }, // every component: md corners
components: { Button: { radius: 'square' } }, // except buttons: always sharpdefineConfig checks at compile time that every color has both modes, that references (defaults, components, headings, focusRing.color, text style families) point at existing keys, that components only names known components and props they have, and that required keys exist. The plugin warns when a color's foreground fails WCAG AA contrast.
Input accepts startContent and endContent for icons, text and buttons inside its border. Padding belongs to the editable input; slot content controls its own spacing. Direct buttons have square inner corners. Native form attributes and refs target the input; className and style target the outer control.
Pagination supports numbered pages with ellipses and layout="compact" for a live page summary with previous and next buttons at the end.
Controls: Button, IconButton, ButtonGroup, SplitButton, ToggleGroup, Input, NumberInput, ColorPicker, Textarea, Checkbox, Radio, RadioGroup, Switch, Slider, Select, Combobox, DatePicker, Field, Label Navigation: NavItem, NavSection, TreeView, Stepper
Typography & layout: Text, Heading, Link, AppLayout, SplitPane, Stack, Inline, Grid, Card, Divider, VisuallyHidden Floating: Tooltip, HoverCard, Popover, Menu Overlays: Dialog, Drawer, CommandPalette, Toast Data & navigation: Avatar, AvatarGroup, Tabs, Accordion, Table, Pagination, Breadcrumb
Table includes typed column resizing and visibility controls through useTableColumns and native compound parts.
Feedback: Badge, Alert, Spinner, Skeleton, Progress, EmptyState
Reference: DesignReference from @yarcl/react/reference renders your whole design system from your config.
Define component recipes in the config with typed token references, slots, variants and defaults:
export default defineConfig(defaults, (yarcl) => ({
recipes: {
Action: {
slots: { root: { borderRadius: yarcl.radii.rounded } },
variants: {
emphasis: { strong: { root: { fontWeight: 700 } } },
},
defaults: { emphasis: 'strong' },
},
},
}));import { Button, createComponent } from '@yarcl/react';
const Action = createComponent('Action', Button);
<Action emphasis="strong">Save changes</Action>The plugin generates CSS and the recipe adds typed props while preserving Button's behavior.
Use custom markup with named slots or useRecipe for your own behavior. See the
component extension guide.
@yarcl/react/themes ships nine themes that share the library defaults' keys, so they're interchangeable: Brutalist, Bloom, Compact, Editorial, Cyberpunk, Vermeer, Art Deco, Circuit and Studio. Cyberpunk uses neon accents and luminous panels; Vermeer pairs blue and ochre with full serif typography; Art Deco uses geometric type and gold double frames; Circuit combines cyan and navy with monospace labels; Studio uses heavy typography, large controls and cobalt with citrus accents. Every theme includes light and dark modes.
export { editorial as default } from '@yarcl/react/themes';@yarcl/react/css exports generateCss, the plugin's generator, for switching themes at runtime. The docs include a theme playground: a full dashboard you can re-theme live.
| Path | What |
|---|---|
library/ |
the @yarcl/react package: components, defineConfig, build plugins, CSS generator |
consumer/ |
demo app with one design system; every component, light and dark |
docs-web/ |
documentation site (Astro + Starlight) with live examples and a config playground |
e2e/ |
browser tests; e2e/consumer/ is a fixture app with completely different keys ("Maison Talla"), proving the types come from each app's own config |
docs/decisions/ |
architecture decision records |
pnpm install
pnpm dev # consumer on :5173
pnpm typecheck # library + both consumers, including @ts-expect-error contract checks
pnpm size # gzip budgets per package entry and typical component imports
pnpm test # Vitest browser mode in headless Chrome: behavior, styling and axe audits, light and dark
pnpm test:visual # screenshot comparisons in the pinned Playwright Linux image, requires Docker
pnpm docs:dev # documentation site on :4321
pnpm docs:build # static docs site → docs-web/dist
pnpm docs:api # API reference from JSDoc → docs/apiThe consumer's Bundled themes view and the full theme playground have screenshot baselines for every theme in both color schemes, at desktop and mobile widths. See visual regression tests for filtering runs, reviewing diffs and explicitly updating baselines.
- One config per build. Two brands in one bundle would need a Provider for the runtime values.
- Types are a development-time guarantee. Keep config changes and deploys in the same build.
Spacing, layout alignment and Grid columns accept typed maps of your configured breakpoints. CSS applies them while form state and focus stay in place.
<Stack gap={{ base: 'sm', lg: 'lg' }}>
<Grid columns={{ base: 1, md: 2, lg: 4 }}>
<Card padding={{ base: 'sm', lg: 'lg' }}>Overview</Card>
</Grid>
</Stack>See the responsive props guide for supported props, defaults and custom breakpoint names.