# Setting Up a React App with Tale UI ## Quick Start ```bash pnpm add @tale-ui/react @tale-ui/react-styles ``` ```tsx // App entry — import styles once import '@tale-ui/react-styles'; // Import components per-file import { Button } from '@tale-ui/react/button'; export default function App() { return ; } ``` That's it. Components automatically apply their BEM base class (`tale-button`). `@tale-ui/react-styles` pulls in `@tale-ui/css` (the design-token layer) automatically. --- ## Package Architecture ``` @tale-ui/css CSS design tokens, foundations, layout utilities, themes ↑ @tale-ui/react-styles Component CSS (.tale-button, .tale-select__popup, …) ↑ @tale-ui/react Styled React components (BEM class names applied automatically) ↑ @tale-ui/utils Shared hooks & helpers (pulled automatically) @tale-ui/themes Optional standard and monochrome theme presets ``` | Package | What it provides | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `@tale-ui/css` | Design tokens (`--color-*`, `--neutral-*`, `--space-*`, `--text-*`), utility classes (`.gap--m`, `.grid--3`), dark mode, typography foundations | | `@tale-ui/react-styles` | Opinionated CSS for every `@tale-ui/react` component — built entirely on `@tale-ui/css` tokens | | `@tale-ui/react` | Accessible React components that automatically apply BEM class names. Accepts `variant` and `size` props where applicable. Override via `className`. | | `@tale-ui/themes` | Optional standard and monochrome theme CSS, data attributes, and typed metadata | | `@tale-ui/utils` | Internal utilities (colour generation, React hooks, DOM helpers) | --- ## CSS Import Strategies Components render with the correct BEM class names automatically. You still need to import the stylesheet so those classes have rules applied. ### All-in-one (recommended) ```ts import '@tale-ui/react-styles'; // tokens + all component CSS ``` This single import loads `@tale-ui/css` (tokens, foundations, themes) followed by every component stylesheet. ### Per-component ```ts import '@tale-ui/css'; // tokens — must import separately import '@tale-ui/react-styles/button'; // just the button CSS import '@tale-ui/react-styles/dialog'; // just the dialog CSS ``` When importing individual components you **must** also import `@tale-ui/css` yourself, because per-component exports do not re-import it. --- ## Colour System ### 17 named colour families `red` · `orange` · `amber` · `yellow` · `lime` · `green` · `emerald` · `teal` · `cyan` · `sky` · `indigo` · `violet` · `purple` · `fuchsia` · `pink` · `rose` + semantic: `error` · `warning` · `success` Each family spans 11 shades: **5 · 10 · 20 · 30 · 40 · 50 · 60 · 70 · 80 · 90 · 100**. ### Token rules | Token layer | Purpose | Dark-mode behaviour | | ------------- | --------------------------------------------------------------------- | ------------------- | | `--color-*` | All UI styling (buttons, borders, focus rings, etc.) | **Auto-inverts** | | `--brand-*` | Palette definitions only (`:root` overrides, `.color-{name}` classes) | **Never inverts** | | `--neutral-*` | Backgrounds, text, borders | **Auto-inverts** | **Critical rule:** Never use `--brand-*` in component or UI CSS. Always use `--color-*` — it inverts automatically in dark mode. ### Setting a custom primary colour Override `--brand-5` through `--brand-100` at `:root` in your app CSS (imported **after** the design system): ```css :root { --brand-5: #fbf5f9; --brand-60: #7e4271; --brand-100: #36162f; } ``` Dark-mode inversion works automatically — you only need to define the light-mode palette. ### Scoped colour Add a `.color-{name}` class to any container. All `--color-*` tokens inside that subtree resolve to the named palette: ```html
``` ### 6 neutral families `neutral-cool` · `neutral-slate` · `neutral-gray` · `neutral-onyx` · `neutral-mono` · `neutral-warm` (default) Neutral shades use an irregular scale: **5 · 10 · 12 · 14 · 16 · 18 · 20 · 22 · 24 · 26 · 28 · 30 · 40 · 50 · 60 · 70 · 80 · 82 · 84 · 86 · 88 · 90 · 92 · 94 · 96 · 98 · 100**. Full token reference: [packages/css/docs/design-tokens.md](../packages/css/docs/design-tokens.md) --- ## Spacing Use spacing by relationship and hierarchy: | Token range | Typical use | | ----------------------------- | --------------------------------------------------------------------------------------------------------- | | `--space-4xs` / `--space-3xs` | Micro gaps inside compact controls, badges, metadata rows, icon/text pairs, and very small inline padding | | `--space-2xs` | Tight label/value, chip, and compact inline-control gaps | | `--space-xs` | Action rows, heading-to-content gaps inside dense panels, code-block padding, compact card content | | `--space-s` | Standard card/panel padding, related item groups, form field stacks, medium-density grids | | `--space-m` | Larger component groups, relaxed content stacks, mobile page gutters, roomy panels | | `--space-l` / `--space-xl` | Page-level rhythm, desktop page gutters, major grids, section separation | | `--space-2xl` | Large editorial or marketing gaps; largest gap utility size | | `--space-3xl` / `--space-4xl` | Extra-large custom CSS only; no gap utility class exists | Use `--section-space-*` only for vertical padding on full-width page sections or bands. Do not use section spacing for card padding, form stacks, or compact dashboards. --- ## Typography ### 6 type roles | Role | Font family | Weights | Sizes | Use | | ----------- | ----------- | ------- | ---------------------------------------------------------------------------------- | --------------------------------------------- | | **Display** | Inter | 600 | `--display-l-font-size` (2.5625rem) · `m` (2.375rem) · `s` (2.125rem) | Marketing heroes, highly prominent page leads | | **Heading** | Inter | 600 | `--heading-l-font-size` (1.875rem) · `m` (1.70625rem) · `s` (1.5375rem) | Page titles, prominent section headings | | **Title** | Inter | 600 | `--title-l-font-size` (1.50625rem) · `m` (1.36875rem) · `s` (1.31875rem) | Section, panel, card, and grouped-list titles | | **Label** | Inter | 500 | `--label-l-font-size` (1.2rem) · `m` (1rem) · `s` (0.83125rem) · `xs` (0.76875rem) | UI labels, item names, navigation | | **Body** | Inter | 400 | `--text-l-font-size` (1.2rem) · `m` (1rem) · `s` (0.83125rem) · `xs` (0.76875rem) | Body copy, descriptions, helper text | | **Mono** | Roboto Mono | 400 | `--mono-l-font-size` (1.2rem) · `m` (1rem) · `s` (0.83125rem) · `xs` (0.76875rem) | Code, commands, paths, IDs, route values | Additional font: **Playfair Display** (serif) is available via `--expressive-font-family`. ### Hierarchy guidance Type size should usually get smaller as content becomes more nested. Keep semantic HTML separate from visual style: for example, a dense dashboard can render `` instead of using a visually oversized `h2` default. For dashboards and operational tools, a practical hierarchy is: - Page title: `heading-l` - Top-level section title: `title-l` - Panel or card title: `title-m` / `title-s` - Nested item names: `label-m` / `label-s` - Supporting copy: `text-s` / `text-xs` - Commands, paths, IDs, and route values: `mono-s` / `mono-xs` Reserve `display-*` for heroes, editorial pages, or intentionally prominent first-screen content. Avoid it inside cards, tables, sidebars, popovers, and dense repeated layouts. ### CSS classes ```html

Application title

Section title

Card title

Body text

code ``` ### Rem base The design system uses the browser-standard root size, `html { font-size: 100%; }`, so in a default browser **1rem = 16px**. Do not add a Tale-specific root font-size override after importing the styles. See [framework-integration.md](../packages/css/docs/framework-integration.md) for framework import-order guidance. --- ## Dark Mode / Light Mode ### Three-layer system | Priority | Trigger | Selector | | ----------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- | | 1 (lowest) | Default | `html:not([data-color-mode="dark"])` — light mode when no attribute is set | | 2 | OS preference | `@media (prefers-color-scheme: dark)` + `html:not([data-color-mode="light"])` — auto-dark unless explicitly overridden to light | | 3 (highest) | Explicit attribute | `html[data-color-mode="dark"]` — always dark regardless of OS | > **Common mistake:** Do not toggle dark mode by _removing_ the `data-color-mode` attribute. Removing the attribute does not mean "light mode" — it means "no explicit preference", which falls back to OS preference via `prefers-color-scheme`. If the user's OS is set to dark mode, removing the attribute keeps the page dark. Always set the attribute to either `"dark"` or `"light"` explicitly. ### What happens in dark mode - All `--neutral-*` shades **invert** (light ↔ dark) - All `--color-*` shades **invert** (5 ↔ 100, 10 ↔ 90, etc.) - `--brand-*` does **NOT** invert — it is palette-only - `--text-color`, `--display-color`, `--mono-color` automatically adjust ### Setting it up **Inline `` script (required)** Add this script in `` before any CSS to avoid a flash of wrong theme. It reads from `localStorage` first (for `ColorModeToggle` persistence), then falls back to OS preference: ```html ``` **Optional: custom toggle hook** If you need a custom toggle instead of `ColorModeToggle`, use this hook: ```tsx function useDarkMode() { const [dark, setDark] = React.useState(() => { const stored = localStorage.getItem('color-mode'); if (stored) return stored === 'dark'; return window.matchMedia('(prefers-color-scheme: dark)').matches; }); React.useEffect(() => { const mode = dark ? 'dark' : 'light'; document.documentElement.setAttribute('data-color-mode', mode); localStorage.setItem('color-mode', mode); }, [dark]); return [dark, setDark] as const; } ``` **Option C — scoped dark section** ```html
``` --- ## Component Catalogue All components are imported from `@tale-ui/react/{name}`. BEM base classes are applied automatically — you only need extra `className` when overriding specific modifiers not exposed as props. Components that accept variant/size props apply the BEM modifier class for you: ```tsx // → class="tale-button tale-button--primary tale-button--sm" // → class="tale-input tale-input--lg" // → class="tale-radio tale-radio--sm" ``` ### Form Controls | Component | Import path | Key classes | | ------------------- | ------------------------------- | --------------------------------------------------------------------------------------- | | Button | `@tale-ui/react/button` | `.tale-button`, `--primary`, `--neutral`, `--ghost`, `--danger`, `--sm`, `--md`, `--lg` | | IconButton | `@tale-ui/react/icon-button` | `.tale-icon-button`, `--sm`, `--md`, `--lg` | | Input | `@tale-ui/react/input` | `.tale-input`, `--sm`, `--lg` | | TextField | `@tale-ui/react/text-field` | `.tale-text-field` | | TextArea | `@tale-ui/react/text-area` | `.tale-text-area` | | Checkbox | `@tale-ui/react/checkbox` | `.tale-checkbox` | | Checkbox Group | `@tale-ui/react/checkbox-group` | — | | Radio | `@tale-ui/react/radio` | `.tale-radio` | | Radio Group | `@tale-ui/react/radio-group` | — | | Switch | `@tale-ui/react/switch` | `.tale-switch` | | Toggle Button | `@tale-ui/react/toggle-button` | `.tale-toggle-button`, `--sm`, `--md`, `--lg` | | Toggle Button Group | `@tale-ui/react/toggle-button` | `.tale-toggle-button-group` | | Select | `@tale-ui/react/select` | `.tale-select__trigger`, `__popup`, `__item` | | Combobox | `@tale-ui/react/combobox` | `.tale-combobox__input`, `__popup`, `__item` | | Autocomplete | `@tale-ui/react/autocomplete` | `.tale-autocomplete__input`, `__popup`, `__item` | | Number Field | `@tale-ui/react/number-field` | `.tale-number-field` | | Slider | `@tale-ui/react/slider` | `.tale-slider` | | Search Field | `@tale-ui/react/search-field` | `.tale-search-field` | ### Date & Time | Component | Import path | Key classes | | ----------------- | ---------------------------------- | ------------------------- | | Calendar | `@tale-ui/react/calendar` | `.tale-calendar` | | Range Calendar | `@tale-ui/react/range-calendar` | `.tale-range-calendar` | | Date Field | `@tale-ui/react/date-field` | `.tale-date-field` | | Date Picker | `@tale-ui/react/date-picker` | `.tale-date-picker` | | Date Range Picker | `@tale-ui/react/date-range-picker` | `.tale-date-range-picker` | | Time Field | `@tale-ui/react/time-field` | `.tale-time-field` | ### Color | Component | Import path | Key classes | | ------------------- | ------------------------------------ | --------------------------- | | Color Area | `@tale-ui/react/color-area` | `.tale-color-area` | | Color Slider | `@tale-ui/react/color-slider` | `.tale-color-slider` | | Color Wheel | `@tale-ui/react/color-wheel` | `.tale-color-wheel` | | Color Swatch | `@tale-ui/react/color-swatch` | `.tale-color-swatch` | | Color Swatch Picker | `@tale-ui/react/color-swatch-picker` | `.tale-color-swatch-picker` | | Color Field | `@tale-ui/react/color-field` | `.tale-color-field` | | Color Picker | `@tale-ui/react/color-picker` | — (headless state provider) | ### Layout | Component | Import path | | ----------- | ---------------------------- | | Accordion | `@tale-ui/react/accordion` | | Disclosure | `@tale-ui/react/disclosure` | | Tabs | `@tale-ui/react/tabs` | | Scroll Area | `@tale-ui/react/scroll-area` | | Separator | `@tale-ui/react/separator` | ### Overlay | Component | Import path | | ------------ | ----------------------------- | | Dialog | `@tale-ui/react/dialog` | | Alert Dialog | `@tale-ui/react/alert-dialog` | | Popover | `@tale-ui/react/popover` | | Drawer | `@tale-ui/react/drawer` | | Tooltip | `@tale-ui/react/tooltip` | | Preview Card | `@tale-ui/react/preview-card` | ### Navigation | Component | Import path | | --------------- | -------------------------------- | | Menu | `@tale-ui/react/menu` | | Context Menu | `@tale-ui/react/context-menu` | | Menubar | `@tale-ui/react/menubar` | | Navigation Menu | `@tale-ui/react/navigation-menu` | | Toolbar | `@tale-ui/react/toolbar` | | Breadcrumbs | `@tale-ui/react/breadcrumbs` | | Link | `@tale-ui/react/link` | ### Feedback & Display | Component | Import path | | ----------- | ----------------------------- | | ProgressBar | `@tale-ui/react/progress-bar` | | Meter | `@tale-ui/react/meter` | | Avatar | `@tale-ui/react/avatar` | | GridList | `@tale-ui/react/grid-list` | | Table | `@tale-ui/react/table` | | Tree | `@tale-ui/react/tree` | | TagGroup | `@tale-ui/react/tag-group` | ### Interaction | Component | Import path | | ----------- | ----------------------------- | | DropZone | `@tale-ui/react/drop-zone` | | FileTrigger | `@tale-ui/react/file-trigger` | ### Form Structure | Component | Import path | | --------- | ------------------------- | | Field | `@tale-ui/react/field` | | Fieldset | `@tale-ui/react/fieldset` | | Form | `@tale-ui/react/form` | ### Utilities | Export | Import path | Purpose | | ------------ | ------------------------------ | ----------------------------------------------------------- | | Container | `@tale-ui/react/container` | Sets `--color-*` vars for a named/random palette | | CSP Provider | `@tale-ui/react/csp-provider` | Content Security Policy nonce injection | | I18nProvider | `@tale-ui/react/i18n-provider` | Locale and text direction (wraps React Aria's I18nProvider) | | `mergeProps` | `@tale-ui/react/merge-props` | Merge multiple prop objects | | Icon | `@tale-ui/react/icon` | Renders a Lucide icon with Tale UI sizing | | ColorModeToggle | `@tale-ui/react/color-mode-toggle` | Light/dark mode toggle with persistence | --- ## Data Attributes for Styling Components expose state via data attributes. Use these in CSS selectors: | Attribute | Meaning | | -------------------------------------- | --------------------------------------- | | `data-disabled` | Component is disabled | | `data-open` | Popup / disclosure is open | | `data-closed` | Popup / disclosure is closed | | `data-checked` | Checkbox, radio, or switch is checked | | `data-unchecked` | Checkbox, radio, or switch is unchecked | | `data-selected` | Item is selected (select, combobox) | | `data-highlighted` | Item has keyboard/pointer highlight | | `data-focus-visible` | Keyboard focus is visible | | `data-side="top\|bottom\|left\|right"` | Popup placement side | | `data-starting-style` | Enter animation start | | `data-ending-style` | Exit animation start | | `data-popup-open` | Trigger element while its popup is open | --- ## Component Composition Patterns Tale UI components use two composition patterns. Choose based on whether the component has built-in label/description parts. ### Pattern A: Compound components with built-in parts Most form controls (Input, TextField, Select, Combobox) have their own Label, Description, and ErrorMessage parts: ```tsx import { Input } from '@tale-ui/react/input'; Email address We'll never share your email. ; ``` React Aria automatically links the label to the input via `aria-labelledby` and the description via `aria-describedby`. ### Pattern B: Field wrapper for custom or plain controls When using a plain `` or a component that doesn't have built-in label parts, wrap it with Field: ```tsx import { Field } from '@tale-ui/react/field'; Password Must be at least 8 characters. ; ``` > **Note:** `Field.Error` requires a React Aria validation context to render. For error messages, use a validatable component like `Input.Root` with `isInvalid` (see Pattern A) instead of `Field.Root`. ### When to use which | Situation | Use | | ----------------------------------------------------------- | ----------------------------------------------------------------------- | | Using a Tale UI form control (Input, Select, etc.) | Pattern A — use the component's built-in `.Label`, `.Description` parts | | Wrapping a plain ``, `