# yarcl
> React component library where the consumer's config file is the design system. Every key in the config becomes a typed prop value and a generated CSS class. No codegen, no provider.
## Why yarcl exists
yarcl was created because design systems and component libraries disagree about who owns the contract. Most libraries decide the options: buttons come in `sm`, `md` and `lg`, colors are `primary` and `secondary`. When your design system has different names or values, you either live with the library's choices, pass free-form strings and lose type safety, or maintain a second list of types by hand that drifts from the real values.
yarcl inverts that: the consumer's config defines the sizes, colors, variants, spacing and text styles, and the library derives both its TypeScript API and its CSS from that one file. The library ships mechanisms (buttons, inputs, dialogs, menus); the config makes the decisions.
Benefits:
- **Type-safe design tokens.** `` compiles only if `xl` is in your config. Removing a key turns every stale usage into a compile error.
- **One source of truth.** The same file drives the prop types and the generated CSS, so they can't drift apart.
- **No runtime styling cost.** CSS is generated at build time: no inline styles, no CSS-in-JS, no provider. It's CSP- and SSR-safe.
- **Themeable without code changes.** Light and dark mode from `{ light, dark }` pairs, per-component defaults in the config, bundled themes, and `applyTheme` for runtime switching.
- **Accessible by default.** Keyboard support, ARIA wiring and computed readable text colors, checked with axe.
Trade-off: one design system per build.
## Install
Requires React 19+. Build plugins are available for Vite, webpack, Rspack, Rollup and esbuild.
```sh
npx @yarcl/react init
```
This installs the package, updates the detected build config and TypeScript, and creates `src/yarcl.config.ts`. Use `pnpm dlx @yarcl/react init`, `yarn dlx @yarcl/react init` or `bunx @yarcl/react init` with those package managers. Pass `--bundler ` when more than one supported build config exists.
For manual setup, all three wiring steps are required. This example uses Vite; other build tools use `@yarcl/react/webpack`, `/rspack`, `/rollup` or `/esbuild`.
1. `vite.config.ts`:
```ts
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import yarcl from '@yarcl/react/vite';
export default defineConfig({ plugins: [react(), yarcl({ config: 'src/yarcl.config.ts' })] });
```
2. `tsconfig.json`, so types come from the same file:
```json
{
"compilerOptions": {
"paths": {
"@yarcl/config": ["./src/yarcl.config.ts", "./node_modules/@yarcl/react/dist/yarcl.config.d.ts"]
}
}
}
```
3. `src/yarcl.config.ts`:
```ts
import { defineConfig } from '@yarcl/react/define';
import defaults from '@yarcl/react/defaults';
export default defineConfig({
...defaults,
colors: { ...defaults.colors, brand: { light: '#4f46e5', dark: '#a5b4fc' } },
defaults: { ...defaults.defaults, color: 'brand' },
});
```
Then use components:
```tsx
import { Button, Field, Input } from '@yarcl/react';
```
Importing from `@yarcl/react` also loads the generated CSS and the component styles; there is no stylesheet to import. Editing the config hot-reloads the CSS.
All yarcl styles use ordered layers: `yarcl.tokens` (generated variables, modifiers, typography and font faces), `yarcl.base` (components and reference), then `yarcl.recipes` (built-in part styles and extensions). Component rules preserve size props, automatic table text sizing and invalid-state colors; recipes can override both layers. Unlayered consumer CSS overrides normal yarcl declarations regardless of specificity or import order, including after `applyTheme`. Declare external layer order before importing libraries: `@layer reset, yarcl, app;`, or with Tailwind 4 `@layer theme, base, yarcl, components, utilities;`. Keep resets layered below yarcl to preserve focus rings and dialog centering. For Starlight, put `@layer starlight, yarcl;` first in its first custom stylesheet. Use `not-content` around component previews. See https://yarcl.dev/configuration/css-layers/.
Add `class="yarcl-root"` to the body, or `className="yarcl-root"` to an app root or section, to opt into theme-driven page styles. It uses `neutrals.bg`, `neutrals.text`, the `typography.fonts.body` font role, and size, weight, line height and letter spacing from `defaults.textStyle`. `applyTheme` updates them at runtime. It inherits `color-scheme`, preserving an ancestor's forced light or dark mode; you can also set `style={{ colorScheme: 'dark' }}` on the root itself. Page background and typography stay consumer-owned without this class. Keep layout and margins in your own CSS.
## How the build plugin and tsconfig work together
The library imports a placeholder module, `@yarcl/config`. Two pieces of wiring point it at your config file, one for runtime and one for types:
- **Build plugin** (`yarcl()` from `@yarcl/react/vite`, `/webpack`, `/rspack`, `/rollup` or `/esbuild`):
- aliases `@yarcl/config` to your config file, so components read your values at runtime
- loads the config at build time and generates the virtual stylesheet (`@yarcl/react/styles.css`): CSS variables and one class per key
- emits `yarcl.tokens.css` in the build output directory by default, containing `:root` variables, `color-scheme` and `.yarcl-type-{key}` text-style classes; set `emitTokens: false` or `emitTokens: ''` to disable, or use a string for a custom output-relative filename
- with Vite, also writes this file to `build.outDir` on dev-server startup and updates it when the config or its local imports change, so a separate backend can serve it during development
- regenerates the CSS when the config or its imports change
- verifies that TypeScript resolves `@yarcl/config` to the same file and reports a clear error when it does not
- option `config`: path to the config file, relative to the project root; default `'src/yarcl.config.ts'`. If the file doesn't exist, it falls back to the library defaults.
- **tsconfig `paths`**: makes TypeScript resolve `@yarcl/config` to the same file, so prop types are the keys of your config. The second entry is a fallback to the library defaults.
- Both must point at the same file. The plugin stops startup if they differ. Re-run `npx @yarcl/react init` to repair the mapping.
- With Vite's React template, the app's compiler options live in `tsconfig.app.json`: put `paths` there, not in the root `tsconfig.json` that only has `references`. `paths` resolve relative to the tsconfig file; no `baseUrl` needed.
- Tests that run through a build tool need the matching plugin too. With Vitest, add the Vite adapter or reuse the Vite config with `mergeConfig`.
## Configuring
- Token props (`size`, `color`, `radius`, `variant`, `gap`, `padding`, `shadow`, `textStyle`, `density`) accept only keys of the project's config. Read the project's `src/yarcl.config.ts` before picking values; don't assume `md` or `primary` exist.
- `Type '"md"' is not assignable to ...` means that key isn't in the config: use an existing key or add it.
- To add a size, color, variant, spacing step or text style, add a key to its config group. It becomes a valid prop and a rendered style with no other changes.
- Open groups (any keys): `colors`, `sizes`, `radii`, `variants`, `spacing`, `shadows`, `density`, `modalSizes`, `typography.styles`. Groups with required keys: `neutrals`, `zIndex`, `motion`, `timing`, `borders`.
- Colors are `{ light, dark }` pairs; dark mode follows `color-scheme`. Readable text colors are computed.
- `typography.fonts` is optional. Without it, the `body` and `heading` roles use the `sans` family when present, and the `mono` role uses the `mono` family when present. Each otherwise uses the first configured family.
- `Button` and `IconButton` render native anchors when given `href`, preserving link semantics and anchor attributes for router navigation. Without `href`, they render native buttons. Link forms do not accept button-only props such as `disabled`, `loading`, or `type`.
- `Text` without an explicit text style follows a table cell's density font size, so `muted` and `truncate` work alongside plain cell text. Pass `inherit` to use the surrounding font in any container. An explicit `textStyle` retains its configured style.
- `Input` accepts `startContent` and `endContent` for text, icons or buttons inside its shared border. `className` and `style` target the outer control; native form attributes and refs target the input. Mark decorative icons `aria-hidden` and label icon buttons. Padding belongs to the editable input; slot content controls its own spacing. Direct buttons attach with square inner corners and the input's outer corners. Supplied actions keep their own disabled state and focus rings.
- `Button` and `Badge` accept `startIcon` and `endIcon` for decorative icons beside their labels. The icon size comes from the selected size's `iconSize`, and spacing follows that size's padding. `Button` hides these icons while loading.
- `Button autoHeight` grows to fit multiline content, retaining the selected size's minimum height and token spacing. Compose it with an `Avatar` and a `Stack` of name and email for an account menu trigger.
- `Badge` truncates long labels with an ellipsis within its container, preserving icons and remove buttons. Use `wrap` for multiline labels.
- `Pagination layout="compact"` shows a live page summary and previous/next buttons at the inline end, filling its available width. The default `layout="numbered"` shows numbered pages with ellipses. `summaryLabel(page, count)` customizes the compact text; no pages displays page 0. Both layouts share configured button styling and retain focus on unavailable boundary controls.
- `SplitButton` executes the selected option through `onAction(value)`. Its separate arrow menu changes `value` through `onValueChange` without executing. Options have `value`, `label`, optional decorative `icon` and `disabled`. Both segments follow `components.SplitButton`; menu items follow `components.Menu`. Native attributes and refs target the outer group.
- `Menu.RadioGroup` takes a controlled `value` and `onValueChange`; `Menu.RadioItem value="..."` renders `menuitemradio`, `aria-checked`, and a check indicator. Name the group with `aria-label` or `aria-labelledby`. Selecting a radio closes the menu and restores trigger focus.
- Single `Combobox` initializes its text from the selected value and follows value and label changes, including arriving options. Typed searches survive option updates until selection, blur or dismissal; changing the selected value resets the search. Controlled `inputValue` takes precedence.
- `FileDropzone` defaults to a single-file picker with Replace and Remove controls. Use `value` and `onChange(file | null)` for a controlled cover image, `preview` for a selected image thumbnail, and `previewUrl` plus `previewName` for an existing remote image. `multiple` keeps the file list and `onFilesChange` array callback. In multi-file mode use `files` plus `onFilesChange` for a controlled list, `defaultFiles` for an initial list, and `files={[]}` for upload-on-select. `inputRef` exposes the native file input for an external button to call `.click()`; the root `ref` remains a div. The input resets after selection so the same file can be picked again. Local `labels` customize picker and drop copy. See https://yarcl.dev/components/forms/file-dropzone/.
- Wrap sanitized HTML or an editor surface in `.yarcl-prose` to style headings, paragraphs, lists, quotes, code, links, tables, images and rules. It follows `typography.headings`; `typography.prose` optionally selects body and code text styles and spacing keys. Each text style always emits a `-letter-spacing` variable, defaulting to `normal`, and heading levels emit `--yarcl-h1-*` through `--yarcl-h6-*` variables.
- Built-in text lives in the required `labels` catalog. Spread `defaults.labels` to translate selected entries; keys are fixed and message formatters receive names or numeric counts. `useLabels()` reads the active typed catalog and re-renders after `applyTheme({ ...currentConfig, labels: translatedCatalog })`; `resetTheme()` restores build-time copy. Explicit instance label/message props win, including empty strings and null ReactNode messages. Dialog/Drawer use `closeLabel`, Alert and toast entries use `dismissLabel`, and FileDropzone accepts a partial `labels` object. DatePicker's `locale` separately controls calendar/date formatting. Visible shortcut modifier labels may be translated while `aria-keyshortcuts` keeps standard tokens. See https://yarcl.dev/configuration/labels/.
- Built-in part styles go in `components..slots`, checked by `defineConfig`. Supported components: Table (root frame, header cells, row, cell), Card (root, optional Header/Body/Footer), Dialog/Drawer (root, header/body/footer), Tabs (list/trigger), Menu and shared Listbox (panel/item). Listbox is config-only and styles Select plus single/multiple Combobox. Use token keys for radius, padding, shadow, density and textStyle; background is surface|tint|none; border is solid|dashed|dotted|double|none; borderWidth references borders. Properties are restricted per part, with unknown tokens/parts/properties rejected even through variables. `componentSlots` and `ComponentSlotConfig` are exported from `/define`. Part elements expose stable `data-part` values; Table.header means th, Table.root means its outer frame, and Table.cell defaults also apply to header cells. Use compound table parts for styles, rather than raw tr/th/td. Card.Header/Body/Footer are optional divs with native props and React 19 refs, and add no implicit structure. Explicit root props override slots; slots override component/global defaults. Explicit Table.density overrides cell-slot density; cell padding/textStyle override density spacing/font size; header styles override shared cell styles. Dialog/Drawer header textStyle styles the title too. Active option and selected/striped/hovered row backgrounds keep interaction cues; Tabs.trigger cannot configure borders, preserving its underline. applyTheme swaps generated part CSS while preserving content; resetTheme restores it. Slot rules use yarcl.recipes before consumer recipes and are omitted from token-only CSS. There is no per-instance slots prop; use scoped data-part CSS and compound className props (Table.wrapClassName scopes its frame). Portaled panels are outside trigger ancestors. Guide: https://yarcl.dev/configuration/component-slots/.
- Global defaults go in `defaults`; per-component defaults in `components`, e.g. `components: { Button: { radius: 'square' }, Tooltip: { padding: 'sm', textStyle: 'caption' } }`. Props override both.
- Components with variants may set `components..variants` to a nonempty map of the same recipes as shared `variants`. Its keys replace shared keys for that component's typed `variant` prop. Other components retain shared fallbacks. `ComponentVariant<'Button'>` is the scoped key type; `Variant` remains shared. Set the component's `variant` default when its map omits the inherited default key; ToggleGroup and Pagination also need a `selectedVariant` default when the local map omits the shared selected key. Label stays plain until given a variant. ButtonGroup uses Button recipes; ToggleGroup, Pagination and AvatarGroup apply their own recipes to children. Explicit ButtonGroup and AvatarGroup child variants win. Local classes are `yarcl-{Component}-variant-{key}`; shared classes remain `yarcl-variant-{key}`. Runtime themes must retain keys the app uses. See https://yarcl.dev/configuration/variants/.
- Sized components may set `allowedSizes` to a non-empty subset of global size keys; the component's `size` prop is narrowed to that list. Omit it to allow every global size. `sizeOverrides` partially overrides `height`, `paddingX`, `fontSize` or `iconSize` for a component without restricting its prop type.
- Component spacing, typography and radii come from config tokens. `Alert`, `Tooltip` and `Toast` accept token-backed `gap`, `padding`, `textStyle` and `radius` where applicable; `Table` accepts `radius` for its frame.
- Style with props and config, not inline styles. Custom CSS can use the generated variables, e.g. `var(--yarcl-color-brand)`, `var(--yarcl-space-md)`.
- Imports: components and `toast` from `@yarcl/react`; config helpers from `/define` and `/defaults`; build plugins from `/vite`, `/webpack`, `/rspack`, `/rollup` or `/esbuild`; bundled themes from `@yarcl/react/themes`; `applyTheme` from `@yarcl/react/css`.
- "Failed to resolve import @yarcl/config" means the build plugin is missing. Type errors on `@yarcl/config` mean the tsconfig `paths` entry is missing or points at a different file.
## Component recipes
- Use `defineConfig(baseConfig, (yarcl) => ({ recipes: { ... } }))` to define custom styles. The callback's `yarcl.colors.success`, `yarcl.spacing.sm`, `yarcl.radii.md`, and other groups autocomplete the actual config keys and return serializable CSS variable references.
- Every recipe has `slots.root`; extra slots are named by the consumer. `variants` maps axes to choices to slot styles. `defaults` selects omitted choices. `compounds` supplies `{ when, slots }` for combinations. Slot names, default choices and compound references are checked at compile time.
- `createComponent('Action', Button)` adds the config recipe's typed visual props to an existing component, preserves its behavior and refs, and merges root classes. Recipe axes cannot replace inherited props.
- `createComponent('Status', 'div', ({ rootProps, slots }) => ...)` builds custom markup. Spread `rootProps` onto the root and apply `slots.label`, etc. to declared elements. Only declared recipe names, slots and choices are valid. Recipes do not supply accessibility behavior for arbitrary markup.
- `useRecipe(name, props)` supports custom behavior and state. Remove your behavior props before spreading its `rootProps`; configured recipe props are consumed automatically. `ExtendedComponentProps<'Status', 'div'>` combines native props with inferred visual props.
- Style objects use normal CSS property names, token references, literal CSS strings and `var(--external)` expressions. Support `&:hover`, nested `@media` and `@supports`, and custom properties. Dimensional numbers receive px; unitless and custom-property numbers do not.
- `tokens(baseConfig)` and `defineRecipes(...)` from `/define` support separate recipe modules. Do not import the final config or React entry point inside config definitions. Keep token definitions independent to avoid circular imports.
- The existing plugin generates recipe CSS. Runtime themes must preserve recipe names, slots, choices and referenced tokens. `applyTheme` updates recipe CSS and defaults; `resetTheme` restores them. Token-only CSS assets exclude component recipes.
- Full guide: https://yarcl.dev/configuration/component-recipes/
## Working in a yarcl project
- Start by reading `src/yarcl.config.ts` (or the file passed to the build plugin): it lists every valid size, color, variant, spacing step and text style.
- Props and JSDoc for every component ship as declarations: `node_modules/@yarcl/react/dist/components/.d.ts`.
- To type your own components with the same tokens: `import type { Size, ComponentSize, Color, Variant, Radius, Spacing, TextStyle } from '@yarcl/react'`. `Size` is every global size; `ComponentSize<'Button'>` respects that component's `allowedSizes`.
- Config values at runtime: `config` from `@yarcl/react`, or `useConfig()` inside components (follows runtime theme switches).
- Icons are plain `