# 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/core` (the design-token layer) automatically. --- ## Package Architecture ``` @tale-ui/core 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) ``` | Package | What it provides | |---------|-----------------| | `@tale-ui/core` | 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/core` 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/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/core` (tokens, foundations, themes) followed by every component stylesheet. ### Per-component ```ts import '@tale-ui/core'; // 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/core` 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) --- ## Typography ### 6 type roles | Role | Font family | Weights | Sizes | |------|------------|---------|-------| | **Display** | Inter | 600 | `--display-l-font-size` (4.1rem) · `m` (3.8rem) · `s` (3.4rem) | | **Heading** | Inter | 600 | `--heading-l-font-size` (3.0rem) · `m` (2.73rem) · `s` (2.46rem) | | **Title** | Inter | 600 | `--title-l-font-size` (2.41rem) · `m` (2.19rem) · `s` (2.11rem) | | **Label** | Inter | 500 | `--label-l-font-size` (1.92rem) · `m` (1.60rem) · `s` (1.33rem) · `xs` (1.23rem) | | **Body** | Inter | 400 | `--text-l-font-size` (1.92rem) · `m` (1.60rem) · `s` (1.33rem) · `xs` (1.23rem) | | **Mono** | Roboto Mono | 400 | `--mono-l-font-size` (1.92rem) · `m` (1.60rem) · `s` (1.23rem) | Additional font: **Playfair Display** (serif) is available via `--expressive-font-family`. ### CSS classes ```html

Display medium

Body text

code ``` ### Font-size caveat The design system sets `html { font-size: 62.5% }` so that **1rem = 10px**. If your app also uses Tailwind, shadcn/ui, or Bootstrap, add `html { font-size: 100%; }` after the Tale UI import. See [framework-integration.md](../packages/css/docs/framework-integration.md) for the full workaround. --- ## 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 **Option A — OS preference only (no toggle)** Add an inline script in `` before any CSS to avoid a flash of wrong theme: ```html ``` **Option B — with a toggle** ```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` | | Input | `@tale-ui/react/input` | `.tale-input`, `--sm`, `--lg` | | 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` | ### 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` | ### Feedback & Display | Component | Import path | |-----------|------------| | ProgressBar | `@tale-ui/react/progress-bar` | | Meter | `@tale-ui/react/meter` | | Avatar | `@tale-ui/react/avatar` | ### 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 | | `useRender` | `@tale-ui/react/use-render` | Custom render hook | --- ## 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. This field is required. ``` ### 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 ``, `