# App shell Source: https://ui.tale.dev/docs/components/app-shell `AppShell` provides the shared theme, translation, and tooltip context. It does not draw your page header or navigation. Compose those separately with `PageLayout`, the adaptive header components, and `ContentArea`. ## Mount the providers [Installation](/docs/getting-started/installation) includes a complete entry point. A router-based application uses this composition: ```tsx import { AppShell } from '@tale/ui/app-shell'; import { RouterProvider } from '@tanstack/react-router'; // i18n and router are initialized by the host application. ; ``` | Option | What it adds | | --- | --- | | `i18n` | Required service instance, supplied to `I18nextProvider`. | | `locale={{ mode: 'client' }}` | Preference/browser locale detection and synchronization. `onChange` can load additional locale data; `defaultLocale` supplies a fallback. | | `theme` | The shared theme provider with the normal system-preference behavior. | | `children` | Your router or application content. | The full optional stack is ThemeProvider, TooltipProvider, LocaleProvider, I18nextProvider, then locale synchronization and content. Do not add a TooltipProvider around each button; the outer provider shares tooltip timing across controls. For a URL-driven locale, omit `locale` and synchronize the route's language through `LocaleSync`. Query clients, authentication, authorization, branding, and a toast viewport remain host responsibilities. `AppShell` imports the shared Inter fonts. ## Compose the page This is a labelled layout illustration. Its nested application header and controls are inert; inspect **Code** to see how the pieces compose without adding a second accessible application to this page. | Piece | Responsibility | | --- | --- | | `PageLayout` | Flex page and scroll container; wraps an optional `header` in `StickyHeader`. | | `AdaptiveHeaderRoot` | The title/action row, with optional border and responsive treatment. | | `AdaptiveHeaderTitle` | The page title, rendered as `h1`. | | `ContentArea` | Content spacing and width: `page`, `narrow`, or `panel`. | Give flex ancestors a usable height and `min-h-0` when the page should scroll inside them. `PageLayout` reserves scrollbar space to reduce sideways movement as row counts change. `ContentArea` includes clearance for mobile floating actions. Choose one divider between header and content. Use `showBorder` for a plain header; avoid adding another border when a tab strip already supplies the divider. ## Plan the mobile header Adaptive headers coordinate through `AdaptiveHeaderProvider`. The desktop root alone is not a complete mobile header: the host must render the receiving `AdaptiveHeaderSlot` in its mobile chrome. The mobile treatment removes the desktop title from the accessibility tree and presents the active title through that slot. Check a real phone-width page after composing the providers and slots. It should have one accessible `h1`, visible actions, and enough bottom clearance for any floating action bar. A desktop layout illustration cannot prove this integration for your service. ## Add breadcrumbs `HeaderBreadcrumbs` renders a labelled navigation list whose leaf is the current page's `h1`. Supply ancestor links or buttons through the crumb content and use `HEADER_CRUMB_LINK_CLASS` for their shared treatment. The component does not resolve application routes for you. Below `md`, the trail collapses toward an immediate-parent back control. `showImmediateParentOnMobile` can preserve the parent's visible name where there is room. Long titles still need testing in the full header, including the action buttons beside them. ## Add a section rail `SubPanel` provides the bordered secondary navigation surface. The default width is 224px; `wide` is 256px. It is hidden below `md`, so the host needs a mobile navigation path, such as a labelled drawer. ```tsx import { SubPanel } from '@tale/ui/sub-panel'; import { SubPanelRowLink, SubPanelSectionHeader } from '@tale/ui/sub-panel-list'; ``` The caller supplies rows and scrolling. Use `SubPanelRowLink` for destinations and `SubPanelSectionHeader` for groups. For a custom row, reuse `SUB_PANEL_ROW_CLASS` and `useSubPanelRowTreatment` rather than reproducing selection styles. Start the application with a `SkipLink` targeting `
`. Name each navigation landmark. Scrolling a rail to its active row should not steal the reader's initial keyboard position. Use the [list-page](/docs/patterns/list-page) or [settings-page](/docs/patterns/settings-page) pattern for the content inside this shell. # Button Source: https://ui.tale.dev/docs/components/button Use `Button` for an action on the current screen. Give it a specific verb, such as **Save changes** or **Export**, and choose its appearance according to the action's importance and consequence. For navigation, use a link with button styling. ```tsx import { Button, LinkButton } from '@tale/ui/button'; ``` ## Variants | Variant | Appropriate use | | --- | --- | | `primary` (default) | The main action in the current task. | | `secondary` | A supporting action such as Cancel. | | `ghost` | A quiet action in a toolbar or row. | | `destructive` | A consequential removal, such as deleting or revoking. | | `warning` | A consequential action that requires caution. | | `success` | A positive action or completion treatment where the meaning is clear. | | `link` | A text-style action; this remains a button unless you change its element. | Use one primary action per decision area. A destructive color does not ask for confirmation or enforce permissions; the host owns those behaviors. Use a [confirmation dialog](/docs/components/dialog) when the decision needs explanation. ## Sizes and responsive labels `default` is 36px high (`h-9`), and `sm` is 32px (`h-8`). `icon` and `icon-sm` are square controls at those heights. There is no `lg` size. The text-style `link` variant uses an automatic height instead of the fixed control box. Pass a Lucide component through `icon`; Button supplies a 16px decorative icon and spacing. `collapseLabel` visually hides the text below `sm` while preserving the accessible name. Pair it with an icon so the mobile control still has visible content. ## Show work in progress Choose **Save changes** to see a short simulated operation. `isLoading` displays a spinner, sets `aria-busy`, and disables activation. The label remains in place. The example resets after 1.5 seconds; a real screen should clear loading when its request settles. Set `type="submit"` for form submission and `type="button"` for other form actions. The component does not generally override the browser's default button type. Disable duplicate submissions in your handler as well, and keep a failed save visible near the form. ## Explain an unavailable action Focus **Publish** with the keyboard to read why it is unavailable. When `disabled` and a nonempty `disabledReason` are present, the component uses `aria-disabled` instead of native `disabled`, preserves keyboard focus, and blocks clicks, Enter, and Space. A plain disabled button leaves the tab order. The reason only applies while `disabled` is true. Describe what would make the action available; use visible nearby text when the explanation is essential to completing the task. A disabled UI control is not an authorization check. ## Navigate with a link `asChild` merges styling onto one child element, such as an anchor. Keep link behavior on that child. Tooltips and `disabledReason` are suppressed in this mode, and an anchor does not acquire native button disabling. Do not use `disabled` as a way to prevent a link from navigating. For TanStack Router destinations, `LinkButton` accepts `href`, `params`, `search`, and `prefetch`. It requires router context. For an external URL, an anchor inside `Button asChild` keeps normal browser link behavior. ## Props Native button attributes pass through. These are the component-specific choices: | Prop | Values or default | Behavior | | --- | --- | --- | | `variant` | `primary`, `secondary`, `ghost`, `destructive`, `warning`, `success`, `link`; default `primary` | Visual emphasis. | | `size` | `default`, `sm`, `icon`, `icon-sm`; default `default` | Control dimensions. | | `icon`, `iconClassName` | Lucide component; optional extra classes | Leading decorative icon. | | `isLoading` | `false` | Spinner, busy state, and activation blocking. | | `disabledReason` | Optional React content | Focusable explanation while disabled; unavailable with `asChild`. | | `fullWidth` | `false` | Fills the available width. | | `collapseLabel` | `false` | Hides text visually below `sm`. | | `asChild` | `false` | Styles one child instead of rendering a button. | | `title` | Optional string | Tooltip; also names an icon-sized Button unless `aria-label` overrides it. | | `tooltip` | Optional React content | Overrides the visible tooltip text. | | `tooltipSide` | `top`, `right`, `bottom`, `left`; default `top` | Tooltip placement. | | `tooltipOpen`, `onTooltipOpenChange` | Optional controlled state | Lets a caller control a state-announcing tooltip. | ## Accessibility and alternatives Icon-sized Buttons require `aria-label` or `title` at the type level. Text-sized buttons still need meaningful children; the type system cannot judge the label's quality. A tooltip alone is a description, not the control's name. `title` on a text Button adds a tooltip without replacing its visible accessible name. Use `IconButton` from `@tale/ui/icon-button` for a toolbar glyph: it requires `aria-label`, defaults to `ghost`, and supplies a tooltip. For a table's create action, prefer `DataTable.addAction`; for a row menu, use the table's column builders. Check keyboard focus, the disabled explanation, and the loading state in both themes. # Data table Source: https://ui.tale.dev/docs/components/data-table `DataTable` renders a table and its surrounding search, filters, create action, and paging controls. You provide the data and state transitions. The component does not fetch rows, authorize actions, or filter a backend query for you. ```tsx import { DataTable } from '@tale/ui/data-table/data-table'; import type { ColumnDef } from '@tanstack/react-table'; ``` ## Define the visible columns Each TanStack `ColumnDef` supplies an accessor or cell renderer and a header. Use a stable domain ID through `getRowId` when rows can be selected, expanded, reordered, or refreshed. Otherwise row-index identity can attach state to the wrong item after a data change. Pass a descriptive `caption`, such as **Agents in this workspace**. It becomes a screen-reader table caption. It is optional in the TypeScript interface, but a data table still needs an accessible name in the page. The builders exported from `@tale/ui/data-table/column-builders` include text, date, creation-time, selection, and action columns. Reuse them for those common shapes; use custom cells when the content requires them. ## Wire search to the rows Activate **Search automations**, then type `Weekly`: only **Weekly digest** remains. Type a value that matches nothing to see the shared **No results found** state. Clear the query to restore all four rows. The example filters names in memory; it does not search the trigger column. `search={{ value, onChange, placeholder }}` renders and controls the search field. Your callback updates the query and the rows. For a backend search, send the new query to the backend, reset the paging cursor, and pass the resulting rows back to the table. Preserve meaningful query/filter state in the URL when the screen needs shareable results. `emptyState` describes an initially empty collection. An active query or filter with zero rows uses the table's shared no-results copy instead. If cursor pages remain, the table treats zero visible matches as still loading rather than declaring the entire source empty. ## Choose toolbar controls | Prop | Host responsibility | | --- | --- | | `search` | Own the query and apply it to the data source. | | `filters`, `dateRange` | Supply available choices, selected values, and handlers. | | `onClearFilters` | Reset the relevant filters consistently. | | `filtersContent` | Place an additional filter-side control inside the toolbar. | | `addAction` | Supply a label and a click handler, destination, or create-menu items. | | `actionMenu` | Supply bespoke primary-side toolbar content; it takes precedence over `addAction`. | When an initially empty table has no search/filter toolbar, `addAction` moves into the empty state. With toolbar controls present, it stays in the header. Pass the permission-dependent disabled state from your service; the table does not decide access. ## Loading and errors Set `isLoading` while fetching the initial data. `approxRowCount` helps reserve space: an unknown count gives the default skeleton; a positive estimate gives skeleton rows up to the component's cap; zero allows the supplied initial empty state. Do not pass zero merely because a request has not returned yet. Pass `error` and `onRetry` for a failed query. A load failure should explain recovery rather than masquerade as an empty collection. Keep filter state when retrying so the request still matches what the reader sees. ## Pick one paging model | Model | Configuration | | --- | --- | | All rows already loaded | `pagination.clientSide: true`; the table slices the in-memory data. | | Server pages | Supply `pagination` callbacks/counts and the one-based `currentPage`; replace rows after each request. | | Cursor loading | Supply `infiniteScroll.hasMore`, `onLoadMore`, and loading state; append returned rows in the host. | Cursor loading is automatic by default and also provides a load-more control. Supply `entityLabel: { one, other }` for count-aware footer text. `totalCount` is the unfiltered total; `displayedCount` is useful when one visible row represents multiple entities. Do not report an estimate as an exact total. ## Selection, sorting, and row actions `enableRowSelection` accepts a boolean or a per-row predicate. Pair controlled `rowSelection` with `onRowSelectionChange`, a stable `getRowId`, and a selection column. A disabled UI row is not a server-side permission boundary. The `sorting` configuration enables sorting and carries `initialSorting` with `onSortingChange`. Verify whether your host is sorting the complete local set or requesting a sorted backend set; sorting only the currently loaded page is not a global ordering. `onRowClick` receives a TanStack `Row`, so domain data is in `row.original`. `isRowClickable` can exclude aggregate or restricted rows. Keep a named keyboard-accessible link or action in the row; a pointer click handler alone is not equivalent to a navigation link. Use `onRowMouseEnter` for optional route preloading. `enableExpanding` and `renderExpandedRow` reveal inline detail. Keep the expansion control distinct from row navigation and test both with the keyboard. For the surrounding screen, use the [list-page pattern](/docs/patterns/list-page); for a short static table without this chrome, use `Table` from `@tale/ui/table`. # Dialog Source: https://ui.tale.dev/docs/components/dialog Use a dialog when someone needs to complete a focused task or make a decision before returning to the current page. `Dialog` supplies the modal structure; your application supplies its state, content, and callbacks. ```tsx import { Dialog } from '@tale/ui/dialog/dialog'; import { ConfirmDialog } from '@tale/ui/dialog/confirm-dialog'; ``` ## Open and close a dialog Choose **Invite a member**, enter a sample email, and press Escape or **Cancel**. Focus returns to the opener. **Send invitation** only closes this local example; it sends no email and performs no validation or persistence. `open`, `onOpenChange`, and `title` are required by `Dialog`. Pass a `trigger` when the opener is available in the same composition. Otherwise update `open` from your own button or menu and let the dialog capture the active opener. The title is the accessible name. `description` gives context under it. Place required instructions where they remain visible, and label every form control independently. The footer is caller-owned: dismissing action first, confirming action second. ## Explain a consequential decision Choose **Delete project**, then cancel or confirm. Confirmation updates only the example's local message. The sample consequence text demonstrates where an application would explain its own deletion rules; it is not a specification of Tale project deletion. `ConfirmDialog` renders Cancel and Confirm actions for you. `confirmText` and `cancelText` override shared translated defaults. `variant` selects `default`, `destructive`, or `warning` styling; it does not implement the underlying operation. Your `onConfirm` handler owns the request and closing behavior. Set `isLoading` while it runs: confirmation, cancellation, and close requests are blocked until it settles. On failure, keep the decision context and explain the problem. `disableConfirm` disables confirmation without disabling cancellation. For a type-to-confirm decision, set `requireConfirmPhrase`. The trimmed input must match the phrase exactly, including case. It resets when the dialog opens again. This deliberate UI step is additional confirmation, not a substitute for authorization. ## Choose the right wrapper | Component subpath | Use | | --- | --- | | `dialog/dialog` | Custom content and footer. | | `dialog/confirm-dialog` | A decision with paired cancel/confirm actions. | | `dialog/delete-dialog` | Entity-specific deletion wording. | | `dialog/form-dialog` | A form with submission state and actions. | | `dialog/view-dialog` | Read-only detail. | | `overlays/responsive-dialog` | The responsive wrapper's dialog/drawer composition. | The base Dialog itself uses a bottom-sheet layout below `md` and a centered modal above it. Its header and footer remain outside the scrollable body. Test long content on a phone; choosing a large desktop size does not remove the need for that check. ## Base dialog options | Prop | Purpose | | --- | --- | | `size` | `sm`, `default`, `md`, `lg`, `xl`, `3xl`, or `wide`; default `default`. | | `children`, `footer` | Body and action content; either may be omitted. | | `icon`, `headerActions` | Additional header content. | | `onBack`, `backLabel` | A labelled back control for an in-dialog subview. | | `customHeader` | Replaces the visible header; the required title remains available to assistive technology. | | `hideClose` | Hides the close control; provide an accessible dismiss path unless the current operation deliberately blocks it. | | `className`, `headerClassName`, `bodyClassName`, `footerClassName` | Targeted layout adjustments. | | `restoreFocusRef` | Stable fallback when the captured opener unmounts, for example after a menu closes. | | `preventCloseAutoFocus` | Opt out of automatic restoration only when the caller explicitly manages the next focus target. | ## Handle lifecycle and focus deliberately The modal traps focus while open. Escape and the close control request dismissal; controlled state determines whether the request is accepted. Restore focus to a useful surviving control after close, especially when a successful action removes the original row. Content can remain mounted through a closing animation. Do not assume `open=false` immediately stops its subscriptions or requests. If hook-heavy content has a closing-lifecycle problem, move it into a separate component and conditionally mount that component; do not call hooks conditionally inside one component. Use an inline error for repairable form problems and a [toast](/docs/components/toast) for an optional completion notice. Use a persistent page or side panel when the task needs more room or the reader needs to refer to the surrounding content continuously. # Input Source: https://ui.tale.dev/docs/components/input `Input` combines an input element with a label, help text, and validation feedback. Supply a visible `label` for ordinary forms; if the surrounding UI already labels the control, provide the corresponding accessible name yourself. ```tsx import { Input } from '@tale/ui/input'; ``` ## Give the field enough context Use `label` for the field's name, `description` for context before entry, and `hint` for a short format or usage rule below the control. A placeholder is an example value, not a replacement for a label: it disappears when the person types. The component generates an ID unless you pass one. It associates the label, description, hint, and error with the input, and preserves additional IDs you supply through `aria-describedby`. ## Validate and explain the fix `errorMessage` displays an inline alert and sets the invalid state automatically. `isInvalid` can also set the state without an error string, for example when a separately rendered error summary explains the problem. The component displays validation supplied by the host; it does not decide whether an email, URL, or identifier is valid for your application. For a controlled field, pass `value` and update it from `event.target.value` in `onChange`. For an uncontrolled example, use `defaultValue`. Keep the user's draft after a failed submission and explain how to repair the value. ## Choose read-only or disabled behavior | Need | Use | | --- | --- | | Show a value that can be focused and copied but not edited | Native `readOnly`; it automatically selects the borderless read-only appearance unless you specify a variant. | | Keep the outlined appearance while preventing edits | `readOnly` with `variant="default"`. | | Make the control unavailable | `disabled`. | | Explain why it is unavailable on hover and focus | `disabled` with `disabledReason`. | `variant="readOnly"` changes styling only. Pass `readOnly` as well to prevent editing. A disabled field with a reason stays focusable and read-only with `aria-disabled`; native disabled fields leave the tab order and are excluded from normal form submission. Account for that difference when reading form values. ## Distinguish account passwords from secrets For an account sign-in field, use `type="password"` with `autoComplete="current-password"`. For a new account password, use `autoComplete="new-password"`. Explicit autocomplete lets these fields retain normal password-manager behavior. For an API key or token, use `sensitive`. A password field with no explicit `autoComplete` is also treated as sensitive. This branch uses a text input masked with CSS, `autocomplete="off"`, and password-manager opt-out hints. These reduce unwanted autofill; they do not encrypt the value or guarantee that every browser extension ignores it. Never use a real secret in a demonstration. The reveal toggle is enabled by default for password or sensitive fields. Set `passwordToggle={false}` to hide it. In the example, enter a sample value and use **Show password** and **Hide password** to inspect the two states. ## Props | Prop | Type or default | Purpose | | --- | --- | --- | | `label` | Optional string | Visible field name; supply another accessible name if omitted. | | `description`, `hint` | Optional React content | Context above and below the control. | | `errorMessage` | Optional string | Error text and invalid state. | | `isInvalid` | Optional boolean | Additional way to mark invalid. | | `variant` | `default`, `unstyled`, `readOnly` | Appearance; native `readOnly` automatically selects the last when no variant is set. | | `passwordToggle` | `true` | Reveal control for a password or sensitive value. | | `sensitive` | Optional boolean | Secret-entry behavior described above. | | `disabledReason` | Optional React content | Explanation while `disabled`. | | `prefix`, `suffix` | Optional React content | Fixed text inside the outlined field; do not combine with the password toggle. | | `labelInfo` | Optional React content | Additional label tooltip. | | `wideControl` | `false` | Lets the control fill a layout-owned frame instead of the settings control column. | | `wrapperClassName` | Optional string | Classes on `FieldShell`. | Other native input attributes pass through, except native `size`; `prefix` is reserved for the component's fixed addon. A prefix or suffix is visual context, not part of the submitted input value. Your host must construct and validate any combined value. ## Layout and related controls `ContentArea variant="narrow"` selects the shared settings field layout: stacked on small screens, label beside control from `sm`. See [Settings page](/docs/patterns/settings-page) before adding per-field widths. Use `Textarea` for multiple lines, `Select` for a fixed set, `SearchableSelect` for a searchable set, `JsonInput` for structured JSON, and `CopyableField` for a value primarily meant to be copied. A table search belongs in [`DataTable.search`](/docs/components/data-table), where it can stay associated with the filtered results. # Tabs and navigation Source: https://ui.tale.dev/docs/components/tabs-and-navigation Use `Tabs` to switch panels within a screen. Use `TabNavigation` for destinations that should support a URL, reload, and browser history. Similar visual treatments do not make these controls interchangeable. ```tsx import { Tabs } from '@tale/ui/tabs'; import { TabNavigation } from '@tale/ui/tab-navigation'; ``` ## Switch local panels Choose **Skills** or **Runs**. The visible panel changes without a URL change. Focus a tab and use the arrow keys to move through the strip. The Radix-based component supplies tab roles and panel associations. Each item has a stable `value`, a `label`, and optional `content`. Use `defaultValue` for local state, or `value` and `onValueChange` when the host controls the selection. Supply an initial selection explicitly; do not assume the first item becomes selected automatically. The `underline` variant suits a section strip. The default `pill` variant suits a smaller switch inside a panel: ## Preserve panel state intentionally Inactive panel content unmounts by default. Returning to a panel can therefore reset its local form or expanded state. Set `keepMounted` to retain the mounted panels and hide inactive ones instead. Mounted hidden content can still run hooks and subscriptions. Use this option when state should survive, and make loading or polling rules explicit in the panel. If the draft must survive route changes or reloads too, keep it in a host-owned store or persistence layer; `keepMounted` only addresses tab-panel mounting. | `Tabs` prop | Default and purpose | | --- | --- | | `items` | Required `TabItem[]`: `value`, `label`, optional `content`, `disabled`, `ariaLabel`. | | `variant` | `pill`; alternatively `underline`. | | `value`, `defaultValue`, `onValueChange` | Controlled state or initial local selection. | | `listAriaLabel` | Accessible name for the tab list. | | `equalWidth` | `false`; distributes items across the available list width. | | `actions`, `toolbar` | Trailing controls or a separate row beneath the strip. | | `overflowMenu` | `false`; folds tabs that no longer fit into a menu when enabled. | | `overflowMenuLabel` | Defaults to `More`; pass a localized label in translated screens. | | `keepMounted` | `false`; retains hidden panel content when enabled. | | `className`, `listClassName`, `triggerClassName` | Targeted layout customization. | ## Navigate between routes `TabNavigation` requires TanStack Router context. Its items carry `label` and `href`, and the component reads the current location. Supply resolved URLs: unlike `LinkButton`, an item has no separate `params` prop to fill a `$id` placeholder. This composition excerpt assumes your application defines the listed routes and supplies `projectId`: ```tsx ``` The default `matchMode` is `startsWith`; choose `exact` for an overview route that would otherwise also match its children. `additionalActivePaths` keeps an item active for related sibling routes. `isActive` overrides path matching, useful when several views share a pathname and differ by a search parameter. | `TabNavigation` option | Behavior | | --- | --- | | `search` on an item | Search parameters passed with that destination. | | `disabled` on an item | Keeps the label visible without navigation. | | `trailing` on an item | Optional badge or other supporting content. | | `prefetch` | Defaults to `true`; preloads on intent. | | `standalone` | Defaults to `true`; set `false` inside an existing `StickyHeader`. | | `overflow` | Defaults to `scroll`; `menu` folds the trailing destinations into a menu. | | `dirtyKeys` | A `ReadonlySet` from the editor, intersected with each item's `dirtyKeys` array to show an unsaved-change dot. | ## Check navigation on a narrow screen Do not assume a route strip always becomes a dropdown on mobile: horizontal scrolling is the default. Select `overflow="menu"` when a growing list of destinations needs a menu. Trailing route-strip actions move to the mobile floating-action area below `md`; leave room for that area in the content layout. Name local tab lists with `listAriaLabel` and route landmarks with `ariaLabel`. Name icon-only local tabs with the item's `ariaLabel`. The active route uses `aria-current`, while a local selected tab uses `aria-selected`. Route tabs can use the host accent context; local `Tabs` uses its own theme classes. Use a [settings-page editor](/docs/patterns/settings-page) to protect drafts when leaving a route. A dirty dot communicates state but does not block navigation by itself. # Toast Source: https://ui.tale.dev/docs/components/toast Use a toast for brief feedback after an action, such as a successful save. Keep instructions, repairable errors, and progress that someone must revisit in the page itself. A transient message is not a history of completed operations. ## Trigger a notification Choose **Save settings**. This example shows a notification only; it does not save organization settings. The title states the result, and the optional description supplies one useful detail. ```tsx import { Button } from '@tale/ui/button'; import { useToast } from '@tale/ui/use-toast'; export function SaveNoticeDemo() { const { toast } = useToast(); return ( ); } ``` In an application, call `toast` after the operation succeeds, not merely when the button is pressed. For a failed save, retain the draft and show a persistent explanation near the affected fields. ## Mount one viewport ```tsx import { Toaster } from '@tale/ui/toaster'; // Inside your AppShell, beside the router or application content: ; ``` Mount one `Toaster` for the application. `AppShell` does not mount it automatically. The examples on this site share the root viewport; their source files only trigger notifications. The toast store is shared across callers and holds one current notification. A new toast replaces the previous one. Two mounted Toasters subscribe to that same store and render duplicate messages, rather than creating two isolated queues. ## Choose the message type | Variant | Presentation and use | | --- | --- | | `default` | Neutral text, without a leading status icon. | | `success` | A check icon for successful completion. | | `destructive` | An error icon for a failed operation; keep actionable recovery available elsewhere. | `position` is `top-right` by default or `top-center`. The current toast determines the viewport position. Keep placement consistent within a workflow. ## Timing and programmatic control The default duration is five seconds. Radix pauses dismissal while the notification is hovered or focused and when the window loses focus. A toast can also be dismissed by a swipe. The component does not render a close button. A `duration` can be supplied per toast. A longer duration alone does not make time-sensitive information accessible to everyone; information needed to continue should remain available in a persistent surface. See W3C's [timing-adjustable guidance](https://www.w3.org/WAI/WCAG21/Understanding/timing-adjustable.html). `useToast()` returns `toast`, `dismiss`, and the current `toasts`. Creating a toast returns its `id`, a scoped `dismiss`, and `update`. Use these handles for an operation-specific change rather than relying on a notification's position in the store. An `action` can hold a React action element, but an essential action should also have a stable home in the application. ## Review the complete feedback path Trigger two notices in quick succession and confirm only the latest remains. Check that a notice does not cover the action needed next, that keyboard focus stays where the task expects, and that the same information has a persistent location when required. Use `Input.errorMessage` for a field issue, `Alert` for a persistent page condition, and [Dialog](/docs/components/dialog) for a decision. Use inline status for a long operation whose progress and outcome need to remain visible. # Accessibility Source: https://ui.tale.dev/docs/foundations/accessibility Tale targets WCAG 2.1 AA. Shared components provide useful semantics and interaction, but a complete screen still needs a human review: labels, focus order, contrast, and recovery depend on how you compose them. ## Start with names and structure Give each page one accessible `h1`, then organize its sections without skipping heading levels. The application header can supply that title; do not duplicate it in the body or omit it because the page appears in navigation. | Surface | What you supply | | --- | --- | | Text button | A meaningful visible action label. | | Icon button | `aria-label`; an icon-sized `Button` also accepts `title` as its name. | | Input | `label`, or a deliberate accessible name when a visible label is supplied elsewhere. | | Dialog | A required title, useful context, and named controls. | | Data table | A descriptive caption and understandable column headings. | | Tabs or navigation | A name for the list/landmark and clear destination labels. | A tooltip alone does not name a control. Decorative icons beside text should be hidden from assistive technology. Validation should include readable error text, not only a red border; [`Input`](/docs/components/input) associates that feedback with its control. ## Walk the keyboard path Use Tab and Shift+Tab to move through this example. Focus should be visible and its order should follow the task. Try Enter or Space on actions and arrow keys within composite controls such as tabs and menus. Add a skip link before repeated navigation: ```tsx import { SkipLink } from '@tale/ui/skip-link'; export function PageFrame() { return ( <> Skip to main content

Example page

); } ``` The target must accept programmatic focus. `tabIndex={-1}` makes the main region focusable without adding another ordinary Tab stop. Test the first Tab after a fresh deep-link load: initial scrolling or autofocus must not unexpectedly bypass the skip link. ## Check overlays and disabled controls A modal dialog traps focus while open and should return it to a useful opener on close. If that opener unmounts, supply a stable `restoreFocusRef`. Test Escape, explicit cancellation, completion, and a failed request. Nonmodal tooltips and popovers do not all share modal focus behavior; do not assume every overlay traps focus. `disabledReason` keeps supported disabled controls reachable so people can discover why an action is unavailable. Check both the tooltip and the blocked action. Keep essential instructions visible outside the tooltip as well. ## Measure contrast in the rendered state For ordinary text, check 4.5:1 contrast; large text and meaningful non-text UI have different criteria. Use semantic foreground/background pairs, then measure the actual pairing in light and dark themes. Muted text, placeholder text, status colors, and host accents deserve particular attention. The package's token names are not a guarantee that every combination passes. Opacity, an image underneath, a hover fill, or a disabled treatment can change the result. Do not carry a ratio measured against one surface over to another. [Colours](/docs/foundations/colors) explains the available pairings. ## Respect motion and timing preferences The shared stylesheet reduces CSS animation and transition durations and disables smooth scrolling under `prefers-reduced-motion: reduce`. JavaScript-driven motion needs its own handling: use the motion library's reduced-motion support, and request instant scrolling when the preference is active. A disappearing toast must not be the only location for information someone needs to continue. A five-second timeout alone does not establish compliance with [WCAG timing requirements](https://www.w3.org/WAI/WCAG21/Understanding/timing-adjustable.html). ## Combine automated and manual checks Type checks catch missing required icon labels; lint checks catch many invalid roles and associations. Component tests use `checkAccessibility()` with axe. That helper disables contrast checking in jsdom because it has no real layout or painted colors. Before shipping a page, complete its main task with the keyboard, inspect it at a narrow width and increased text size, test both themes and reduced motion, and verify focus after dialogs and navigation. Include error and empty states. An automated axe pass is evidence about the rules it can inspect, not proof that the entire experience is accessible. # Colours Source: https://ui.tale.dev/docs/foundations/colors Choose a color token for its role: page surface, secondary text, input edge, or status. This lets the shared stylesheet adjust the appearance centrally without changing each component. Avoid introducing raw palette colors in new UI code when an existing semantic token fits. ## Compare the two themes Use the header theme menu to switch between **Light** and **Dark**. The swatches retain their class names while their values change. Check text against the surface it actually sits on, rather than judging an isolated swatch. ## Canonical semantic tokens These tokens are declared in `@theme` in `packages/ui/src/globals.css`. The surface, text, border, and accent families have dark overrides. | Utility | Intended role | | --- | --- | | `bg-bg-base` | Main component surface. | | `bg-bg-elevated` | Raised or hovered surface. | | `bg-bg-muted` | Quiet inset area. | | `bg-bg-overlay` | Backdrop over other content. | | `text-fg-base` | Primary text. | | `text-fg-muted` | Supporting description. | | `text-fg-subtle` | Metadata or captions; check the actual background. | | `text-fg-inverse` | Text on a suitable inverted fill. | | `border-border-base` | Dividers and ordinary surface edges. | | `border-border-strong` | Stronger boundary treatment. | | `border-border-input` | Form-control outline. | | `bg-accent-base` with `text-accent-fg` | Primary neutral action fill and its foreground. | `border-border-input` resolves to the strong border in light mode and the base border in dark mode. Use the input primitive for its full outline/focus treatment rather than approximating the field with a plain divider border. The canonical status tints include `bg-success-bg`, `bg-warning-bg`, `bg-danger-bg`, `text-danger`, and `bg-info-bg`. These values do not all have dark overrides. Pair them deliberately, or use a shared status component that already applies the intended treatment. ## HSL-compatible aliases The stylesheet also exposes the familiar HSL token family used by many existing components: | Utilities | Intended pairing | | --- | --- | | `bg-background` and `text-foreground` | Page surface and primary text. | | `bg-card` and `text-card-foreground` | Card surface and text. | | `bg-popover` and `text-popover-foreground` | Menu or popover surface and text. | | `bg-primary` and `text-primary-foreground` | Primary fill and text. | | `bg-secondary` and `text-secondary-foreground` | Secondary fill and text. | | `bg-muted` and `text-muted-foreground` | Quiet fill and supporting text. | | `bg-accent` and `text-accent-foreground` | Highlighted surface and text. | | `text-destructive` | Error text or a destructive affordance. | | `bg-success`, `bg-warning`, `bg-info` | Status fills, each with a corresponding `-foreground` token. | | `border-border`, `bg-input`, `ring-ring` | Ordinary border, field fill, and focus-ring role. | | `bg-sidebar`, `bg-tab` | Navigation and selected-tab surfaces. | Follow the existing file's family rather than partially converting its colors while adding an unrelated feature. The canonical and HSL names are supported vocabularies, not interchangeable values you can mix without checking the result. ## Use chart-specific colors Charts have `chart-1` through `chart-5` plus `chart-success`, `chart-failure`, `chart-warning`, `chart-neutral`, and `chart-primary`. CSS consumers can read `var(--color-chart-1)` and its siblings; Tailwind exposes the corresponding color utilities. Use labels, shapes, or line patterns as well as color to distinguish meaningful series. Verify legends and tooltips in both themes, including small text and a single low-value data point. ## Add a color only when the meaning is missing First inspect the existing tokens and neighboring components. If a new semantic role is needed, add its token and intended theme treatment centrally. Record which foreground/background pairing it supports and check rest, hover, focus, selected, and error states. A host accent is a separate runtime input used by participating components; see [Theming](/docs/getting-started/theming). Neither an accent color nor a semantic token removes the need for the rendered [accessibility review](/docs/foundations/accessibility). # Icons Source: https://ui.tale.dev/docs/foundations/icons Use Lucide for interface glyphs and the package's existing custom marks for supported brands. Choose the icon from the action's meaning, then decide whether it supplements visible text or carries the whole label. ## Choose the icon size | Class | Typical use | | --- | --- | | `size-3` | Small inline metadata. | | `size-4` | Standard button, row, and navigation icons. | | `size-5` | A more prominent standalone control or status. | | `size-6` | A feature or empty-state glyph. | Lucide's normal stroke is the starting point. `Button.icon` uses `size-4`; `IconButton` accepts `iconSize={3 | 4 | 5 | 6}` and defaults to 4. Changing the glyph size does not change the button's pointer target. ## Hide decoration, name controls ```tsx import { Button } from '@tale/ui/button'; import { IconButton } from '@tale/ui/icon-button'; import { Download, Search } from 'lucide-react'; export function FileActions() { return (
); } ``` The Export icon is decorative because the visible text already names the action. Button hides that icon from assistive technology. The search control has no visible text, so its `aria-label` supplies the name and the shared IconButton uses that label for its tooltip. `IconButton` requires `aria-label` at the type level. An icon-sized `Button` accepts `aria-label` or `title`; in that specific case, `title` supplies both an accessible name and tooltip. A generic Tooltip only supplies a description. Do not rely on its text to name an otherwise unnamed button. For a stateful action, name the next action or communicate the current state clearly. A changed glyph alone may be ambiguous; use appropriate state attributes such as `aria-pressed` where the control is a toggle. ## Reuse custom brand marks ```tsx import { IconButton } from '@tale/ui/icon-button'; import { GithubIcon } from '@tale/ui/icons/github'; export function SourceLink() { return ( } /> ); } ``` The package exposes GitHub, Claude, Google, Microsoft, Gmail, Google Drive, OneDrive, Outlook, SharePoint, Shopify, website, enter-key, and locale-flag marks through its exported icon paths. Check `packages/ui/package.json` for the exact subpath; names such as `github` and `google-drive-icon` are not uniform enough to guess safely. Before drawing a new mark, search the package and Lucide. A new shared icon should match the existing prop/ref convention, carry no embedded screen-specific label, and be tested at its intended sizes. ## Keep the target larger than the glyph An IconButton is 36px square by default or 32px with `size="sm"`. Keep that hit area even when the glyph is only 12px. On touch-heavy layouts, favor additional space around important actions and inspect neighboring targets for accidental activation. WCAG 2.2 AA [Target Size (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html) uses 24×24 CSS pixels with specified exceptions. WCAG 2.1's [Target Size criterion](https://www.w3.org/WAI/WCAG21/Understanding/target-size.html) is the 44×44 AAA criterion. These are different requirements; do not cite the latter as a 24px rule. Check focus visibility in both themes and verify that an icon inside an overlay does not make Escape or focus restoration confusing. [Accessibility](/docs/foundations/accessibility) covers the full page review. # Spacing and layout Source: https://ui.tale.dev/docs/foundations/spacing-and-layout Build layout from the shared spacing scale and containers. Consistent control heights and section gaps make related fields and actions easier to scan; responsive behavior still depends on how the full page is composed. ## Align ordinary controls The ordinary app control height is 36px (`h-9`). Buttons also offer a 32px (`h-8`) dense variant; their icon-sized counterparts are square. Input has no size axis. Match neighboring controls through these APIs rather than assigning individual pixel heights. The primary app header row is 52px (`h-13`). Other primitives, such as text-style link buttons and multiline fields, have different footprints. Do not force a multiline control into the single-line height. ## Use a deliberate gap scale ```tsx import { Grid, Row, Stack } from '@tale/ui/layout'; export function SectionLayout() { return ( {/* Related actions */} {/* Two responsive groups */} ); } ``` | Gap | Typical use | | --- | --- | | `2` | Tightly related items or field content. | | `4` | Items within a section; the layout primitives' default. | | `6` | A more open group. | | `8` | Separation between sections. | The full scale is `0`, `1`, `2`, `3`, `4`, `5`, `6`, `8`, `10`, `12`. Prefer the recommended steps for new app layouts; `5`, `10`, and `12` remain for existing compositions. `Stack`, `Row`, and `Grid` accept a semantic `as` element or `asChild` for one child. Choose one, rather than combining them. `Row` does not wrap by default. Enable `wrap` for action groups that should form another line; give flexible text children `min-w-0` where truncation or wrapping must work. `Grid` accepts `sm`, `md`, `lg`, and `xl` column overrides. ## Choose the content measure | Container | Use | | --- | --- | | `ContentArea` with `page` | Normal application content; shared padding and gaps. | | `ContentArea` with `narrow` | Centered configuration column, capped at `max-w-3xl`, with settings field layout. | | `ContentArea` with `panel` | Content inside a secondary panel. | | `NarrowContainer` | A centered form column capped at 544px. | | `Container` | Generic width-constrained content, with `md`, `lg`, `xl`, or `full` sizing. | `ContentArea` defaults to `page` and `gap={6}`. Its narrow variant also declares the `FieldShell` row layout: stacked labels on small screens and a shared control column from `sm`. It includes bottom clearance for mobile floating actions; avoid replacing that clearance with ad hoc padding. ## Use Card for a bordered object ```tsx import { Card, CardContent, CardHeader, CardTitle } from '@tale/ui/card'; export function MemberCard() { return ( Members Three members have access. ); } ``` This example assumes the card sits under an `h2` section because `CardTitle` is an `h3`. Card owns its padding; the header and content slots do not add their own padding. | Option | Values and default | | --- | --- | | `padding` | `none`, `sm` (12px), `md` (16px), `lg` (20px), `xl` (24px); default `xl`. | | `radius` | `lg` (normally 8px) or `xl` (16px); default `lg`. | | `shadow` | `none`, `sm`, `md`; default `none`. | | `interactive` | Hover/focus styling; default `false`. | | `asChild` | Merge the frame onto one child element; default `false`. | `interactive` does not turn a `div` into a keyboard-operable control. For a linked or clickable card, compose it onto a real anchor, router link, or button with `asChild` and an appropriate name. The shared radius tokens are `rounded-sm` 6px, `rounded-md` 8px, `rounded-lg` from the normal 8px radius variable, and `rounded-xl` 16px. Prefer component variants over rebuilding their edge and fill styles manually. ## Check the whole layout Test a long title, longer translated labels, an open side panel, and a phone width. Keep wide tables and code scrollable inside their containers rather than forcing the entire page sideways. Check that fixed actions leave the final field reachable. Use [App shell](/docs/components/app-shell) for page composition and [Settings page](/docs/patterns/settings-page) for draft/save behavior. # Typography Source: https://ui.tale.dev/docs/foundations/typography Use `Heading` for section headings and `Text` for common body, label, caption, and feedback styles. Select the HTML element from the content's role, then choose its visual size. A small heading remains a heading; large text is not automatically a page title. ## Compare the text scale Inspect the headings and supporting text in both themes. Use ordinary body text for instructions someone needs to read, and reserve captions for secondary metadata rather than shrinking important information to fit. ## Choose the heading level separately ```tsx import { Heading } from '@tale/ui/heading'; export function MemberSectionTitle() { return Members; } ``` | Prop | Values | Default | | --- | --- | --- | | `level` | `1` through `6`; selects the heading element. | `2` | | `size` | `xs`, `sm`, `base`, `lg`, `xl`, `2xl` | `base` | | `weight` | `medium`, `semibold`, `bold` | `semibold` | | `tracking` | `tighter`, `tight`, `normal` | Unset | | `truncate` | Adds an ellipsis and allows a flex child to shrink. | `false` | Use one accessible `h1` for the page. In an application layout, the header title or breadcrumb leaf usually supplies it, so body sections begin at `h2`. A settings rail label is navigation, not a replacement for the heading. `CardTitle` renders an `h3`. Use it under an `h2` section; if that is the wrong depth, choose an explicit `Heading` inside the card instead of accepting a skipped level. ## Pick a text variant ```tsx import { Text } from '@tale/ui/text'; export function DigestDescription() { return Send a weekly summary to the selected members.; } ``` | Variant | Treatment | | --- | --- | | `body` (default) | Primary text, `text-sm`. | | `body-sm` | Primary text, `text-xs`. | | `muted` | Supporting text, `text-sm`. | | `caption` | Supporting metadata, `text-xs`. | | `label`, `label-sm` | Medium-weight primary text, at small or extra-small size. | | `code` | Monospace, `text-xs`. | | `error`, `error-sm` | Destructive-color feedback, at small or extra-small size. | | `success` | Medium-weight success feedback, `text-sm`. | `Text` defaults to a paragraph. Its `as` prop accepts `p`, `span`, `div`, `label`, or `h3`; `align` accepts `left`, `center`, or `right`. Changing to `label` does not associate the text with a control by itself: use a form component's label API or provide a valid association. Use truncation for compact navigation or metadata only when the full content remains discoverable. Avoid truncating instructions, errors, or the only meaningful name of a record. ## Load the intended font `AppShell` imports Inter at weights 400, 500, 600, and 700 from the shared `fonts.ts` module. The font assets are self-hosted through the application build. Latin weights 400 and 500 are preloaded; a metric-adjusted Arial fallback reduces layout movement while the font loads. Preloading reduces delay but does not guarantee that a fallback is never visible. If text uses the fallback after loading, inspect the emitted font requests and confirm the app entry mounts `AppShell`. Do not add an unrelated remote font import to hide a broken asset path. Monospace uses the system stack: `ui-monospace`, SFMono-Regular, Menlo, Monaco, Consolas, then monospace. ## Use the marketing scale on public pages `@tale/marketing-ui/section-heading` uses the same typeface with larger, normal-weight display styles. Its display size defaults to `h1`; section and subsection sizes default to `h2`. Choose the semantic `as` level explicitly when nesting it. See [Marketing UI](/docs/marketing-ui/overview) for the surrounding layout. # Internationalization Source: https://ui.tale.dev/docs/getting-started/i18n A screen has two kinds of text. Your application supplies its own titles, field labels, and actions through props or children. Shared components translate the copy they own, such as a dialog's close control or an input's password toggle. Neither kind is translated automatically from an English string. An empty `Button` has no useful name. Pass visible text, or name an icon-only control explicitly. ## Initialize the catalogs The [first-control example](/docs/getting-started/installation) shows a complete minimal setup. A service with its own YAML catalogs uses this configuration, with paths relative to that service's i18n module: ```ts import { initServiceI18n } from '@tale/ui/i18n/init-service'; import { uiMessages } from '@tale/ui/i18n/messages'; import enMessages from '../../messages/en.yml'; import deMessages from '../../messages/de.yml'; import frMessages from '../../messages/fr.yml'; import globalMessages from '../../messages/global.yml'; export const i18n = initServiceI18n({ bundles: { en: enMessages, de: deMessages, fr: frMessages }, regional: import.meta.glob('../../messages/*-*.yml', { eager: true, import: 'default', }), global: globalMessages, packages: [uiMessages], }); ``` Register the YAML Vite plugin from Installation. The glob must remain a literal at the call site. Add `marketingUiMessages` from `@tale/marketing-ui/i18n/messages` to `packages` when rendering that package. | Input | Purpose | | --- | --- | | `bundles` | The service's complete base-locale namespace trees. | | `packages` | Shared catalogs, merged before the service's catalogs. Later package entries override earlier ones per key. | | `regional` | Sparse regional overrides, such as `de-CH`, discovered by the glob. | | `global` | Locale-neutral keys shared across locales, such as product names. | The merge is deep: overriding one service key preserves sibling keys from the package. A service override wins over the corresponding package key. Use that ability deliberately; changing a shared action label can affect multiple controls. ## Read a translated label ```tsx import { Button } from '@tale/ui/button'; import { useT } from '@tale/ui/i18n/client'; export function SaveButton() { const { t } = useT('common'); return ; } ``` `useT` wraps `react-i18next` for a namespace. Services can expose a typed wrapper narrowed to their own namespace catalog. Keep interpolation variables separate from translated prose; never assemble a sentence from separately translated fragments. ICU handles counts and interpolation: ```yaml members: count: "{count, plural, one {# member} other {# members}}" ``` Read it with `t('count', { count })` in the `members` namespace. Supply equivalent native plural forms in each locale and test zero, one, and multiple items. ## Choose one locale source For preference-driven apps, use `AppShell` with `locale={{ mode: 'client' }}`. Its locale provider detects the saved preference and browser language; the bridge synchronizes i18n. For URL-driven sites, omit that option and mount `LocaleSync` from the root route with the route's locale. Do not run two competing locale sources. This component documentation site deliberately pins `LocaleSync` to English: its English pages and examples do not have translated routes, even though its chrome catalogs contain EN, DE, and FR keys. ## Add or change copy 1. Find the component that owns the text. Put reusable control copy in the package catalog; put screen-specific copy in the service catalog. 2. Update `en`, `de`, and `fr` together. Use `de-CH` only where a regional override is needed, rather than copying the entire German catalog. 3. Write each locale from the intended meaning. Tale uses `du` in German and `tu` in French. Preserve values, interpolation keys, and conditions while making the sentence natural. 4. Check the actual state that renders the key: loading, disabled, success, error, or empty. Inspect long labels on a narrow screen. ## Check catalog wiring Services use `defineI18nTests` from `@tale/ui/i18n/tests`. Its `packageCatalogs` option lists the package message directories merged at runtime. These directories inform missing-key checks; package-owned tests remain responsible for orphan keys inside the package. If a component shows a dotted key, first check its namespace and spelling, then the runtime `packages` list and service override. If tests alone report a shared key missing, compare `packageCatalogs` with the runtime configuration. Run the service's tests after a catalog change; key parity cannot judge native grammar or whether the label fits its control. # Installation Source: https://ui.tale.dev/docs/getting-started/installation The packages ship React and TypeScript source. A working installation needs a compatible source-consuming build tool, the shared stylesheet, the package message catalog, and `AppShell`. The examples below use Tale's Bun, Vite, React 19, and Tailwind 4 setup. ## Add the package Inside this monorepo, add the workspace dependency to the service that uses it and run `bun install` from the repository root: ```json { "dependencies": { "@tale/ui": "workspace:*" } } ``` Add `"@tale/marketing-ui": "workspace:*"` only if that service renders marketing components. Existing services may already have these dependencies; check their `package.json` before changing it. ### Consume it from another repository The release workflow publishes each package as a root-level Git snapshot. For a trial installation: ```bash bun add 'github:tale-project/tale#dist/ui' react@19 react-dom@19 tailwindcss@4 bun add --dev vite @vitejs/plugin-react @tailwindcss/vite typescript @types/react @types/react-dom ``` For marketing components, also install `github:tale-project/tale#dist/marketing-ui`. That package requires `@tale/ui` as a peer. For a reproducible release, replace the moving branch with a published `ui-v` tag and, when used, its matching `marketing-ui-v` tag. `` is a placeholder, not a tag to install literally. Commit the resulting lockfile. To deliberately refresh a moving Git dependency, use `bun install --force` and review the lockfile changes. The packages' export maps point at TypeScript source. Run Vite through Bun (`bun --bun vite` and `bun --bun vite build`), as the Tale service scripts do. Use TypeScript's bundler module resolution and React JSX transform. Inside Tale-project repositories, select the appropriate shared `tsconfig` family rather than adding workspace-specific compiler options. ## Load Tailwind and YAML This Vite configuration handles React, Tailwind, and the YAML files imported by the translation catalogs: ```ts // vite.config.ts import { yamlImports } from '@tale/ui/vite/yaml'; import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [yamlImports(), react(), tailwindcss()], }); ``` Load the application stylesheet from your entry module: ```css /* src/globals.css */ @import '@tale/ui/globals.css'; ``` For a marketing site, use `@import '@tale/marketing-ui/globals.css';` instead. Each stylesheet declares the package source that Tailwind must scan. Keep your own application source within Tailwind's scan as well. ## Render a first control This complete client entry assumes `index.html` contains `
`. It uses the package's translations with empty service catalogs; replace those catalogs when you add application-specific copy. ```tsx // src/main.tsx import { AppShell } from '@tale/ui/app-shell'; import { Button } from '@tale/ui/button'; import { initServiceI18n } from '@tale/ui/i18n/init-service'; import { uiMessages } from '@tale/ui/i18n/messages'; import { Input } from '@tale/ui/input'; import { createRoot } from 'react-dom/client'; import './globals.css'; const i18n = initServiceI18n({ bundles: { en: {}, de: {}, fr: {} }, regional: {}, packages: [uiMessages], }); const root = document.getElementById('root'); if (!root) throw new Error('Missing root element'); createRoot(root).render(
, ); ``` `AppShell` mounts the translation and tooltip providers and imports Inter. `theme` enables light, dark, and system preferences. Client locale mode detects a saved or browser language. A URL-driven site should omit client locale mode and synchronize its route language instead; see [Internationalization](/docs/getting-started/i18n). Start the development server with `bun --bun vite`. The input should have a visible outline, the button should be 36px high, and its tooltip should appear on keyboard focus or hover. The button deliberately has no save callback. ## Troubleshoot setup | Symptom | Check | | --- | --- | | Components render without styling | Import the stylesheet from the entry that Vite actually loads, and enable the Tailwind plugin. | | YAML import or parse errors | Register `yamlImports()` before importing the service or package catalogs. | | A label renders as a dotted key | Include `uiMessages` in `packages`; add `marketingUiMessages` when rendering marketing components. | | Theme controls do nothing | Mount `AppShell` with `theme` and import the themed stylesheet. | | A package import fails | Check its public export path and the installed snapshot; avoid private filesystem imports. | | A router link fails outside routing context | Mount your router before using `LinkButton` or `TabNavigation`; the first-control example above needs no router. | PWA and Storybook support have additional optional peers. Install them only when using those exported integrations, following the package's `peerDependencies` and `peerDependenciesMeta`. # Introduction Source: https://ui.tale.dev/docs/getting-started/introduction Use `@tale/ui` to build application screens: forms, tables, dialogs, navigation, and the providers behind them. Use `@tale/marketing-ui` for public websites: page sections, calls to action, site navigation, and product illustrations. The marketing package builds on the application package, so you install both when building a marketing site. ## Start with your task | You want to… | Start here | | --- | --- | | Render your first Tale component | [Installation](/docs/getting-started/installation) | | Add an action or edit a value | [Button](/docs/components/button) and [Input](/docs/components/input) | | Build a searchable collection | [Data table](/docs/components/data-table), then [List page](/docs/patterns/list-page) | | Build configuration with Save and Discard | [Settings page](/docs/patterns/settings-page) | | Add dark mode or translate component labels | [Theming](/docs/getting-started/theming) and [Internationalization](/docs/getting-started/i18n) | | Build a public product page | [Marketing UI](/docs/marketing-ui/overview) | ## Choose the package by the screen's purpose Application screens favor compact controls, readable status, and predictable placement for repeated tasks. Marketing pages use larger headings, wider sections, and pill-shaped calls to action. This site's documentation uses the application components; its [home page](/) uses marketing components. | Package | Typical building blocks | Stylesheet | | --- | --- | --- | | `@tale/ui` | `Button`, `Input`, `DataTable`, `Dialog`, `PageLayout` | `@tale/ui/globals.css` | | `@tale/marketing-ui` | `MarketingButton`, `SectionHeading`, feature sections, `DemoShell` | `@tale/marketing-ui/globals.css` | The marketing stylesheet imports the application stylesheet. Load the stylesheet for your site once; you do not need both imports. Both packages ship TypeScript source. Import documented package subpaths, such as `@tale/ui/button`, rather than reaching into another workspace's `src` directory. The package's `exports` map defines which paths consumers can use. ## Use the examples Each **Live example** renders package components. Choose **Code** to inspect the source for that example, and **Hide code** to close it. You can change inputs, open dialogs, and switch tabs without connecting a Tale backend. Examples demonstrate UI behavior with local sample data. They do not send invitations, connect providers, or save organization settings. Page-layout and marketing-window examples are labelled illustrations: their contents are deliberately inert so a second application header and its controls do not enter the page's keyboard or screen-reader navigation. Use the linked component examples for interaction. The guides and examples are currently in English. The packages support translated interface text; [Internationalization](/docs/getting-started/i18n) explains how to supply it in your application. ## Keep business rules in the service The shared package owns reusable presentation and interaction. Your service owns authorization, network requests, validation rules specific to its domain, and persistence. For example, `DataTable` displays the rows you pass; it does not fetch organization members. `ConfirmDialog` asks for a decision; your callback decides whether and how to delete a record. Pass screen-specific titles and labels as props. Shared labels such as a dialog's close control come from the package catalog. Before introducing a new component, check the existing exports and [composition patterns](/docs/patterns/list-page). A service wrapper around a shared component is the appropriate place for a backend query or permission check. # Theming Source: https://ui.tale.dev/docs/getting-started/theming Semantic color tokens let a component keep the same classes in light and dark themes. Mount the shared theme provider, then choose colors by their purpose rather than their current appearance. ## Enable theme switching [Installation](/docs/getting-started/installation) mounts the provider with ``. Its default choice is `system`; `ThemeProvider` resolves the operating system preference and applies `.dark` to the document when needed. An explicit choice is saved under `tale-theme` in local storage. ```tsx import { ThemeSwitcher } from '@tale/ui/theme-switcher'; export function AppearanceControl() { return ; } ``` Try the theme control in this page's header. Choose **Dark**, then **Light**, and compare the swatches below. Choose **System** to follow the operating system again. `ThemeSwitcher` defaults to a menu. Its `segmented` variant presents the choices inline. Both must live inside the provider tree. ## Read the choice or the displayed result ```tsx import { useTheme } from '@tale/ui/theme'; export function ThemeSummary() { const { theme, resolvedTheme, setTheme } = useTheme(); return ( ); } ``` `theme` is `light`, `dark`, or `system`. `resolvedTheme` is the resulting `light` or `dark`. Use the resolved value when selecting an image or chart palette. Querying `prefers-color-scheme` independently would ignore an explicit user choice. The Tailwind `dark:` variant follows the `.dark` class. The provider also updates CSS `color-scheme` and briefly suppresses transitions during a switch. This avoids animating every color on the page at once. ## Choose a token family consistently The stylesheet exposes two supported families: | Family | Example surface | Example secondary text | | --- | --- | --- | | Canonical semantic tokens | `bg-bg-base text-fg-base border-border-base` | `text-fg-muted` | | HSL-compatible aliases | `bg-background text-foreground border-border` | `text-muted-foreground` | Follow the surrounding component's vocabulary. Both resolve through the shared stylesheet; neither requires a second set of light and dark classes at every call site. [Colours](/docs/foundations/colors) maps common tokens to their uses. When extending the token set, define the light and dark values together. Check the actual foreground/background pairing, including hover, disabled, error, and focus states. A semantic name does not by itself prove sufficient contrast. ## Apply a host accent `AccentColorProvider` supplies a runtime accent to components that opt into it, including route-tab indicators and selected sub-panel rows: ```tsx import { AccentColorProvider } from '@tale/ui/accent-color'; {children} ; ``` This is a composition excerpt: the host supplies `children` and a validated color. Without the provider, participating components use their default treatment. The context does not recolor every component or replace all theme tokens; for example, `Tabs` uses its stylesheet classes directly. Keep organization lookup and branding policy in the service. Review an accent on both themes before using it for a meaningful indicator. ## Keep browser assets in sync `ThemeAssets`, mounted inside the theme tree, updates the favicon and theme-color metadata to match an explicit theme choice. Your HTML must provide the elements it updates: `favicon-light`, `favicon-dark`, `theme-color`, and `theme-color-dark`. If the page changes theme but its browser tab icon does not, inspect those IDs and the asset URLs. If only part of a page changes, look for hardcoded colors or an extra theme provider. Use the [accessibility checks](/docs/foundations/accessibility) to verify contrast and reduced-motion behavior in the rendered page. # Marketing UI overview Source: https://ui.tale.dev/docs/marketing-ui/overview `@tale/marketing-ui` supplies the public website's visual language on top of `@tale/ui`: site navigation, section headings, calls to action, content panels, and product-demo frames. Use it for discovery and explanation pages; use application components for repeated work inside the product. ## Compare the primitives The example combines `SectionHeading`, `MarketingButton`, `MarketingPanel`, and `MarketingCard`. The buttons demonstrate styling only. The cards have no `to` destination, so they are static content rather than links. Marketing surfaces use `surface-site` tokens, normal-weight display text, and rounded calls to action. The application package still supplies shared tokens, utilities, and underlying controls. ## Install the layer Install both packages and follow [Installation](/docs/getting-started/installation) for source-consuming Vite setup. Load one stylesheet: ```css @import '@tale/marketing-ui/globals.css'; ``` That file imports `@tale/ui/globals.css` and adds the marketing vocabulary. Include both `uiMessages` and `marketingUiMessages` in `initServiceI18n.packages` so shared controls and demo-window labels resolve. ## Choose a building block | Area | Exported subpaths | | --- | --- | | Site navigation and frame | `site-header`, `site-footer`, `site-container` | | Core page composition | `button`, `link`, `external-link`, `cta-group`, `card`, `panel`, `stack`, `page-section`, `section-heading`, `reveal` | | Feature sections | `feature-hero`, `feature-capability`, `feature-steps`, `feature-faq`, `feature-cta`, `related-pages`, `docs-links` | | Comparison and discovery | `marketing-section`, `tier-card`, `compare-table`, `segmented-radio`, `logo-cloud-section`, `progress-bar` | | Product illustrations | `demo-shell`, `demo-chrome`, `demo-stage`, `demo-tour-row`, `demo-tour-section`, `demo-typing-text`, `demo-stream-text`, `use-demo-timeline` | | Setup | `globals.css`, `routing`, `entrance`, `i18n/messages`, `tailwind-preset` | Import from the named package subpaths. Keep page-specific claims, translated titles, destinations, and scenarios in the host. A reusable feature section should not learn your service's pricing or permissions. ## Make headings and cards semantic `SectionHeading` takes a required `title` plus optional description and eyebrow. `size` chooses `display`, `section`, or `subsection`; `align` is `center` by default or `start`. Display defaults to `h1`, while section and subsection default to `h2`. Use `as` for the correct nested heading level; visual size does not determine the document outline. `MarketingButton` offers `tone="primary"` or `secondary` and `size="default"` or `lg`. Use `asChild` around an appropriate link when the action navigates. `MarketingCard` is static without `to` and becomes an internal link with it. Its surface is `plain` by default, with `raised` and `inset` alternatives. Do not put nested competing links inside a card that is itself a link. ## Connect host routing Internal marketing links use the link component supplied by `MarketingRouterProvider`. Without that provider, the fallback is TanStack Router's Link, which still requires router context. ```tsx import { MarketingRouterProvider, type MarketingLinkComponentProps, } from '@tale/marketing-ui/routing'; function SiteLink({ to, activeProps: _activeProps, ...props }: MarketingLinkComponentProps) { return ; } export function MarketingRoot({ children }: { children: React.ReactNode }) { return {children}; } ``` This plain-anchor adapter works without a client router and deliberately does not apply active-route styling. A localized router adapter should resolve locale prefixes and active styles in the host. Keep the package's `to` value a site path; do not duplicate locale routing inside each card or call to action. ## Present product windows as illustrations `DemoShell` places content inside the product window frame; `DemoStage` supplies the surrounding presentation. The window is one labelled illustration: `role="img"` with an accessible description, and an `aria-hidden`, `inert` payload. Its demo copy is excluded from search snippets with `data-nosnippet`. That treatment lets a marketing page show real components without exposing a second application navigation or a misleading form to keyboard users. Supply a description of what the illustration demonstrates, and keep the explanation outside it complete. If the reader must interact, build an explicitly interactive example instead of placing required controls inside an inert frame. `useDemoTimeline` coordinates animated sequences. Use the shared entrance/reduced-motion utilities, then inspect the result with reduced motion enabled. A static, understandable result should remain when motion is skipped. To inspect the broader frame catalog locally, run `bun run --filter @tale/marketing-ui storybook`. For working application controls, continue with [Button](/docs/components/button), [Dialog](/docs/components/dialog), or [Data table](/docs/components/data-table). # List page Source: https://ui.tale.dev/docs/patterns/list-page A list page helps someone find an item, inspect it, or create another. Start with one page title and a `DataTable` that owns the collection toolbar. Keep data access, permissions, and filter state in your service. ## Inspect the composition This frame is an inert layout illustration. The header contains the title; the table toolbar contains search and the create action. For a working search interaction, use the [Data table example](/docs/components/data-table). The following excerpt assumes the host supplies columns, filtered rows, query state, and `openCreate`: ```tsx Automations } > row.id} caption="Automations" search={{ value: query, onChange: setQuery }} addAction={{ label: 'New automation', onClick: openCreate }} emptyState={{ title: 'No automations yet' }} /> ``` Import these components from their `@tale/ui` subpaths and mount the [adaptive header context and mobile slot](/docs/components/app-shell) in the surrounding application. This fragment is the page body, not a complete app entry point. ## Connect the controls to one data source The search field and filters describe the rows beneath them. Update the query in their handlers, then filter the complete local dataset or request filtered results from the backend. Reset page or cursor state when the query changes so a new search does not start halfway through the old result set. Use `filters`, `dateRange`, and `onClearFilters` for the shared facet controls. Put additional filter-side content in `filtersContent`. Keep shareable filter state in the URL when reloads and copied links should preserve the view. `addAction` creates the primary toolbar affordance; it does not open a dialog by itself. Supply `onClick`, `href`, or menu items and derive availability from the host's permission state. Do not duplicate the same create action in the page header. ## Separate no data, no matches, and a failed request | State | What to communicate | | --- | --- | | Initial request pending | A loading skeleton, using `isLoading` and a meaningful `approxRowCount`. | | Collection has no items | `emptyState` explaining the collection and how to create the first item. | | Active search has no matches | The table's shared no-results state; preserve a way to clear the query. | | More cursor pages could contain matches | Continue loading; do not claim the entire collection has no results yet. | | Request failed | `error` plus `onRetry`, preserving the reader's query. | An unknown approximate count is `undefined`, not zero. Positive counts reserve skeleton rows up to the component cap. A create action moves into the initial empty state only when no search/filter toolbar needs to remain visible. ## Make each row usable Choose stable IDs with `getRowId`. Use a named link or action for the item's destination, even if `onRowClick` also makes pointer navigation convenient. Keep selection checkboxes, expansion, menus, and row navigation distinct, and check that activating one does not trigger another. `isRowClickable` excludes rows that should not navigate. `onRowMouseEnter` can preload a destination, but the click handler still needs to work when there was no hover, including keyboard and touch use. ## Choose paging and prove the states Use client-side pagination only when the full set is present. For server pages, pass the one-based page and paging callbacks. For cursor sources, append batches through `infiniteScroll` and supply accurate `hasMore` and loading state. Provide singular and plural `entityLabel` values for count copy. Before shipping, try an initially empty collection, a nonmatching search, a rejected request followed by Retry, and a narrow viewport. Tab to the search, a row action, and the create control. Confirm the host prevents unauthorized writes even if its UI state is bypassed. # Settings page Source: https://ui.tale.dev/docs/patterns/settings-page A settings page needs a clear distinction between the saved configuration and the current draft. Align related fields with `ContentArea variant="narrow"`, then make Save, Discard, and navigation behavior reflect that distinction. ## Try the draft and saved states Change **Workspace name**, then choose **Save**. The example adopts that value as its new saved baseline and disables the actions. Edit again and choose **Discard** to return to that baseline. This example saves only in memory. Reloading restores the sample values. The region and digest controls demonstrate form layout; they do not change data residency or schedule emails. The bar stays visible while its status and button availability change. ## Align fields through the container ```tsx import { ContentArea } from '@tale/ui/content-area'; import { Input } from '@tale/ui/input'; export function SettingsLayoutExample() { return ( ); } ``` This layout-only example has uncontrolled fields; use an editor controller for a persisted form. The narrow container caps content at `max-w-3xl` and declares the shared field layout. `FieldShell` stacks labels and controls on small screens and places them beside one another from `sm`, with a consistent control column. If a field does not align, first check whether it uses `FieldShell`. Use `wideControl` deliberately for content that needs the available width. Group related settings with `FormSection`; reserve cards for distinct objects such as a connected account, rather than wrapping every input in a separate panel. The surrounding page header supplies the `h1`. Do not repeat it in the settings body, and do not assume a navigation label replaces a page heading. Begin settings section headings at the appropriate level beneath the page title. ## Connect a real editor `useFormEditor` adapts React Hook Form to the shared editor contract. It accepts server `data`, optional initial `defaultValues` and a validation schema, plus an asynchronous `save` callback. The returned controller exposes the form, status, `save`, `reset`, `submit`, and `dirtyKeys`. | Integration | Why it matters | | --- | --- | | Keep server data separate from the draft | A failed request must not erase the person's edit. | | Wire native form submission to `editor.submit` | This updates the saved baseline after success; calling the persistence callback directly bypasses that step. | | Use `EditorActions` with the controller | Save/Discard availability follows dirty, valid, loading, and saving state. | | Register related sections through `EditorGroup` | Their controllers contribute to one active-editor action area. The group does not draw a bar on its own. | | Map server field errors where possible | Show repairable errors beside the relevant field rather than only in a disappearing toast. | For nested configuration objects that do not suit flat form paths, inspect `useJsonConfigEditor` before introducing another editor mechanism. Import controllers from `@tale/ui/editor/*`. ## Protect edits when leaving Mount `DirtyBlockerProvider` within the router tree and register the editor's dirty state. `useFormEditor` performs its dirty-source registration; the provider supplies the navigation decision and before-unload handling. Without the surrounding provider, a controller cannot provide a complete leave-page warning. For route-based settings tabs, use [`TabNavigation`](/docs/components/tabs-and-navigation). Pass the controller's `dirtyKeys` set and configure corresponding keys on the affected items. The dot indicates unsaved content; it is separate from the blocker that asks before leaving. The form editor reports `hasRemoteUpdate` when upstream data changes while a draft is dirty. Give the person a visible way to understand and resolve that situation. Do not silently overwrite a local draft with a fresh server response. ## Verify a real save workflow Test Save success, validation failure, server failure, Discard, and navigation away with a draft. After a successful save, reload and confirm the backend value persists. After a failed save, confirm the draft remains. The local example above demonstrates state transitions, but only your host integration can prove persistence, authorization, and unsaved-change protection.