# Loui DS: full documentation > Loui DS is a dark, minimal, functional React design system (React 19, React Aria Components, CSS Modules, DTCG tokens) for internal tools, dashboards and apps. It is built to be operated by humans and AI agents. Rules for generated UI: - Use only components, hooks and tokens exported by the package; never write raw colors, px sizes, z-indexes or inline styles. - One primary action per screen (`Button variant="primary"`), top right. The accent blue is for focus, selection and links only. - Hierarchy comes from surface plus border; shadows only on overlays. - Color never carries meaning alone: pair it with text or an icon with a name. - Every interactive element needs an accessible name; icon-only controls require a `label`. - Microcopy: English, sentence case, buttons are verb + object, no exclamation marks, no emoji. See content-rules.json. - No user-visible string literals in components: use the message catalog and `DSProvider`. - Money is integer minor units plus an ISO 4217 code; format dates, numbers and money with the provided utilities. - Do not use `experimental` or `deprecated` components in recipes or generated code unless the user asks. --- ## Getting started Browse every component live in Storybook: . AI tools can load (see [Using with AI](./using-with-ai.md)). ### Requirements - React 19 and React DOM 19 (peer dependencies). React 18 is not supported. - An evergreen browser. Node 24 or later for tooling. - A bundler that handles CSS imports and `woff2` assets (Vite, Next.js, webpack, esbuild). ### Install ```sh pnpm add loui-ds react react-dom ``` `npm install loui-ds` and `yarn add loui-ds` work too. To upgrade, run `pnpm update loui-ds` and read the [release notes](https://github.com/xrazai/loUI/releases) first: before 1.0 a minor version may break. Every release also attaches the package tarball, if you need to install a version by URL. `react-aria-components`, `react-stately` and `lucide-react` are regular dependencies of the package; you do not install them yourself. ### 1. Load the stylesheet once Import the stylesheet once, at the root of your app, before any component renders: ```tsx import 'loui-ds/styles.css'; ``` It contains, in this layer order, `reset`, `tokens`, `base`, `components` and `utilities`. Because everything sits in named cascade layers, your own unlayered CSS always wins over the design system without specificity tricks. The stylesheet sets the dark theme on the page (`bg-canvas` on `html`, `bg` and `text` on `body`, the Geist font), so you normally do not need your own reset. ### 2. Wrap the app in `DSProvider` ```tsx import { DSProvider } from 'loui-ds'; export function Root({ children }: { children: React.ReactNode }) { return {children}; } ``` | Prop | Purpose | | ---------- | ---------------------------------------------------------------------------------------------------------------- | | `locale` | BCP 47 tag. Drives messages, number and date formatting, sorting, and the direction (RTL) of React Aria widgets. | | `messages` | Overrides keyed by component, e.g. `{ Alert: { dismiss: 'Close' } }`. Applied over the built-in catalogs. | | `navigate` | Client-side navigation for `Link` and link-like components (your router's `navigate`). | | `useHref` | Maps a router path to a real `href` (needed for basename routing). | Without a provider everything still works in `en-US`; it just does not follow the user's locale. English is the default and `pt-BR` ships with the library. See `docs/architecture/i18n.md` for how messages resolve. Client-side routing, for example with React Router: ```tsx import { useNavigate, useHref } from 'react-router'; function Providers({ children }: { children: React.ReactNode }) { const navigate = useNavigate(); return ( {children} ); } ``` ### 3. Use components ```tsx import { Alert, Button } from 'loui-ds'; export function Example() { return ( <> 3 boxes of notebooks are left. Reorder to avoid delays. ); } ``` Use `onPress`, not `onClick`: it is the React Aria press event and works for mouse, touch, keyboard and screen readers. ### Fonts Geist (UI) and Geist Mono (data) are self-hosted and ship inside the package: `styles.css` references `./fonts/*.woff2` relative to itself, and the build copies them to `dist/fonts`. Nothing is fetched from Google Fonts or a CDN at runtime. Your bundler resolves the font files when it processes `loui-ds/styles.css`; there is nothing to configure. Both families load with `font-display: swap` and a metric-matched fallback so the layout does not shift. ### Theming Dark is the only theme. The semantic token layer (`--ds-bg`, `--ds-surface`, `--ds-text`, ...) can be overridden in your own CSS if you must; component code only reads semantic tokens. A `[data-theme="paper"]` scope provides the light surface for receipt and PDF previews. Forced-colors and reduced-motion modes are handled by the components. ### Server-side rendering - Components never touch `window` or `document` while rendering or at import time, so they render on the server. - IDs come from React `useId`; React Aria needs no `SSRProvider` with React 19. - `useMediaQuery` returns its `defaultValue` (`false`) on the server and corrects after hydration. Prefer CSS media queries for styling. - Import `loui-ds/styles.css` in the root layout so the first paint is already styled. ### Framework notes **Vite.** Import the stylesheet in `main.tsx`; nothing else is needed. **Next.js (App Router).** See [SSR and Next.js](#ssr-and-nextjs) below. ### SSR and Next.js The package ships a `"use client"` directive **per module**: every file that uses hooks, React context or React Aria has it, and the entry points (`index.js`) and pure helpers do not. So a React Server Component (the default in the Next.js App Router) can import the design system directly, with no wrapper file: ```tsx // app/layout.tsx (Server Component) import 'loui-ds/styles.css'; import { DSProvider, ToastRegion } from 'loui-ds'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ```tsx // app/page.tsx (Server Component): components are client references and render on the server too import { Button, DataTable } from 'loui-ds'; ``` Rules that follow from the React Server Components model, not from the library: - Props that cross from a Server to a Client Component must be serializable. **Functions cannot**: `onPress`, `onChange`, render props such as `DataTable` column `render` or `Dialog` `footer={({ close }) => ...}`, and component values such as an `icon` prop. Put those in a file that starts with `'use client'` (a small component per interaction, see `examples/next-app/app/CreateOrderButton.tsx`). - `DSProvider` takes plain props (`locale`, `messages`) and works in a Server layout; `navigate` and `useHref` are functions, so wrap the provider in a client component when you use client-side routing. - Hooks (`useHotkeys`, `useToast`, ...) and the `toast` API only work in Client Components. - Import the stylesheet once in the root layout. `dist/styles.css` and the Geist fonts need no `transpilePackages` or other `next.config` settings. `sideEffects` in `package.json` lists only `**/*.css`, so bundlers drop unused components (importing `Button` alone costs about 12 kB brotli, enforced by `pnpm size` and `pnpm test:treeshake`). The packaging is verified on every pull request by building `examples/vite-app` and `examples/next-app` (Next.js 16, App Router, Turbopack) against a packed tarball: `pnpm examples:verify`. ### Next steps - Read the [Principles](./principles.md) and the [Content guidelines](./content-guidelines.md) before building a screen. - Browse the components in Storybook; each page lists props, variants, states and accessibility notes. - Agents: start from [Using with AI](./using-with-ai.md). --- ## Principles These rules apply to every screen built with Loui DS. They are short on purpose. When a case is not covered, choose the option that keeps the user's attention on one thing. ### Three rules 1. **One accent.** The blue accent (`--ds-accent`) is only for focus, selection, links and "open" states. Never use it for buttons or large fills. 2. **Ink for action.** The primary button is light on dark (`--ds-primary-bg`). Use **one primary action per screen**. 3. **Borders first.** Hierarchy comes from surface plus border. Shadows only on overlays: menus, toasts, dialogs and sheets. ### Clarity and focus The system is designed for use that needs no effort of attention (the first product's owner has ADHD). Every new screen follows these rules. 1. **One main action per screen,** always at the top right, with a keyboard shortcut. 2. **The most important thing comes first and is largest.** Everything else is secondary text (`--ds-text-2`, `--ds-text-3`). 3. **Color only carries meaning, and is always accompanied by text.** Never signal status by color alone. 4. **Fixed positions.** The same element lives in the same place in every module, with no misalignments: a 4 px grid and fixed heights. 5. **Deleting always asks for confirmation** in a dialog: the title "Delete X?", the consequence in one sentence, a red button that names the action, and **initial focus on Cancel**. A toast with "Undo" follows. Reversible actions (mark, link, change status) never ask; they only offer "Undo". 6. **Lists are alphabetical by default** (locale-aware, accent-insensitive), including groups, columns and select options. Exceptions: tasks (by urgency) and dated records such as orders (newest first). Use `sortByText`. 7. **Visible progress.** Counters that go down, a day progress bar, a green check for done. Feedback is **subtle**: a 320 ms transition, no confetti. 8. **Save automatically** and show "Saved". 9. **Keyboard first.** `⌘K` for everything, and shortcuts on primary actions. ### Agents (AI features) For products that put an AI agent in front of customers or money. 1. **Approve before sending.** Anything that goes to a customer or touches money goes through an approval queue. The default is to ask for approval. 2. **Autonomy per task,** never per whole agent. Low-star reviews (1 to 3 stars) always require approval. 3. **Undo, don't confirm.** Sending and discarding show a toast with Undo. 4. **The chat is for the owner,** not for the customer: to test, ask why and adjust. It reads the guidelines and the current data. 5. **Customer-facing replies** are short, factual, make no delivery-time or discount promises, and use no emoji. ### Visual foundations The authoritative values are the design tokens (see the Tokens page). The summary: - **Color.** A slightly warm neutral ramp from `#050506` to `#EDEDEA` (text is never pure white). Signals are pastel-bright so they keep contrast on dark: accent `#8AB4FF`, success `#5FD49A`, warning `#E9B65C`, danger `#F07A6E`. Soft backgrounds use `color-mix`. - **Layers.** canvas, bg, surface, raised, hover. Move one layer at a time; do not skip layers. - **Type.** Geist (400, 500, 600) for the interface, Geist Mono (400, 500) for data. Display and title use negative tracking. - **Spacing.** A 4 px grid: 4, 8, 12, 16, 20, 24, 32, 48, 64. Card padding is 20; gaps of 8 to 16 inside components and 24 to 32 between sections. - **Controls.** Heights 32, 40 (default) and 48. Buttons and inputs share the same height. - **Radius.** 4 for chips, menu items and checkboxes; 6 for buttons and inputs; 10 for cards, menus and dialogs; pill for badges and switches. - **Lines.** 1 px always. Subtle for internal dividers, regular for cards, strong for controls and overlays. Dashed means disabled or a total row. - **Elevation.** Only `--ds-shadow-overlay` and `--ds-shadow-dialog`. No inner shadows, no glow, no blur. - **States.** Hover is one surface step lighter. There is no press animation. Focus is an accent border plus a 3 px ring. A selected row has a raised background and a 2 px inset ink bar; the active tab has a 2 px ink underline. Disabled controls use reduced opacity and, for inputs, a dashed border. - **Motion.** Minimal: 150 ms, `cubic-bezier(.2, 0, 0, 1)`, only color, opacity and position. Honor `prefers-reduced-motion`. - **Backgrounds.** Flat. No gradients, textures or illustrations. Photos appear only in avatars and thumbnails. ### Iconography - The icon set is **Lucide**, 1.5 px stroke, `currentColor`, **bundled** (never fetched from a CDN at runtime). Import icons from `loui-ds` (`PlusIcon`, `SearchIcon`, ...), not from `lucide-react`. - Icons are decorative unless given a label. Icon-only controls use `IconButton`, which requires a `label`. - No emoji. No filled or colored icons. Unicode glyphs (`+`, `×`, `⌘`) only inside labelled icon buttons. - Outside the sidebar the system is text-first: actions use a label. --- ## Content guidelines How the interface talks. These rules apply to every string in the UI: labels, buttons, titles, toasts, errors, empty states and confirmations. They are also available as data in `docs/ai/content-rules.json`. ### Tone - **Direct, calm, technical without jargon.** - Speak to "you", usually implied: "Select an order", not "Please select an order". - State what happened and what to do. Do not blame the user and do not dramatize. ### Casing - **Sentence case everywhere:** titles, buttons, tabs, menu items. "New order", not "New Order" or "NEW ORDER". - Uppercase appears only in mono group captions, and it is applied with CSS, never typed. ### Buttons - **A verb plus an object:** "Create order", "Mark as shipped", "Retry charge". - Never a bare "OK", "Send" or "Submit". - Destructive buttons name the action: "Delete order". ### Confirmations Deleting always asks. The dialog names the object and the consequence in one sentence, the red button names the action, and initial focus is on Cancel. > **Delete "Northwind Web"?** > "Northwind Web" and its 14 environments will be removed. This can't be undone. > [Cancel] [Delete project] Reversible actions (mark, link, change status) do not ask. They happen and offer "Undo" in a toast. ### Toasts Short and in the past tense: what just happened. - "Order #1051 created" - "3 items archived" Not "Creating order", not "Success!", not "Order was successfully created". ### Empty states Say what happened, then what to do. - "No orders found. Try another filter or search term." ### Errors Say how to fix the problem. - "Enter a complete email address." Not "Invalid email!". Link the message to its field (`aria-describedby`); the `Field` components do this for you. ### Punctuation and symbols - **No emoji, no exclamation marks, no decorative ellipses.** - The ellipsis character appears only on menu items that open something that needs more input: "Move to…". - No "Loading..." or "Saving...". Use a spinner or skeleton, and state the result when it is done. ### Numbers, codes and shortcuts - Numbers, codes, amounts and shortcuts are set in the mono font (Geist Mono): `ORD-1048`, `$1,240.00`, `⌘K`. - **Never build currency, dates or numbers by hand.** Format them with `Intl` for the active locale through the provided utilities: `formatCurrency`, `formatDate`, `formatNumber`, `formatPercent`, `formatRelativeDays`. - Money is an integer in minor units plus an ISO 4217 currency code (ADR-0006). `formatCurrency(124000, 'USD', locale)` renders `$1,240.00`. ### Lists Alphabetical by default (locale-aware, accent-insensitive), including groups, columns and select options. Use `sortByText`. Exceptions: tasks (by urgency) and dated records (newest first). ### What belongs to the product, not to the design system Domain vocabulary (for example, an ERP rule that sold units are always called "Quantity") belongs to the product's own guidelines. The design system's built-in strings stay generic. ### Built-in strings and translation Components contain no user-visible string literals. Every built-in label and ARIA string lives in the component's message catalog (`.messages.ts`), is written in English following the rules above, and can be overridden per instance or through `DSProvider`. `pt-BR` ships with the library and follows the same tone. --- ## Accessibility Accessibility is part of the definition of done for every component. The target is **WCAG 2.2 Level AA**, using the WAI-ARIA Authoring Practices patterns. ### Conformance target - Real semantics first: native `