snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
---
title: Accessibility
description: Label controls, preserve keyboard behavior, and test accessible Twenty UI interfaces.
---
Migrated interactive components use Base UI primitives for semantics and keyboard behavior. Your application still supplies meaningful labels, descriptions, error messages, and focus order.
## Label every control
Use `Field.Label` with an input to associate a visible label with the control. `Field.Description` supplies supporting text:
```tsx
import { Field, Input } from 'twenty-ui/primitives/input';
export const EmailField = () => (
<Field.Root>
<Field.Label>Email</Field.Label>
<Input type="email" name="email" required />
<Field.Description>
We send account updates to this address.
</Field.Description>
<Field.Error>Enter a valid email address.</Field.Error>
</Field.Root>
);
```
Placeholders supplement labels; they do not replace them. For a control without visible text, supply an accessible name using `aria-label` or `aria-labelledby`.
## Preserve native behavior
Use the component's state and change props, such as `checked` and `onCheckedChange` on `Switch`. Keep focus indicators visible. When using `render` to replace an element, preserve the ref, event handlers, and attributes the component passes to it.
A disabled control cannot be changed and is generally excluded from keyboard navigation and form submission. A read-only control retains its value and may remain focusable; choose the state that matches the interaction you intend.
## Test the complete interface
Check keyboard navigation, labels, error announcements, focus movement in overlays, and zoom in the context where the component is used. Verify contrast in both palettes and after applying custom token overrides.
Twenty UI's stories contain interaction and automated accessibility checks. Some stories still defer color-contrast checks, so those checks do not establish full accessibility conformance for an application.

View File

@@ -0,0 +1,94 @@
---
title: IconButton
description: Trigger actions with accessible square icon buttons.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import IconButtonReference from '/snippets/ui/generated/input/icon-button.mdx';
`IconButton` composes [Button](/ui/primitives/input/button) with a square surface. Pass the icon as children and provide an `aria-label` describing the action. Icon content is decorative and hidden from assistive technology.
## Usage
```tsx
import { IconButton } from 'twenty-ui/components';
import { IconPlus } from 'twenty-ui/icon';
export const CreateRecordButton = () => (
<IconButton
aria-label="Create record"
variant="solid"
color="accent"
size="sm"
>
<IconPlus />
</IconButton>
);
```
Sizes are `sm` (24px) and `md` (32px). The defaults are `md`, `outline`, and `neutral`. Variants and colors match Button.
<StoryEmbed
title="Icon button example"
storyId="ui-components-iconbutton--catalog"
height={620}
/>
## Tooltip
Add `tooltip` to show text on hover and keyboard focus. Keep `aria-label` as the accessible name of the action. Disabled and loading buttons retain hover tooltips.
```tsx
import { IconButton } from 'twenty-ui/components';
import { IconSearch } from 'twenty-ui/icon';
export const SearchRecordsButton = () => (
<IconButton aria-label="Search" tooltip="Search records" variant="ghost">
<IconSearch />
</IconButton>
);
```
<StoryEmbed
title="Icon button with tooltip"
storyId="ui-components-iconbutton--tooltip"
height={180}
/>
The tooltip defaults to the bottom, with a 1000ms delay and a 5px offset. Use `tooltipPlace`, `tooltipDelay`, and `tooltipOffset` to adjust it. Omitting `tooltip` renders the button directly.
## State ownership
Your application supplies `loading` and `disabled`. Both prevent activation. Loading preserves the square dimensions and accessible label. The button does not track a selected value or set loading automatically after a click. Native keyboard focus follows the browser; use `aria-pressed` when your application owns a toggle state.
## Grouped actions
```tsx
import { ButtonGroup } from 'twenty-ui/primitives/input';
import { IconButton } from 'twenty-ui/components';
import { IconPlus, IconSearch } from 'twenty-ui/icon';
export const RecordActions = () => (
<ButtonGroup aria-label="Record actions" size="sm" variant="outline">
<IconButton aria-label="Search records">
<IconSearch />
</IconButton>
<IconButton aria-label="Create record">
<IconPlus />
</IconButton>
</ButtonGroup>
);
```
[ButtonGroup](/ui/primitives/input/button-group) supplies size, variant, color, and adjoining corners. Individual buttons do not need a position prop.
## Links and forms
Use `href` to render an anchor. The caller can provide a custom `render` element, including its own router link. Supply `href` to select link semantics; the renderer must preserve an anchor. Without `href`, preserve a native button. Neither `nativeButton` nor `role` is part of the public button interface. Disabled links cannot activate. Routing stays in the application.
Native attributes, event handlers, refs, and render composition pass through to Button. The default is `type="button"`; set `type="submit"` explicitly for form submission.
## Props
<IconButtonReference />

View File

@@ -0,0 +1,56 @@
---
title: LightButton
description: Present lightweight actions with standard or subtle emphasis.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import LightButtonReference from '/snippets/ui/generated/input/light-button.mdx';
Use the shared `LightButton` component for a lightweight action. It composes
[Button](/ui/primitives/input/button) with a ghost surface, small size, regular
font weight, and no elevation.
<StoryEmbed
storyId="ui-components-lightbutton--default"
title="LightButton example"
height={240}
/>
## State ownership
`LightButton` uses Button's state model and adds no state of its own. Your application supplies `loading`, `disabled`, and `soon`; `emphasis` only changes appearance. See [Button's state ownership example](/ui/primitives/input/button#state-ownership) for managing an asynchronous action. There are no controlled and uncontrolled selection modes.
## Usage
```tsx
import { LightButton } from 'twenty-ui/components';
import { IconPlus } from 'twenty-ui/icon';
export const FilterActions = () => (
<LightButton emphasis="subtle" startIcon={<IconPlus />}>
Add filter
</LightButton>
);
```
The default `emphasis="standard"` uses secondary neutral text.
`emphasis="subtle"` uses tertiary neutral text on ghost, outline, and soft
surfaces. Semantic colors and solid surfaces retain their contrast treatment.
Override `variant`, `color`, `size`, or `elevated` when needed.
[ButtonGroup](/ui/primitives/input/button-group) can set a shared size, surface,
and color; subtle emphasis does not replace the group's semantic color.
## Interaction
`LightButton` accepts Button's native attributes, refs, event handlers, icon
slots, loading and disabled states, and render composition. Its native default
is `type="button"`. Set `type="submit"` for submission or use `href` for a link.
Supply `render` to choose a custom root, including an application-owned router
link. The root must preserve a native button, or an anchor when `href` is set.
Button derives the interaction behavior from `href`.
## Props
<LightButtonReference />

View File

@@ -0,0 +1,56 @@
---
title: MainButton
description: Present prominent actions with shared design defaults.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import MainButtonReference from '/snippets/ui/generated/input/main-button.mdx';
Use the shared `MainButton` component for a prominent action. It composes
[Button](/ui/primitives/input/button) with a solid surface, elevation, semibold
text, and 12px horizontal padding. Its default color is neutral and size is `md`.
<StoryEmbed
storyId="ui-components-mainbutton--default"
title="MainButton example"
height={240}
/>
## State ownership
`MainButton` uses Button's state model and adds no state of its own. Your application supplies `loading`, `disabled`, and `soon`; there are no controlled and uncontrolled selection modes. See [Button's state ownership example](/ui/primitives/input/button#state-ownership) for managing an asynchronous action.
## Usage
```tsx
import { MainButton } from 'twenty-ui/components';
export const SaveProfile = () => (
<MainButton type="submit" fullWidth>
Save profile
</MainButton>
);
```
Width follows the content unless you set `fullWidth` or provide application
layout styling. Override `variant`, `color`, `size`, and `elevated` as needed;
for example, use `variant="outline"` and `elevated={false}` for an outlined action.
Keep page layout in your application's styling.
## Interaction
`MainButton` accepts Button's native attributes, event handlers, refs, icon slots,
`loading`, `disabled`, and `soon` props. Buttons default to `type="button"`;
set `type="submit"` explicitly for form submission.
Use `href` for a link. Supply `render` to choose a custom root, including an
application-owned router link. The root must preserve a native button, or an
anchor when `href` is set. Button derives the interaction behavior from `href`.
[ButtonGroup](/ui/primitives/input/button-group) can set a shared size, surface,
and color. Consumer classes, including state functions, pass through to the root.
## Props
<MainButtonReference />

View File

@@ -0,0 +1,18 @@
---
title: Components
sidebarTitle: Overview
description: Shared design presets and reusable app building blocks composed from Twenty UI primitives.
---
Components combine [primitives](/ui/primitives/overview) into recurring design presets and reusable app building blocks. Like primitives, they receive data and callbacks from your application.
Import shared components from `twenty-ui/components`. See [Getting started](/ui/getting-started) for installation and theme setup.
## Browse components
| Component | Purpose |
| ------------------------------------------------ | --------------------------------------------------------------------- |
| [MainButton](/ui/components/input/main-button) | Prominent actions with a solid surface, elevation, and semibold text. |
| [LightButton](/ui/components/input/light-button) | Lightweight actions with standard or subtle emphasis. |
| [IconButton](/ui/components/input/icon-button) | Square icon actions built on Button. |
| [Section](/ui/components/section) | Related content with a heading, description, and optional action. |

View File

@@ -0,0 +1,52 @@
---
title: Section
description: Group content with a heading, supporting description, and optional action.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import SectionReference from '/snippets/ui/generated/components/section.mdx';
`Section.Root` groups related content. `Section.Header` combines [Heading](/ui/primitives/typography/heading) with a description and an optional `adornment`. Descriptions are ordinary text associated with the heading, so they do not add levels to the document outline.
<StoryEmbed
storyId="ui-components-section--documentation"
title="Section example"
height={200}
/>
## Add a description and action
```tsx
import { Section } from 'twenty-ui/components';
import { Button } from 'twenty-ui/primitives/input';
import { Text } from 'twenty-ui/primitives/typography';
export const WorkspaceSection = () => (
<Section.Root>
<Section.Header
title="Workspace settings"
description="Manage your workspace name and preferences."
adornment={<Button>Edit workspace</Button>}
/>
<Text>Workspace preferences appear here.</Text>
</Section.Root>
);
```
`Section.Root` defaults to a full-width `div` with left alignment and the primary text color. Use `align`, `color`, and `fullWidth` to adjust the content wrapper.
`Section.Header` can also be used by itself. Its `level`, `size`, and `color` configure the heading and default to `2`, `md`, and `primary`. The header has one spacing-4 margin after it. Use native `style` or `className` to adjust that spacing.
## Description overflow
String descriptions preserve line breaks, turn URLs into links, and show up to five lines. Set `descriptionLineClamp` to a positive integer to choose another limit. Hovering or focusing an overflowing description reveals its full text; Escape dismisses the tooltip.
Pass a React node for richer supporting content. Rich descriptions render as supplied, without automatic truncation or tooltips.
## Composition
Both parts forward native attributes, refs, class names, styles, and `render` to their outer element. For example, `Section.Root render={<section aria-label="Workspace settings" />}` provides a named section, and `Section.Header render={<header />}` uses a header container.
## Props
<SectionReference />

View File

@@ -0,0 +1,57 @@
---
title: Dark mode
description: Load both Twenty UI palettes and switch the active color scheme.
---
Load both theme stylesheets when your application can switch schemes. `ThemeProvider` selects a palette by applying the `light` or `dark` class.
```tsx
import { useState } from 'react';
import { Switch } from 'twenty-ui/primitives/input';
import { ThemeProvider } from 'twenty-ui/theme-constants';
import 'twenty-ui/style.css';
import 'twenty-ui/theme-light.css';
import 'twenty-ui/theme-dark.css';
export const App = () => {
const [isDark, setIsDark] = useState(false);
return (
<ThemeProvider colorScheme={isDark ? 'dark' : 'light'}>
<label>
<Switch checked={isDark} onCheckedChange={setIsDark} />
Dark mode
</label>
</ThemeProvider>
);
};
```
The provider accepts an explicit scheme. Your application owns preference persistence and any system-preference handling. If you render on the server, use the same initial scheme on the server and client to avoid an initial palette mismatch.
## Mix palettes
Set `applyToRoot={false}` on a nested provider to give a subtree a different palette. Both stylesheets must be loaded:
```tsx
import { ThemeProvider } from 'twenty-ui/theme-constants';
export const Preview = () => (
<ThemeProvider colorScheme="light">
<p>The application uses the light palette.</p>
<ThemeProvider colorScheme="dark" applyToRoot={false}>
<section
style={{
background: 'var(--t-background-primary)',
color: 'var(--t-font-color-primary)',
}}
>
This preview uses the dark palette.
</section>
</ThemeProvider>
</ThemeProvider>
);
```
Use `useThemeColorScheme()` from `twenty-ui/theme-constants` to read the provider's active scheme. In a Twenty front component, use `useColorScheme()` from `twenty-sdk/front-component` to read the workspace scheme supplied by the host.

View File

@@ -0,0 +1,93 @@
---
title: Getting started
description: Install Twenty UI in a React application and use its components and themes.
---
Twenty UI is Twenty's React component library. It provides components, icons, and theme tokens with prebuilt CSS, so your application does not need a CSS-in-JS compiler.
<Note>
Twenty UI is in alpha. Its version follows the Twenty SDK release cycle, and
component APIs can change between releases. In a Twenty app, keep `twenty-ui`,
`twenty-sdk`, and `twenty-client-sdk` on the same version.
</Note>
## Install
For a standalone React application, install the library and its peer dependencies:
```bash
yarn add twenty-ui monaco-editor react@^19 react-dom@^19
```
`monaco-editor` powers the code editor components, but install it even if you never render an editor: the `twenty-ui/primitives/input`, `twenty-ui/primitives/feedback`, and `twenty-ui/primitives/navigation` entry points reference it, and bundlers fail to resolve it otherwise.
If you are building a Twenty app, follow [Using Twenty UI components](/developers/extend/apps/layout/front-components#using-twenty-ui-components). The front component renderer supplies the workspace theme and has a different setup from a standalone browser application.
## Load styles and provide a theme
Import the component stylesheet and a theme stylesheet once at your application entry point. `ThemeProvider` applies the active theme class and makes theme values available to components.
```tsx
import { Field, Input } from 'twenty-ui/primitives/input';
import { ThemeProvider } from 'twenty-ui/theme-constants';
import 'twenty-ui/style.css';
import 'twenty-ui/theme-light.css';
export const App = () => (
<ThemeProvider colorScheme="light">
<Field.Root>
<Field.Label>Email</Field.Label>
<Input name="email" type="email" placeholder="you@example.com" />
<Field.Description>Use your work email address.</Field.Description>
</Field.Root>
</ThemeProvider>
);
```
The component stylesheet includes a global reset. Review its effect when adding Twenty UI to an existing application.
## Choose an entry point
Import components from their public subpath to keep imports focused:
Primitives are foundational controls, including compound controls such as Select,
Menu, and AlertDialog. Shared components combine primitives into recurring
design presets and reusable app building blocks. Both receive data and callbacks
from the application.
Use [MainButton](/ui/components/input/main-button) for prominent actions and
[LightButton](/ui/components/input/light-button) for lightweight actions. Both
come from `twenty-ui/components` and share the foundational
[Button](/ui/primitives/input/button) interaction contract. Routing and page
layout remain in the application.
Each primitive family has a matching public entry point, such as
`twenty-ui/primitives/input`.
| Import | Contents |
| -------------------------------------- | ------------------------------------------------------------ |
| `twenty-ui/components` | Shared design presets and reusable app building blocks |
| `twenty-ui/primitives/accessibility` | Hidden elements and keyboard interaction helpers |
| `twenty-ui/primitives/data-display` | Avatars, chips, tags, status indicators, and display helpers |
| `twenty-ui/primitives/feedback` | Toasts, banners, progress bars, and loaders |
| `twenty-ui/primitives/input` | Buttons, form controls, pickers, and code editors |
| `twenty-ui/primitives/json-visualizer` | JSON tree viewer |
| `twenty-ui/primitives/layout` | Layout, animation, resizing, and section components |
| `twenty-ui/primitives/navigation` | Links, list items, menus, and tabs |
| `twenty-ui/primitives/surfaces` | Cards, dialogs, menus, popovers, and tooltips |
| `twenty-ui/primitives/typography` | Text, headings, labels, and typography helpers |
| `twenty-ui/icon` | Icons |
| `twenty-ui/theme-constants` | Theme provider, hooks, and CSS variable references |
Import individual icons, such as `IconCheck`, from `twenty-ui/icon`. The dynamic icon provider includes the full icon catalog, so reserve it for interfaces that need to look up icons at runtime.
## Continue
- [Theming](/ui/theming): use semantic tokens and scoped overrides.
- [Dark mode](/ui/dark-mode): switch between light and dark palettes.
## Browse the library
- [Primitives](/ui/primitives/overview): foundational controls grouped by family, with usage examples and API references.
- [Components](/ui/components/overview): shared design presets and reusable app building blocks composed from primitives.

View File

@@ -0,0 +1,78 @@
---
title: Avatar
description: Represent a person or record with an image, initial, or icon.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import AvatarReference from '/snippets/ui/generated/data-display/avatar.mdx';
`Avatar` shows an image with a first-letter fallback. Use it beside a record name or inside a [Chip](/ui/primitives/data-display/chip).
<StoryEmbed storyId="ui-data-display-avatar--app" title="Avatar example" />
## Image and fallback
```tsx
import { Avatar } from 'twenty-ui/primitives/data-display';
type PersonAvatarProps = { name: string; imageUrl?: string | null };
export const PersonAvatar = ({ name, imageUrl }: PersonAvatarProps) => (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
<Avatar src={imageUrl} name={name} shape="circle" size="lg" />
{name}
</span>
);
```
When the image is absent or fails to load, the fallback shows the first letter of the trimmed name in uppercase. An empty name shows `-`. `colorSeed` chooses stable fallback colors and defaults to `name`; use a record identifier to keep the color stable when the name changes.
The image and fallback are decorative. Keep the visible name beside the avatar, as above. For a standalone avatar that conveys information, supply `role="img"` and an `aria-label`.
## Shape and appearance
`shape` accepts `square`, `rounded-square`, or `circle`. Sizes run from `xs` (12px) to `xl` (40px); `md` is 16px. `variant="outline"` adds a border to the fallback. The `color`, `backgroundColor`, and `borderColor` overrides customize the fallback, while a loaded image keeps its original appearance.
```tsx
import { IconUser } from 'twenty-ui/icon';
import { Avatar } from 'twenty-ui/primitives/data-display';
export const ContactIcon = () => (
<Avatar
icon={<IconUser />}
shape="rounded-square"
size="xl"
role="img"
aria-label="Contact"
/>
);
```
An `icon` takes precedence over both the image and the first-letter fallback. `pulsing` animates opacity and respects reduced-motion preferences.
## Open a profile
```tsx
import { Avatar } from 'twenty-ui/primitives/data-display';
type ProfileAvatarProps = { onOpenProfile: () => void };
export const ProfileAvatar = ({ onOpenProfile }: ProfileAvatarProps) => (
<Avatar
name="Jane"
shape="circle"
size="lg"
aria-label="Open Jane's profile"
onClick={onOpenProfile}
/>
);
```
Supplying `onClick` renders a button with pointer, Enter, and Space activation. Use `disabled` to prevent activation. The default accessible name comes from `name`, but an action-specific `aria-label` makes the purpose clearer. When replacing the button through `render`, set `nativeButton={false}` for a non-button element.
## Props
Types and defaults are generated from the public component types. Native attributes and event handlers are also accepted.
<AvatarReference />

View File

@@ -0,0 +1,80 @@
---
title: Chip
description: Show a compact record label with optional leading and trailing content.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import ChipReference from '/snippets/ui/generated/data-display/chip.mdx';
Use `Chip` for compact references to people, records, or files. Use [Tag](/ui/primitives/data-display/tag) for colored categories and [Status](/ui/primitives/data-display/status) for workflow states.
<StoryEmbed
storyId="ui-data-display-chip--with-left-avatar"
title="Chip example"
/>
## Record label
```tsx
import { Avatar, Chip } from 'twenty-ui/primitives/data-display';
export const AssigneeChip = () => (
<Chip
variant="soft"
startElement={<Avatar name="Jane" size="sm" shape="circle" />}
>
Jane
</Chip>
);
```
Use `startElement` and `endElement` for avatars, icons, or other content. `endElementDivider` adds a separator before the trailing content. The default chip is a `div`; its slots can contain an action when the chip itself is not interactive.
## Open a record
```tsx
import { Avatar, Chip } from 'twenty-ui/primitives/data-display';
type AssigneeActionProps = { onOpenAssignee: () => void };
export const AssigneeAction = ({ onOpenAssignee }: AssigneeActionProps) => (
<Chip
onClick={onOpenAssignee}
aria-label="Open Jane's record"
startElement={<Avatar name="Jane" size="sm" shape="circle" />}
>
Jane
</Chip>
);
```
`onClick` renders a button with keyboard activation. `clickable` controls hover and pointer styling only; setting it alone does not create a button or add an action. `disabled` prevents activation of the button. Do not nest buttons or links inside an interactive chip.
Use `render` to replace the root element. If an `onClick` chip renders a non-button element, set `nativeButton={false}`. When `isLabelHidden` hides the label, give an interactive chip an `aria-label`.
## Width and empty labels
```tsx
import { Chip } from 'twenty-ui/primitives/data-display';
export const FileChip = () => (
<Chip variant="soft" maxWidth={180}>
Quarterly customer research.pdf
</Chip>
);
```
`maxWidth` constrains the chip in pixels. String labels truncate and show their full text in a tooltip on hover. `tooltipLabel` changes the tooltip text; `alwaysShowTooltip` also enables it when the label fits. Keep essential information available without relying on hover.
Missing, empty-string, or `false` children show `emptyLabel`, which defaults to `Untitled`. `forceEmptyText` suppresses that placeholder. `isLabelHidden` hides both the content label and the placeholder while preserving the slots.
## Appearance
`size` accepts `sm` or `md`. `variant` accepts `ghost`, `soft`, or `solid`; `ghost` is the default. Use `shape="round"` for a pill, `weight="medium"` for emphasis, and `color="secondary"` for secondary text.
## Props
Types and defaults are generated from the public component types. Native attributes and event handlers are also accepted.
<ChipReference />

View File

@@ -0,0 +1,77 @@
---
title: Status
description: Show a named state with a colored indicator and optional loading feedback.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import StatusReference from '/snippets/ui/generated/data-display/status.mdx';
`Status` combines a colored dot, a text label, and an optional loader. Use it for workflow or record states. Use [Tag](/ui/primitives/data-display/tag) for a category without the status indicator.
<StoryEmbed
storyId="ui-data-display-status--documentation"
title="Status example"
/>
## Record state
```tsx
import { Status } from 'twenty-ui/primitives/data-display';
export const RecordStatus = () => <Status color="green">Active</Status>;
```
`color` is required and uses the theme palette. The label communicates the meaning independently of the color. The default element is a `span`; `weight="medium"` emphasizes its text.
## Loading and announcements
```tsx
import { Status } from 'twenty-ui/primitives/data-display';
type SaveStatusProps = { isSaving: boolean };
export const SaveStatus = ({ isSaving }: SaveStatusProps) => (
<div role="status">
<Status color={isSaving ? 'blue' : 'green'} loading={isSaving}>
{isSaving ? 'Saving' : 'Saved'}
</Status>
</div>
);
```
`loading` shows a loader after the label. The component does not add a live region or change its text automatically. Keep a `role="status"` container mounted when updates should be announced to assistive technology, and provide a label that describes the current state.
## Change a state
```tsx
import { Status } from 'twenty-ui/primitives/data-display';
type ChangeStatusActionProps = {
isUpdating: boolean;
onChangeStatus: () => void;
};
export const ChangeStatusAction = ({
isUpdating,
onChangeStatus,
}: ChangeStatusActionProps) => (
<Status
color="blue"
loading={isUpdating}
disabled={isUpdating}
onClick={onChangeStatus}
aria-label="Change record status"
>
{isUpdating ? 'Updating' : 'In progress'}
</Status>
);
```
`onClick` renders a native button with pointer, Enter, and Space activation. `loading` does not prevent repeat activation; pass `disabled` when the action should wait. If `render` replaces the button with a non-button element, set `nativeButton={false}`.
## Props
Types and defaults are generated from the public component types. Native attributes and event handlers are also accepted.
<StatusReference />

View File

@@ -0,0 +1,81 @@
---
title: Tag
description: Label a category or attribute with a theme color and optional icon.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import TagReference from '/snippets/ui/generated/data-display/tag.mdx';
Use `Tag` for categories, priorities, or other short attributes. Keep a text label so the meaning does not depend on color. Use [Status](/ui/primitives/data-display/status) when you need a state indicator and loading feedback.
<StoryEmbed storyId="ui-data-display-tag--documentation" title="Tag example" />
## Category label
```tsx
import { Tag } from 'twenty-ui/primitives/data-display';
export const PriorityTag = () => <Tag color="red">Urgent</Tag>;
```
`color` is required and accepts a theme color or `transparent`. The default element is a `span`. Add a decorative icon with `startIcon`; the label supplies its accessible meaning.
## Variants
```tsx
import { Tag } from 'twenty-ui/primitives/data-display';
export const CategoryTags = () => (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
<Tag color="blue" variant="soft">
Customer
</Tag>
<Tag color="green" variant="solid">
Partner
</Tag>
<Tag color="orange" variant="outline">
Prospect
</Tag>
<Tag color="transparent" variant="outline" borderStyle="dashed">
Unassigned
</Tag>
</div>
);
```
`soft` is the default treatment. `solid` emphasizes the label, `outline` adds a border, and `ghost` removes the background. Use `borderStyle="dashed"` with `outline` and `weight="medium"` for a stronger label.
## Filter by a tag
```tsx
import { Tag } from 'twenty-ui/primitives/data-display';
type CategoryFilterProps = {
selected: boolean;
onToggle: () => void;
};
export const CategoryFilter = ({ selected, onToggle }: CategoryFilterProps) => (
<Tag
color="blue"
variant={selected ? 'solid' : 'outline'}
aria-pressed={selected}
onClick={onToggle}
>
Customer
</Tag>
);
```
Supplying `onClick` renders a button. Selection state belongs to your application; use `aria-pressed` for a toggle and a visible treatment for the selected state. `disabled` prevents pointer and keyboard activation. When `render` replaces an interactive tag with a non-button element, set `nativeButton={false}`.
## Long labels
By default, constrained string labels truncate and expose their full text in a tooltip on hover. `preventShrink` keeps the full label width and turns off that tooltip, so make sure the surrounding layout can accommodate it. `preventPadding` removes the horizontal padding for custom layouts.
## Props
Types and defaults are generated from the public component types. Native attributes and event handlers are also accepted.
<TagReference />

View File

@@ -0,0 +1,81 @@
---
title: Toast
description: Display status messages with optional details, actions, and progress.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import ToastReference from '/snippets/ui/generated/feedback/toast.mdx';
`Toast` renders one notification. Your application controls when it appears, its position, and when it is removed.
<StoryEmbed
storyId="ui-feedback-toast--documentation"
title="Toast example"
height={240}
/>
## Dismissible notification
```tsx
import { useState } from 'react';
import { Toast } from 'twenty-ui/primitives/feedback';
export const SaveNotification = () => {
const [visible, setVisible] = useState(false);
return (
<>
<button type="button" onClick={() => setVisible(true)}>
Show saved notification
</button>
{visible && (
<Toast
variant="success"
description="Your changes are available to your team."
duration={6000}
onClose={() => setVisible(false)}
>
Record saved
</Toast>
)}
</>
);
};
```
`onClose` is called by the close button and when the countdown completes. Remove the toast in that callback. The countdown defaults to 6,000 milliseconds and pauses while the pointer is over the toast.
## Progress and cancellation
```tsx
import { Toast } from 'twenty-ui/primitives/feedback';
type ImportProgressProps = { progress: number; onCancel: () => void };
export const ImportProgress = ({ progress, onCancel }: ImportProgressProps) => (
<Toast
variant="info"
progress={progress}
description="You can continue working while the import runs."
onCancel={onCancel}
cancelLabel="Cancel import"
>
Importing records: {progress}%
</Toast>
);
```
Passing `progress` supplies a percentage and disables the countdown. Cancellation invokes `onCancel`; the parent owns cancellation and removal. `action` accepts a React node for a footer action.
## Appearance and announcements
`variant` accepts `default`, `error`, `success`, `info`, or `warning`. Each variant supplies an icon and theme colors. Use `icon` to replace the icon and `iconLabel` when the default icon needs its own accessible name.
The default role is `status` with polite announcements. Use `role="alert"` for urgent feedback. Set `cancelLabel` and `closeLabel` to localize the controls, and allow enough time to read the content or interact with its actions.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<ToastReference />

View File

@@ -0,0 +1,31 @@
---
title: ButtonGroup
description: Group related buttons with joined borders and shared appearance.
---
import ButtonGroupReference from '/snippets/ui/generated/input/button-group.mdx';
Give a group an accessible name and place related buttons inside it. The group joins adjacent borders and can apply a shared size, variant, and color. Buttons retain their own tab stops.
## State ownership
`ButtonGroup` coordinates appearance, not selection. It has no controlled or uncontrolled selection mode. Each button receives its own action handler and application state; see [Button's state ownership example](/ui/primitives/input/button#state-ownership) for loading behavior.
For a mutually exclusive choice, use [RadioGroup](/ui/primitives/input/radio-group), which owns selection for its radio controls.
## Usage
```tsx
import { Button, ButtonGroup } from 'twenty-ui/primitives/input';
export const RecordActions = () => (
<ButtonGroup aria-label="Record actions" size="sm">
<Button>Edit</Button>
<Button>Duplicate</Button>
</ButtonGroup>
);
```
## Props
<ButtonGroupReference />

View File

@@ -0,0 +1,91 @@
---
title: Button
description: Trigger actions or navigate with styled buttons.
---
import ButtonReference from '/snippets/ui/generated/input/button.mdx';
Use `Button` for an action. Supply its visible label as children, and give icon-only buttons an `aria-label`.
For recurring design defaults, use [MainButton](/ui/components/input/main-button)
or [LightButton](/ui/components/input/light-button) from `twenty-ui/components`.
Both share Button's native props, refs, state, and rendering behavior.
## State ownership
`Button` triggers an action and does not manage a selected value, so it has no controlled or uncontrolled selection mode. Its native `value` prop supplies form data, not selection state.
Your application owns `loading`, `disabled`, and `soon`, which default to `false`. Clicking a button does not change these props or automatically show loading while an asynchronous handler runs. Pass the current application state explicitly.
```tsx
import { useState } from 'react';
import { Button } from 'twenty-ui/primitives/input';
type SaveRecordProps = { onSave: () => Promise<void> };
export const SaveRecord = ({ onSave }: SaveRecordProps) => {
const [loading, setLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const handleSave = async () => {
setLoading(true);
setHasError(false);
try {
await onSave();
} catch {
setHasError(true);
} finally {
setLoading(false);
}
};
return (
<>
<Button loading={loading} onClick={handleSave}>
Save record
</Button>
{hasError && <p role="alert">Unable to save the record. Try again.</p>}
</>
);
};
```
The application starts and clears loading around the save operation. The button prevents activation while loading and becomes available again when the operation finishes, including after an error. `MainButton` and `LightButton` use the same state model.
## Appearance
```tsx
import { Button } from 'twenty-ui/primitives/input';
import { IconPlus } from 'twenty-ui/icon';
export const CreateRecord = () => (
<Button variant="solid" color="accent" size="sm" startIcon={<IconPlus />}>
Create record
</Button>
);
```
Choose `solid`, `outline`, `soft`, or `ghost` independently of the `neutral`, `accent`, `danger`, or `success` color. Sizes are `sm` and `md`; the defaults are `outline`, `neutral`, and `md`. `fullWidth` fills the available width. `startIcon` and `endIcon` accept elements and are decorative. `elevated` adds a shadow. `hotkeys` displays keyboard hints on larger screens; bind the shortcuts in your application.
`disabled`, `loading`, and `soon` prevent activation. Loading keeps the label accessible and preserves the button's width while showing a spinner. Customize the upcoming-feature label with `soonLabel`.
## Links and forms
```tsx
import { Button } from 'twenty-ui/primitives/input';
export const DocumentationLink = () => (
<Button href="https://twenty.com" target="_blank" rel="noreferrer">
Read documentation
</Button>
);
```
`href` renders an anchor with link semantics. The caller can supply its own `render` element or router link. Set `href` and preserve an anchor for navigation; without `href`, preserve a native button. Button derives its semantics without public `nativeButton` or `role` flags. Disabled links cannot activate.
Buttons default to `type="button"`. Set `type="submit"` or `type="reset"` explicitly for forms. Native props, event handlers, refs, and Base UI's `render` prop pass through to the root.
## Props
<ButtonReference />

View File

@@ -0,0 +1,114 @@
---
title: Checkbox
description: Select independent options with checked and indeterminate states.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import CheckboxReference from '/snippets/ui/generated/input/checkbox.mdx';
Use `Checkbox` when each option can be selected independently. For a mutually exclusive choice, use [RadioGroup](/ui/primitives/input/radio-group).
<StoryEmbed
storyId="ui-input-checkbox-checkbox--default"
title="Checkbox example"
height={240}
/>
## Uncontrolled state
Use `defaultChecked` to set the initial checked state. The checkbox owns subsequent changes.
```tsx
import { Checkbox } from 'twenty-ui/primitives/input';
export const EmailPreference = () => (
<label>
<Checkbox name="updates" value="email" defaultChecked />
Receive product updates
</label>
);
```
A checked, named checkbox contributes its value to a form. Set `uncheckedValue` if the form should also submit a value when it is unchecked.
## Controlled state
Pass `checked` and update it with `onCheckedChange` to own the same preference in React state. The callback receives the next boolean and event details.
```tsx
import { useState } from 'react';
import { Checkbox } from 'twenty-ui/primitives/input';
export const EmailPreference = () => {
const [checked, setChecked] = useState(true);
return (
<label>
<Checkbox
name="updates"
value="email"
checked={checked}
onCheckedChange={setChecked}
/>
Receive product updates
</label>
);
};
```
## Indeterminate state
An indeterminate checkbox represents a partial selection. Derive it from the child options independently of the checkbox's checked state.
```tsx
import { useState } from 'react';
import { Checkbox } from 'twenty-ui/primitives/input';
export const ColumnSelection = () => {
const [showEmail, setShowEmail] = useState(true);
const [showPhone, setShowPhone] = useState(false);
return (
<fieldset>
<legend>Visible columns</legend>
<label>
<Checkbox
checked={showEmail && showPhone}
indeterminate={showEmail !== showPhone}
onCheckedChange={(checked) => {
setShowEmail(checked);
setShowPhone(checked);
}}
/>
All columns
</label>
<label>
<Checkbox checked={showEmail} onCheckedChange={setShowEmail} />
Email
</label>
<label>
<Checkbox checked={showPhone} onCheckedChange={setShowPhone} />
Phone
</label>
</fieldset>
);
};
```
## Appearance and interaction
| Prop | Values | Default |
| --------- | ------------------------------ | -------- |
| `size` | `sm`, `md` | `sm` |
| `variant` | `solid`, `outline`, `soft` | `solid` |
| `shape` | `square`, `round` | `square` |
| `color` | `accent`, `success`, `warning` | `accent` |
Tab focuses the checkbox and Space toggles it. Clicking its label also toggles it. Use `disabled` to prevent interaction, or `readOnly` when the value should remain unchanged while the control stays focusable. Keep meaning in the label rather than communicating it through color alone.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<CheckboxReference />

View File

@@ -0,0 +1,86 @@
---
title: Field
description: Connect a form control to its label, description, and validation feedback.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import FieldReference from '/snippets/ui/generated/input/field.mdx';
Use `Field` to group one control with its accessible label and helper text. Components such as [Input](/ui/primitives/input/input) integrate with the field directly.
<StoryEmbed
storyId="ui-input-field--default"
title="Field example"
height={240}
/>
## Anatomy
Place one control and its supporting parts inside `Field.Root`. This tree shows a field built with the styled `Input` component.
```text
Field.Root
├── Field.Label
├── Input
├── Field.Description
└── Field.Error
```
| Part | Requirement | Purpose |
| ------------------------------------- | ------------------- | ---------------------------------------------------------------------------- |
| `Root` | Required | Shares the name, disabled state, and validation with the control. |
| `Label` | Recommended | Supplies a visible, associated label. An accessible name is always required. |
| `Input` or another integrated control | Required | Receives user input and connects to the field. |
| `Control` | Alternative control | Provides an unstyled input when you do not use an integrated control. |
| `Description` | Optional | Adds persistent helper text associated with the control. |
| `Error` | Optional | Shows feedback for a matching validation state. |
| `Validity` | Optional | Exposes validity through a children render function. |
`Input` and `Textarea` already integrate with the field. Place them directly inside `Root`; they supply the control behavior themselves. Compound controls such as [Select](/ui/primitives/input/select#compose-with-a-field) keep their own root and parts inside the field.
## Labeled control
```tsx
import { Field, Input } from 'twenty-ui/primitives/input';
export const EmailField = () => (
<Field.Root name="email" validationMode="onBlur">
<Field.Label>Work email</Field.Label>
<Input type="email" required placeholder="you@example.com" />
<Field.Description>
We send account updates to this address.
</Field.Description>
<Field.Error match="valueMissing">Enter your email address.</Field.Error>
<Field.Error match="typeMismatch">Enter a valid email address.</Field.Error>
</Field.Root>
);
```
`Field.Root` manages the control's name, disabled state, and validity. `Field.Label` and `Field.Description` connect their content to the control. Set validation timing with `validationMode`; use `validate` for custom validation.
## External errors
Use `invalid` when your application owns the error state. An explicit `match` makes the error message visible.
```tsx
import { Field, Input } from 'twenty-ui/primitives/input';
type UsernameFieldProps = { error?: string };
export const UsernameField = ({ error }: UsernameFieldProps) => (
<Field.Root name="username" invalid={Boolean(error)}>
<Field.Label>Username</Field.Label>
<Input autoComplete="username" />
<Field.Error match={Boolean(error)}>{error}</Field.Error>
</Field.Root>
);
```
Keep error messages actionable and provide a visible label even when the control has a placeholder.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<FieldReference />

View File

@@ -0,0 +1,99 @@
---
title: InputGroup
description: Add prefixes, suffixes, and actions around a text input.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import InputGroupReference from '/snippets/ui/generated/input/input-group.mdx';
`InputGroup` gives an [Input](/ui/primitives/input/input) a shared border and optional leading or trailing content. Set `size` on the group to size both the container and its input.
<StoryEmbed
storyId="ui-input-inputgroup--default"
title="InputGroup example"
height={240}
/>
## Anatomy
Place the input inside `InputGroup` and keep field labels and messages outside the shared border.
```text
Field.Root
├── Field.Label
├── InputGroup
│ └── Input
└── Field.Description
```
| Part or slot | Requirement | Purpose |
| --------------------------------------------- | -------------------- | ------------------------------------------------------------------------ |
| `InputGroup` | Required | Supplies the shared border, layout, and size. |
| `Input` as `children` | Required | Owns the input value, native attributes, and editing behavior. |
| `startElement` | Optional prop | Adds a prefix, icon, or other content before the input. |
| `endElement` | Optional prop | Adds a suffix or action after the input. |
| `Field.Root`, `Label`, `Description`, `Error` | Optional composition | Associates a label, helper text, and validation feedback with the input. |
`startElement` and `endElement` are React node props, not separate components. `InputGroup` does not supply a label or manage validation. Give the input an accessible name, either through `Field.Label` or its own labeling attributes.
## Prefix and suffix
```tsx
import { Field, Input, InputGroup } from 'twenty-ui/primitives/input';
export const WebsiteField = () => (
<Field.Root name="website">
<Field.Label>Website</Field.Label>
<InputGroup startElement={<span aria-hidden>https://</span>}>
<Input placeholder="example.com" />
</InputGroup>
<Field.Description>Enter the domain without https://.</Field.Description>
</Field.Root>
);
```
`startElement` and `endElement` accept React nodes. Prefixes and suffixes are visual content; the input's submitted value contains only what the user enters.
## Trailing action
```tsx
import { useState } from 'react';
import { Button, Field, Input, InputGroup } from 'twenty-ui/primitives/input';
export const PasswordField = () => {
const [visible, setVisible] = useState(false);
return (
<Field.Root name="password">
<Field.Label>Password</Field.Label>
<InputGroup
endElement={
<Button
type="button"
aria-label={visible ? 'Hide password' : 'Show password'}
variant="ghost"
size="sm"
onClick={() => setVisible(!visible)}
>
{visible ? 'Hide' : 'Show'}
</Button>
}
>
<Input
type={visible ? 'text' : 'password'}
autoComplete="current-password"
/>
</InputGroup>
</Field.Root>
);
};
```
Put labels, validation, and value handling on the field or input. Give adornment buttons their own accessible names and use `type="button"` when they should not submit the form. The group supports `sm` and `md` sizes and defaults to `md`.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<InputGroupReference />

View File

@@ -0,0 +1,85 @@
---
title: Input
description: A text input with native attributes, field integration, and two sizes.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import InputReference from '/snippets/ui/generated/input/input.mdx';
`Input` renders a text input and accepts native input attributes such as `type`, `name`, `required`, `disabled`, and `autoComplete`.
<StoryEmbed
storyId="ui-input-input--default"
title="Input example"
height={240}
/>
## Uncontrolled state
Use `defaultValue` to set the initial email address. The input owns subsequent changes.
```tsx
import { Field, Input } from 'twenty-ui/primitives/input';
export const EmailField = () => (
<Field.Root>
<Field.Label>Email</Field.Label>
<Input
name="email"
type="email"
autoComplete="email"
defaultValue="alex@example.com"
/>
<Field.Description>Use your work email address.</Field.Description>
</Field.Root>
);
```
## Controlled state
Pass `value` and update it with `onValueChange` to own the same email address in React state. The callback receives the next string value and event details.
```tsx
import { useState } from 'react';
import { Field, Input } from 'twenty-ui/primitives/input';
export const EmailField = () => {
const [email, setEmail] = useState('alex@example.com');
return (
<Field.Root>
<Field.Label>Email</Field.Label>
<Input
name="email"
type="email"
autoComplete="email"
value={email}
onValueChange={setEmail}
/>
<Field.Description>Use your work email address.</Field.Description>
</Field.Root>
);
};
```
## Add an adornment
`InputGroup` owns the surrounding border and sizes its input. The default size is `md`; use `sm` for a compact field. Set `size` on the group: a grouped input stretches to the group's height, so its own `size` has no visible effect.
```tsx
import { IconSearch } from 'twenty-ui/icon';
import { Input, InputGroup } from 'twenty-ui/primitives/input';
export const CompactSearch = () => (
<InputGroup size="sm" startElement={<IconSearch size={16} aria-hidden />}>
<Input aria-label="Search contacts" />
</InputGroup>
);
```
## Props
The reference includes component-specific and inherited Base UI props. Native input attributes are also accepted. A standalone input defaults to `md`; inside a group, the group's `size` applies.
<InputReference />

View File

@@ -0,0 +1,117 @@
---
title: RadioGroup
description: Manage a single selection across a set of radio options.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import RadioGroupReference from '/snippets/ui/generated/input/radio-group.mdx';
`RadioGroup` coordinates the value, keyboard navigation, and form submission of its [Radio](/ui/primitives/input/radio) children.
<StoryEmbed
storyId="ui-input-radiogroup--default"
title="RadioGroup example"
height={240}
/>
## Anatomy
Put each `Radio` inside the group and associate it with a label. `RadioGroup` and `Radio` are separate exports.
```text
fieldset
├── legend
└── RadioGroup
├── label
│ └── Radio
└── label
└── Radio
```
| Part | Requirement | Purpose |
| -------------------- | --------------------------- | ---------------------------------------------------------- |
| `RadioGroup` | Required | Owns selection, keyboard navigation, and form integration. |
| `Radio` | One per option | Supplies an option with a distinct `value`. |
| `label` | Recommended for each option | Provides a visible, clickable name for its radio. |
| `fieldset`, `legend` | Optional grouping | Groups related controls under a visible heading. |
Give the group its own accessible name using `aria-label` or `aria-labelledby`. In the examples below, `aria-labelledby` connects the group to the legend. Each radio also needs its own name. Put selection props on `RadioGroup` and option values on `Radio`.
## Uncontrolled state
Use `defaultValue` to set the initial billing frequency. The group owns subsequent changes.
```tsx
import { useId } from 'react';
import { Radio, RadioGroup } from 'twenty-ui/primitives/input';
export const BillingFrequency = () => {
const labelId = useId();
return (
<fieldset>
<legend id={labelId}>Billing frequency</legend>
<RadioGroup
name="billingFrequency"
defaultValue="monthly"
aria-labelledby={labelId}
>
<label>
<Radio value="monthly" />
Monthly
</label>
<label>
<Radio value="yearly" />
Yearly
</label>
</RadioGroup>
</fieldset>
);
};
```
The group's `name` identifies the selected value in form submission. Give the group an accessible name as well as labeling each radio.
## Controlled state
Pass `value` and update it with `onValueChange` to own the same billing frequency in React state. Selection belongs to the group, not to individual radio controls.
```tsx
import { useId, useState } from 'react';
import { Radio, RadioGroup } from 'twenty-ui/primitives/input';
export const BillingFrequency = () => {
const labelId = useId();
const [billingFrequency, setBillingFrequency] = useState('monthly');
return (
<fieldset>
<legend id={labelId}>Billing frequency</legend>
<RadioGroup
name="billingFrequency"
value={billingFrequency}
onValueChange={setBillingFrequency}
aria-labelledby={labelId}
>
<label>
<Radio value="monthly" />
Monthly
</label>
<label>
<Radio value="yearly" />
Yearly
</label>
</RadioGroup>
</fieldset>
);
};
```
`onValueChange` receives the next value and event details. The generic value type is inferred from the props. Use `disabled` on the group to disable every option, or on an individual radio to disable one choice. Arrow keys navigate the enabled options; Tab moves out of the group.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<RadioGroupReference />

View File

@@ -0,0 +1,57 @@
---
title: Radio
description: Render one option in a mutually exclusive radio group.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import RadioReference from '/snippets/ui/generated/input/radio.mdx';
`Radio` represents one option inside [RadioGroup](/ui/primitives/input/radio-group). Give each option a distinct `value` and a visible label. The group owns the selected value.
<StoryEmbed
storyId="ui-input-radio--documentation"
title="Radio example"
height={240}
/>
## Labeled options
```tsx
import { Radio, RadioGroup } from 'twenty-ui/primitives/input';
export const ContactMethod = () => (
<RadioGroup
name="contactMethod"
defaultValue="email"
aria-label="Preferred contact method"
>
<label>
<Radio value="email" />
Email
</label>
<label>
<Radio value="phone" />
Phone
</label>
<label>
<Radio value="post" disabled />
Postal mail (unavailable)
</label>
</RadioGroup>
);
```
The `size` prop accepts `sm` or `md` and defaults to `sm`. Use the same size for options in a group.
## Keyboard and selection
Tab enters or leaves the group. Arrow keys move between enabled options and select the focused option. Space selects the focused radio. Selection and change handlers belong on `RadioGroup`; see its [uncontrolled example](/ui/primitives/input/radio-group#uncontrolled-state) and [controlled example](/ui/primitives/input/radio-group#controlled-state).
`Radio` is generic over the value type. Keep that type consistent with its group. Native attributes such as `aria-describedby` can associate an option with additional help text.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<RadioReference />

View File

@@ -0,0 +1,240 @@
---
title: Select
description: Choose one or more values from a popup list of options.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import SelectReference from '/snippets/ui/generated/input/select.mdx';
Use `Select` for a form value chosen from a list. Use [Menu](/ui/primitives/surfaces/menu) for commands and actions.
<StoryEmbed
storyId="ui-input-select--documentation"
title="Select example"
height={400}
/>
## Anatomy
Keep the trigger and popup inside the same `Select.Root`. Put option labels directly inside `Select.Item`.
```text
Select.Root
├── Select.Trigger
│ └── Select.Value
└── Select.Popup
└── Select.Item
```
| Part | Requirement | Purpose |
| --------------------- | ----------------------------------- | ------------------------------------------------------------------------------------ |
| `Root` | Required | Owns selection, open state, and form integration. |
| `Trigger` | Required for a trigger-based select | Opens the list and displays its current value. |
| `Value` | Optional | Renders the selected label or a placeholder; you can provide custom trigger content. |
| `Popup` | Required for the option list | Renders the positioned, portaled list. |
| `Item` | One per option | Provides selection behavior and an option label. |
| `Group`, `GroupLabel` | Optional | Group related options under a label. |
| `Separator` | Optional | Separates groups visually. |
Twenty UI includes the chevron in `Trigger`, the option text and checkmark in `Item`, and the portal and positioner in `Popup`. Compose these public parts directly; placement and portal options belong on `Popup`.
## Uncontrolled state
Use `defaultValue` to set the initial priority. The select owns subsequent selection changes.
```tsx
import { Select } from 'twenty-ui/primitives/input';
const PRIORITIES = [
{ value: 'low', label: 'Low' },
{ value: 'normal', label: 'Normal' },
{ value: 'high', label: 'High' },
];
export const PrioritySelect = () => (
<Select.Root name="priority" defaultValue="normal" items={PRIORITIES}>
<Select.Trigger aria-label="Priority">
<Select.Value placeholder="Choose a priority" />
</Select.Trigger>
<Select.Popup>
{PRIORITIES.map(({ value, label }) => (
<Select.Item key={value} value={value}>
{label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
);
```
Supply `items` on `Root` so `Value` renders the selected item's label; without it, the trigger shows the raw value. The selected value can be `null` when nothing is selected.
## Controlled state
Pass `value` and update it with `onValueChange` to own the same priority in React state. The callback receives the next value and event details.
```tsx
import { useState } from 'react';
import { Select } from 'twenty-ui/primitives/input';
const PRIORITIES = [
{ value: 'low', label: 'Low' },
{ value: 'normal', label: 'Normal' },
{ value: 'high', label: 'High' },
];
export const PrioritySelect = () => {
const [priority, setPriority] = useState<string | null>('normal');
return (
<Select.Root
name="priority"
value={priority}
onValueChange={setPriority}
items={PRIORITIES}
>
<Select.Trigger aria-label="Priority">
<Select.Value placeholder="Choose a priority" />
</Select.Trigger>
<Select.Popup>
{PRIORITIES.map(({ value, label }) => (
<Select.Item key={value} value={value}>
{label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
);
};
```
## Multiple selection
```tsx
import { useState } from 'react';
import { Select } from 'twenty-ui/primitives/input';
const LANGUAGES = [
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'French' },
{ value: 'es', label: 'Spanish' },
];
export const LanguageSelect = () => {
const [languages, setLanguages] = useState<string[]>(['en']);
return (
<Select.Root
multiple
value={languages}
onValueChange={setLanguages}
items={LANGUAGES}
>
<Select.Trigger aria-label="Languages">
<Select.Value placeholder="Choose languages" />
</Select.Trigger>
<Select.Popup>
{LANGUAGES.map(({ value, label }) => (
<Select.Item key={value} value={value}>
{label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
);
};
```
With `multiple`, the value is an array. Use `defaultValue={['en']}` instead of `value` and `onValueChange` for an uncontrolled multiple selection. The generic root types represent the option value and whether multiple selection is enabled.
## Open state
Selection and popup visibility are independent. The examples above leave popup visibility uncontrolled. Selecting a single value closes the popup; multiple selection keeps it open. Use `defaultOpen` to set its initial visibility; it defaults to `false`.
Pass `open` and update it with `onOpenChange` to control visibility. This example controls the popup while leaving the selected priority uncontrolled.
```tsx
import { useState } from 'react';
import { Select } from 'twenty-ui/primitives/input';
const PRIORITIES = [
{ value: 'low', label: 'Low' },
{ value: 'normal', label: 'Normal' },
{ value: 'high', label: 'High' },
];
export const PrioritySelect = () => {
const [open, setOpen] = useState(false);
return (
<Select.Root
name="priority"
defaultValue="normal"
items={PRIORITIES}
open={open}
onOpenChange={setOpen}
>
<Select.Trigger aria-label="Priority">
<Select.Value placeholder="Choose a priority" />
</Select.Trigger>
<Select.Popup>
{PRIORITIES.map(({ value, label }) => (
<Select.Item key={value} value={value}>
{label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
);
};
```
## Compose with a field
Wrap the select in `Field.Root` to associate its trigger with a visible label and helper text. The field handles labeling and validation; the select keeps its own selection and popup state.
```tsx
import { Field, Select } from 'twenty-ui/primitives/input';
const PRIORITIES = [
{ value: 'low', label: 'Low' },
{ value: 'normal', label: 'Normal' },
{ value: 'high', label: 'High' },
];
export const PriorityField = () => (
<Field.Root name="priority">
<Field.Label>Priority</Field.Label>
<Select.Root name="priority" defaultValue="normal" items={PRIORITIES}>
<Select.Trigger>
<Select.Value />
</Select.Trigger>
<Select.Popup>
{PRIORITIES.map(({ value, label }) => (
<Select.Item key={value} value={value}>
{label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
<Field.Description>
Choose how soon this record needs attention.
</Field.Description>
</Field.Root>
);
```
## Placement and keyboard
Set `size="sm"` or `size="md"` on `Trigger`. Customize option content with `Item.startIcon`, `Item.endIcon`, and `Item.description` while keeping the option's label in its children.
`Popup` defaults to `side="bottom"`, `align="start"`, and `sideOffset={8}`. Set `alignItemWithTrigger` to align the selected option over the trigger. It uses the [theme's portal container](/ui/theming#scope-an-override) unless you provide `container`.
Keyboard users can open the list from the trigger, navigate options with arrow keys, and type to find an option. Escape dismisses the popup. Label the trigger or integrate the select with [Field](/ui/primitives/input/field); a placeholder alone does not name the control.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<SelectReference />

View File

@@ -0,0 +1,139 @@
---
title: Slider
description: Choose a numeric value or range with draggable, keyboard-accessible thumbs.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import SliderReference from '/snippets/ui/generated/input/slider.mdx';
`Slider` supports a single number or an array of numbers for a range. Compose its label, track, indicator, and thumbs inside `Root`.
<StoryEmbed
storyId="ui-input-slider--default"
title="Slider example"
height={240}
/>
## Anatomy
Put the track and thumbs inside `Slider.Control`, which handles pointer interaction. This tree shows a single-thumb slider.
```text
Slider.Root
├── Slider.Label
├── Slider.Value
└── Slider.Control
└── Slider.Track
├── Slider.Indicator
└── Slider.Thumb
```
| Part | Requirement | Purpose |
| ----------- | ----------------------------- | --------------------------------------------------------------- |
| `Root` | Required | Owns the value, bounds, step, and orientation. |
| `Control` | Required | Handles pointer interaction and positions the thumbs. |
| `Track` | Required for the styled track | Shows the full value range. |
| `Thumb` | One per adjustable value | Provides a focusable handle with keyboard interaction. |
| `Label` | Recommended | Supplies a visible label; every thumb needs an accessible name. |
| `Value` | Optional | Displays the current numeric value. |
| `Indicator` | Optional | Shows the selected portion of the track. |
For a range, add a thumb for each value with its matching `index` and a distinct accessible name. Labels and numeric output stay outside `Control`, so they do not become part of the draggable area.
## Uncontrolled state
Use `defaultValue` to set the initial volume. The slider owns subsequent changes.
```tsx
import { Slider } from 'twenty-ui/primitives/input';
export const VolumeSlider = () => (
<Slider.Root name="volume" defaultValue={40} min={0} max={100} step={1}>
<Slider.Label>Volume</Slider.Label>
<Slider.Value />
<Slider.Control>
<Slider.Track>
<Slider.Indicator />
<Slider.Thumb />
</Slider.Track>
</Slider.Control>
</Slider.Root>
);
```
## Controlled state
Pass `value` and update it with `onValueChange` to own the same volume in React state. The callback receives the next value and event details.
```tsx
import { useState } from 'react';
import { Slider } from 'twenty-ui/primitives/input';
export const VolumeSlider = () => {
const [volume, setVolume] = useState(40);
return (
<Slider.Root
name="volume"
value={volume}
onValueChange={setVolume}
min={0}
max={100}
step={1}
>
<Slider.Label>Volume</Slider.Label>
<Slider.Value />
<Slider.Control>
<Slider.Track>
<Slider.Indicator />
<Slider.Thumb />
</Slider.Track>
</Slider.Control>
</Slider.Root>
);
};
```
Use `onValueChange` to keep the controlled value current during an interaction. `onValueCommitted` handles the final value when the interaction finishes, for example to save it. Both callbacks are also available in uncontrolled mode without taking ownership of the value.
## Range
```tsx
import { Slider } from 'twenty-ui/primitives/input';
export const PriceRange = () => (
<Slider.Root
defaultValue={[25, 75]}
min={0}
max={100}
minStepsBetweenValues={10}
>
<Slider.Label>Price range</Slider.Label>
<Slider.Value />
<Slider.Control>
<Slider.Track>
<Slider.Indicator />
<Slider.Thumb index={0} aria-label="Minimum price" />
<Slider.Thumb index={1} aria-label="Maximum price" />
</Slider.Track>
</Slider.Control>
</Slider.Root>
);
```
Give range thumbs distinct accessible names and an `index` corresponding to their value. `minStepsBetweenValues` sets the minimum gap in steps. The root's generic value type is a number or a readonly array of numbers.
To control a range, store the array in React state and pass it through `value` and `onValueChange`, just as for a single value.
## Keyboard and appearance
Arrow keys adjust a focused thumb by a step. Home and End move it toward the bounds while respecting range constraints. `orientation="vertical"` changes the layout and keyboard direction. Use `disabled` on `Root` to prevent changes.
Set `color="success"` for the alternative palette; the default is `accent`.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<SliderReference />

View File

@@ -0,0 +1,69 @@
---
title: Switch
description: A labeled on or off control with controlled and uncontrolled state.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import SwitchReference from '/snippets/ui/generated/input/switch.mdx';
Use `Switch` for an on or off preference. Give the control a visible label or an accessible name.
<StoryEmbed
storyId="ui-input-switch-switch--default"
title="Switch example"
height={240}
/>
## Uncontrolled state
Use `defaultChecked` to set the initial checked state. The switch owns subsequent changes.
```tsx
import { Switch } from 'twenty-ui/primitives/input';
export const NotificationsPreference = () => (
<label>
<Switch name="notifications" defaultChecked />
Email notifications
</label>
);
```
## Controlled state
Pass `checked` and update it with `onCheckedChange` to own the same preference in React state.
```tsx
import { useState } from 'react';
import { Switch } from 'twenty-ui/primitives/input';
export const NotificationsPreference = () => {
const [checked, setChecked] = useState(true);
return (
<label>
<Switch
name="notifications"
checked={checked}
onCheckedChange={setChecked}
/>
Email notifications
</label>
);
};
```
`onCheckedChange` receives the next boolean and event details. The `size` prop accepts `sm` or `md` and defaults to `md`.
## Keyboard and forms
Tab moves focus to the switch; Space toggles it. Clicking its associated label also toggles it. Use `disabled` to prevent interaction or `readOnly` to retain a focusable control whose value cannot be changed.
A named switch submits its value when checked. Use `value` to customize that value, and `uncheckedValue` if the form should also submit a value when it is off.
## Props
The reference includes component-specific and inherited Base UI props. Native attributes such as `aria-label` and `data-*` are also accepted.
<SwitchReference />

View File

@@ -0,0 +1,93 @@
---
title: Textarea
description: Enter multiple lines of text with optional automatic resizing.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import TextareaReference from '/snippets/ui/generated/input/textarea.mdx';
Use `Textarea` for notes, descriptions, and other multiline values. It integrates with [Field](/ui/primitives/input/field) for labels and validation.
<StoryEmbed
storyId="ui-input-textarea--default"
title="Textarea example"
height={240}
/>
## Uncontrolled state
Use `defaultValue` to set the initial notes. The textarea owns subsequent changes.
```tsx
import { Field, Textarea } from 'twenty-ui/primitives/input';
export const NotesField = () => (
<Field.Root name="notes">
<Field.Label>Notes</Field.Label>
<Textarea rows={4} defaultValue="Follow up next week." />
<Field.Description>Add context for your team.</Field.Description>
</Field.Root>
);
```
Native textarea attributes such as `rows`, `maxLength`, `required`, and `readOnly` are available. `size` accepts `sm` or `md` and defaults to `md`.
## Controlled state
Pass `value` and update it with `onValueChange` to own the same notes in React state. The callback receives the next string value and event details; native `onChange` is also available.
```tsx
import { useState } from 'react';
import { Field, Textarea } from 'twenty-ui/primitives/input';
export const NotesField = () => {
const [notes, setNotes] = useState('Follow up next week.');
return (
<Field.Root name="notes">
<Field.Label>Notes</Field.Label>
<Textarea rows={4} value={notes} onValueChange={setNotes} />
<Field.Description>Add context for your team.</Field.Description>
</Field.Root>
);
};
```
## Automatic resizing
Automatic resizing works in either state mode. This example also uses the controlled value for a character count.
```tsx
import { useState } from 'react';
import { Field, Textarea } from 'twenty-ui/primitives/input';
export const DescriptionField = () => {
const [description, setDescription] = useState('');
return (
<Field.Root name="description">
<Field.Label>Description</Field.Label>
<Textarea
value={description}
onValueChange={setDescription}
autoResize
rows={2}
maxRows={6}
maxLength={500}
/>
<Field.Description>
{description.length} of 500 characters
</Field.Description>
</Field.Root>
);
};
```
`autoResize` adjusts the textarea's height as its content changes. Use `maxRows` to cap the height. Keep keyboard focus on the textarea while the user types and use helper text for limits.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<TextareaReference />

View File

@@ -0,0 +1,121 @@
---
title: ListItem
description: Compose consistent rows with icons, descriptions, indicators, and actions.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import ListItemReference from '/snippets/ui/generated/navigation/list-item.mdx';
`ListItem` supplies the visual layout shared by list and menu rows. Use [Menu](/ui/primitives/surfaces/menu) or [Select](/ui/primitives/input/select) when you need their selection and keyboard behavior.
<StoryEmbed
storyId="ui-navigation-listitem--default"
title="ListItem example"
height={240}
/>
## Anatomy
`ListItem` exposes content slots through props. These slots arrange content inside one row; they are not separately exported components.
```text
ListItem
├── startIcon
├── children
├── description
├── actions
└── endIcon
```
| Slot | Requirement | Purpose |
| ------------------------------------ | -------------------------------- | ------------------------------------------------------------------------------- |
| `children` | Required for a visible row label | Supplies the main text. |
| `startIcon`, `endIcon` | Optional | Add supporting icons at either end of the row content. |
| `description` | Optional | Adds supporting text inline or at the end, according to `descriptionPlacement`. |
| `actions` | Optional | Adds trailing content, such as a separately focusable action. |
| `indicator`, `hotkeys`, `hasSubmenu` | Optional | Configure built-in selection marks, shortcut hints, and the submenu indicator. |
The row supplies layout. Use `render` to choose its element and semantics. For a row with independent action buttons, keep the row itself non-interactive so those buttons remain separate controls.
## Action row
```tsx
import { IconCopy } from 'twenty-ui/icon';
import { ListItem } from 'twenty-ui/primitives/navigation';
type DuplicateActionProps = { onDuplicate: () => void };
export const DuplicateAction = ({ onDuplicate }: DuplicateActionProps) => (
<ListItem
render={<button type="button" />}
startIcon={<IconCopy aria-hidden />}
description="Create a copy"
onClick={onDuplicate}
>
Duplicate
</ListItem>
);
```
The default element is a `div`. Use `render` to give an interactive row appropriate semantics, such as a button or link. `hotkeys` displays shortcut hints; your application must register the shortcuts.
## Selection indicator
```tsx
import { useState } from 'react';
import { ListItem } from 'twenty-ui/primitives/navigation';
export const ShowArchivedToggle = () => {
const [selected, setSelected] = useState(false);
return (
<ListItem
render={<button type="button" aria-pressed={selected} />}
selected={selected}
indicator="check"
onClick={() => setSelected(!selected)}
>
Show archived records
</ListItem>
);
};
```
`selected`, `focused`, and `disabled` control row styling. The `check` and `checkbox` indicators are visual; use a real [Checkbox](/ui/primitives/input/checkbox) for a form control. When using a disabled button or link through `render`, also supply the element's appropriate disabled behavior and semantics.
## Slots
Use `startIcon` and `endIcon` for icons, `description` for supporting text, `actions` for trailing content, and `hasSubmenu` for a submenu indicator. `descriptionPlacement` accepts `inline` or `end`. `color="danger"` styles a destructive action. Avoid nesting interactive controls inside a row rendered as a button.
```tsx
import { Button } from 'twenty-ui/primitives/input';
import { ListItem } from 'twenty-ui/primitives/navigation';
type ContactRowProps = { onOpen: () => void };
export const ContactRow = ({ onOpen }: ContactRowProps) => (
<ListItem
description="Account owner"
actions={
<Button
type="button"
aria-label="Open Alex Morgan's profile"
variant="ghost"
size="sm"
onClick={onOpen}
>
Open
</Button>
}
>
Alex Morgan
</ListItem>
);
```
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<ListItemReference />

View File

@@ -0,0 +1,126 @@
---
title: Tabs
description: Switch between related panels with accessible tab navigation.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import TabsReference from '/snippets/ui/generated/navigation/tabs.mdx';
`Tabs` connects each tab to a panel using a shared `value`. Keep the values unique within a root.
<StoryEmbed
storyId="ui-navigation-tabs--documentation"
title="Tabs example"
height={240}
/>
## Anatomy
Place tabs inside `Tabs.List` and their panels alongside the list, all within the same `Tabs.Root`.
```text
Tabs.Root
├── Tabs.List
│ ├── Tabs.Tab
│ └── Tabs.Tab
├── Tabs.Panel
└── Tabs.Panel
```
| Part | Requirement | Purpose |
| ------- | -------------------- | -------------------------------------------------------------------------- |
| `Root` | Required | Owns the active value and orientation. |
| `List` | Required | Groups the tabs and supplies keyboard navigation and the active indicator. |
| `Tab` | One per panel | Activates the panel with the same `value`. |
| `Panel` | One per content view | Displays its content when its `value` is active. |
Give the list an accessible name and match each selectable tab's `value` to its panel's `value`. Twenty UI includes the active indicator in `List`. Use `Tab.startIcon` and `Tab.badge` for optional supporting content while keeping the tab label in its children.
## Uncontrolled state
Use `defaultValue` to set the initial active tab. The tabs own subsequent selection changes.
```tsx
import { Tabs } from 'twenty-ui/primitives/navigation';
export const RecordTabs = () => (
<Tabs.Root defaultValue="overview">
<Tabs.List aria-label="Record details">
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="activity" badge={3}>
Activity
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview">Company and contact details.</Tabs.Panel>
<Tabs.Panel value="activity">Recent calls and emails.</Tabs.Panel>
</Tabs.Root>
);
```
`Tabs.Tab` accepts `startIcon`, `badge`, and a `size` of `sm` or `md` (default `sm`). The active indicator is included in `Tabs.List`.
## Controlled state
Pass `value` and update it with `onValueChange` to own the same active tab in React state. The callback receives the next value and event details.
```tsx
import { useState } from 'react';
import { Tabs } from 'twenty-ui/primitives/navigation';
export const RecordTabs = () => {
const [tab, setTab] = useState<string | number | null>('overview');
return (
<Tabs.Root value={tab} onValueChange={setTab}>
<Tabs.List aria-label="Record details">
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="activity" badge={3}>
Activity
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="overview">Company and contact details.</Tabs.Panel>
<Tabs.Panel value="activity">Recent calls and emails.</Tabs.Panel>
</Tabs.Root>
);
};
```
## Manual activation
Keyboard activation is independent of state ownership. Use `activateOnFocus={false}` with either controlled or uncontrolled tabs.
```tsx
import { useState } from 'react';
import { Tabs } from 'twenty-ui/primitives/navigation';
export const SettingsTabs = () => {
const [tab, setTab] = useState<string | number | null>('profile');
return (
<Tabs.Root value={tab} onValueChange={setTab}>
<Tabs.List aria-label="Settings" activateOnFocus={false}>
<Tabs.Tab value="profile">Profile</Tabs.Tab>
<Tabs.Tab value="notifications">Notifications</Tabs.Tab>
<Tabs.Tab value="billing" disabled>
Billing
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="profile">Profile settings.</Tabs.Panel>
<Tabs.Panel value="notifications">Notification settings.</Tabs.Panel>
</Tabs.Root>
);
};
```
With `activateOnFocus={false}`, arrow keys move focus and Enter or Space activates the focused tab. `loopFocus` defaults to `true`. Disabled tabs are skipped. Use `orientation="vertical"` on `Root` for vertical navigation.
## Panel lifetime
Use `keepMounted` on a panel when its contents must remain mounted while inactive.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<TabsReference />

View File

@@ -0,0 +1,96 @@
---
title: Primitives
sidebarTitle: Overview
description: Foundational controls for building interfaces with Twenty UI.
---
Primitives are the foundational controls in Twenty UI. They include inputs, display elements, navigation, and compound controls such as Select, Menu, and AlertDialog. Each primitive receives data and callbacks from your application.
Import primitives from a family entry point, such as `twenty-ui/primitives/input`. See [Getting started](/ui/getting-started) for installation and theme setup.
[Components](/ui/components/overview) combine these primitives into shared design presets and reusable app building blocks.
## State ownership
Use uncontrolled state when a component can manage its own interactions. A default prop supplies the initial state. Use controlled state when the application needs to coordinate that state with other controls, navigation, or application data.
| State | Uncontrolled initial state | Controlled state | Change callback |
| ------------------------------- | -------------------------- | ---------------- | ----------------- |
| Input, selection, or active tab | `defaultValue` | `value` | `onValueChange` |
| Checkbox or switch | `defaultChecked` | `checked` | `onCheckedChange` |
| Popup visibility | `defaultOpen` | `open` | `onOpenChange` |
In controlled mode, pass the current state and update it in the change callback. In uncontrolled mode, you can still listen to changes without taking ownership of the state. Adding a callback alone does not make a component controlled.
Choose a mode for each state and keep it for the lifetime of the component. Default props initialize state; changing them later does not replace the current state. Avoid passing both the default and controlled prop for the same state.
Some components have more than one independent state. For example, [Select](/ui/primitives/input/select#open-state) can have a controlled value and uncontrolled popup visibility. [Menu](/ui/primitives/surfaces/menu#selection-and-submenus) can manage its own visibility while the application controls checkbox and radio selections.
State belongs to the part that manages the interaction. Configure radio selection on [RadioGroup](/ui/primitives/input/radio-group), not on each Radio. Display components such as Text do not have a controlled or uncontrolled mode. [Toast](/ui/primitives/feedback/toast) delegates its visibility and removal to the application.
## Composition
Compound primitives expose parts such as `Select.Root`, `Select.Trigger`, and `Select.Popup`. Keep related parts under the same root in the component tree so they share state and accessibility relationships. Each component's anatomy section identifies the required parts and optional content.
Slots such as `InputGroup.startElement` and `ListItem.actions` accept React nodes as props. They add content within a component's layout. Parts and slots are different APIs; use the names listed in the component guide.
### Popup composition
Twenty UI packages the supporting overlay elements inside each public `Popup` part:
| Component | Included in `Popup` | Configure on `Popup` |
| ------------- | -------------------------------------- | -------------------------------------------------------- |
| `Select` | Portal and positioner | Placement, anchor, and portal container |
| `Menu` | Portal and positioner | Placement, anchor, portal container, and mounting |
| `Popover` | Portal, positioner, and optional arrow | Placement, anchor, portal container, mounting, and arrow |
| `AlertDialog` | Portal, backdrop, and viewport | Size, portal container, mounting, and focus |
Place content directly inside these popup parts. Their portal may move the rendered DOM to another container while preserving the component hierarchy and context. See [theming](/ui/theming#scope-an-override) for scoped portal containers.
### Custom components
For parts that support `render`, pass a compatible element or component to replace the default element. For example, `Popover.Trigger` can use a Twenty UI `Button` while retaining its trigger behavior.
A custom wrapper must pass the supplied props and ref to the interactive element. This preserves event handlers, accessibility attributes, positioning, and focus restoration. This example forwards them through `Button`, which attaches them to its native button.
```tsx
import { type ComponentPropsWithRef } from 'react';
import { Button } from 'twenty-ui/primitives/input';
import { Popover } from 'twenty-ui/primitives/surfaces';
type HelpButtonProps = ComponentPropsWithRef<typeof Button>;
const HelpButton = ({ ref, ...props }: HelpButtonProps) => (
<Button {...props} ref={ref} variant="outline" />
);
export const ImportHelp = () => (
<Popover.Root>
<Popover.Trigger render={<HelpButton>Import help</HelpButton>} />
<Popover.Popup>
<Popover.Title>Import a CSV file</Popover.Title>
<Popover.Description>
Use a header row to identify your columns.
</Popover.Description>
<Popover.Close render={<Button>Got it</Button>} />
</Popover.Popup>
</Popover.Root>
);
```
Pass the button through `render` and keep each action as a single interactive control. Preserve the handlers and ref supplied to your wrapper when adding your own behavior. The ref must reach the element that receives focus.
Use a native button for button-like parts. On parts that expose `nativeButton`, set `nativeButton={false}` only when the rendered element is not a native button. A custom component that ultimately renders a button, such as `HelpButton`, keeps the default button behavior.
## Browse primitives
Each page includes usage examples, interaction guidance, and a generated API reference.
| Family | Primitives |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Data display | [Avatar](/ui/primitives/data-display/avatar), [Chip](/ui/primitives/data-display/chip), [Tag](/ui/primitives/data-display/tag), [Status](/ui/primitives/data-display/status) |
| Forms | [Button](/ui/primitives/input/button), [ButtonGroup](/ui/primitives/input/button-group), [Field](/ui/primitives/input/field), [Input](/ui/primitives/input/input), [InputGroup](/ui/primitives/input/input-group), [Textarea](/ui/primitives/input/textarea), [Checkbox](/ui/primitives/input/checkbox), [Radio](/ui/primitives/input/radio), [RadioGroup](/ui/primitives/input/radio-group), [Select](/ui/primitives/input/select), [Slider](/ui/primitives/input/slider), [Switch](/ui/primitives/input/switch) |
| Navigation | [ListItem](/ui/primitives/navigation/list-item), [Tabs](/ui/primitives/navigation/tabs) |
| Surfaces | [AlertDialog](/ui/primitives/surfaces/alert-dialog), [Dialog](/ui/primitives/surfaces/dialog), [Menu](/ui/primitives/surfaces/menu), [Popover](/ui/primitives/surfaces/popover), [Tooltip](/ui/primitives/surfaces/tooltip) |
| Feedback | [Toast](/ui/primitives/feedback/toast) |
| Typography | [Heading](/ui/primitives/typography/heading), [Text](/ui/primitives/typography/text) |

View File

@@ -0,0 +1,162 @@
---
title: AlertDialog
description: Ask for confirmation before an action that needs an explicit decision.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import AlertDialogReference from '/snippets/ui/generated/surfaces/alert-dialog.mdx';
Use `AlertDialog` for a decision that interrupts the current workflow, such as confirming deletion. It provides an accessible modal with a backdrop and focus management.
<StoryEmbed
storyId="ui-surfaces-alertdialog--documentation"
title="AlertDialog example"
height={440}
/>
## Anatomy
Keep the trigger and popup inside `AlertDialog.Root`. This tree shows the recommended layout for a confirmation dialog.
```text
AlertDialog.Root
├── AlertDialog.Trigger
└── AlertDialog.Popup
├── AlertDialog.Header
│ ├── AlertDialog.Title
│ └── AlertDialog.Description
├── AlertDialog.Body
└── AlertDialog.Footer
├── AlertDialog.Close
└── Confirmation action
```
| Part | Requirement | Purpose |
| -------------------------- | --------------------------------------------- | ------------------------------------------------------------------------ |
| `Root` | Required | Coordinates open state and modal behavior. |
| `Trigger` | Optional for a programmatically opened dialog | Opens the dialog and provides a return focus target. |
| `Popup` | Required | Renders the modal content with focus management. |
| `Title` | Recommended | Supplies the visible dialog name. An accessible name is always required. |
| `Description` | Recommended for a confirmation | Explains the decision and its consequences. |
| `Header`, `Body`, `Footer` | Optional layout parts | Arrange headings, additional content, and actions. |
| `Close` | Recommended for cancellation | Supplies an explicit close action. |
`Popup` includes the portal, backdrop, and viewport. Put content directly inside it and set `size`, `container`, and focus props on `Popup`. `Header`, `Body`, and `Footer` arrange content; `Title` and `Description` provide the accessible relationships.
The confirmation action is application content, not an exported part. Use `Close` with a synchronous handler or a `Button` with controlled open state for asynchronous confirmation.
## Uncontrolled state
Use `defaultOpen` to set the initial visibility. The dialog owns subsequent changes from its trigger, close controls, and dismissal interactions.
```tsx
import { AlertDialog } from 'twenty-ui/primitives/surfaces';
type DeleteConfirmationProps = { onDelete: () => void };
export const DeleteConfirmation = ({ onDelete }: DeleteConfirmationProps) => (
<AlertDialog.Root defaultOpen={false}>
<AlertDialog.Trigger>Delete record</AlertDialog.Trigger>
<AlertDialog.Popup>
<AlertDialog.Header>
<AlertDialog.Title>Delete this record?</AlertDialog.Title>
<AlertDialog.Description>
This action cannot be undone.
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Close>Cancel</AlertDialog.Close>
<AlertDialog.Close onClick={onDelete}>Delete</AlertDialog.Close>
</AlertDialog.Footer>
</AlertDialog.Popup>
</AlertDialog.Root>
);
```
`defaultOpen` defaults to `false`, so it can be omitted. This example uses a synchronous confirmation callback and closes after the action runs.
## Controlled state
Pass `open` and update it with `onOpenChange` to own the same dialog's visibility in React state. The callback receives the next boolean and event details, including changes requested by the trigger and close controls.
```tsx
import { useState } from 'react';
import { AlertDialog } from 'twenty-ui/primitives/surfaces';
type DeleteConfirmationProps = { onDelete: () => void };
export const DeleteConfirmation = ({ onDelete }: DeleteConfirmationProps) => {
const [open, setOpen] = useState(false);
return (
<AlertDialog.Root open={open} onOpenChange={setOpen}>
<AlertDialog.Trigger>Delete record</AlertDialog.Trigger>
<AlertDialog.Popup>
<AlertDialog.Header>
<AlertDialog.Title>Delete this record?</AlertDialog.Title>
<AlertDialog.Description>
This action cannot be undone.
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Close>Cancel</AlertDialog.Close>
<AlertDialog.Close onClick={onDelete}>Delete</AlertDialog.Close>
</AlertDialog.Footer>
</AlertDialog.Popup>
</AlertDialog.Root>
);
};
```
For asynchronous confirmation, use a `Button` with your action handler instead of `AlertDialog.Close`. Keep `open` true while the action is pending, call `setOpen(false)` after it succeeds, and show an error inside the dialog if it fails.
## Compose with buttons
Use `render` to give the trigger and close controls Twenty UI button styling. The dialog parts supply their interaction handlers and refs to the buttons.
```tsx
import { Button } from 'twenty-ui/primitives/input';
import { AlertDialog } from 'twenty-ui/primitives/surfaces';
type DeleteConfirmationProps = { onDelete: () => void };
export const DeleteConfirmation = ({ onDelete }: DeleteConfirmationProps) => (
<AlertDialog.Root>
<AlertDialog.Trigger
render={<Button color="danger">Delete record</Button>}
/>
<AlertDialog.Popup>
<AlertDialog.Header>
<AlertDialog.Title>Delete this record?</AlertDialog.Title>
<AlertDialog.Description>
This action cannot be undone.
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Close render={<Button variant="outline">Cancel</Button>} />
<AlertDialog.Close
onClick={onDelete}
render={<Button color="danger">Delete</Button>}
/>
</AlertDialog.Footer>
</AlertDialog.Popup>
</AlertDialog.Root>
);
```
Keep each action as a single interactive control. If you wrap `Button` in your own component, [forward the supplied props and ref](/ui/primitives/overview#custom-components).
## Sizing and portals
`Popup.size` defaults to `md`; see the reference for supported sizes. Use `container` to override the theme's portal container and `keepMounted` to retain mounted content when closed.
## Focus and dismissal
The dialog traps focus while open. Escape can dismiss it; clicking the backdrop does not. Always provide a visible Cancel or Close action. Keep the initial focus on the least destructive action, and preserve a meaningful return focus target when the action removes its trigger. Use `Popup.initialFocus` and `Popup.finalFocus` to customize focus behavior.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<AlertDialogReference />

View File

@@ -0,0 +1,39 @@
---
title: Dialog
description: Give a modal a visible heading and accessible name with Dialog.Title.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import DialogReference from '/snippets/ui/generated/surfaces/dialog.mdx';
Use `Dialog.Title` inside Twenty UI's existing `Modal`. It provides the shared modal heading style and automatically connects the heading to the modal's accessible name. The public `Dialog` namespace currently exports `Title`.
<StoryEmbed
storyId="ui-surfaces-dialog--documentation"
title="Dialog title example"
height={300}
/>
## Add a modal title
```tsx
import { Dialog, Modal } from 'twenty-ui/primitives/surfaces';
import { Text } from 'twenty-ui/primitives/typography';
type CreditModalProps = { isOpen: boolean };
export const CreditModal = ({ isOpen }: CreditModalProps) => (
<Modal isOpen={isOpen} padding="large" autoHeight>
<Dialog.Title>Grant credits</Dialog.Title>
<Text>Add credits to this workspace.</Text>
</Modal>
);
```
The title defaults to an `h2` with large type, primary text color, centered alignment, and spacing-4 below it. Use `level` to change the document outline independently of `size`. Native `className` and `style` can adjust alignment and spacing.
Native attributes, refs, and `render` are forwarded to the heading. Keep a heading element when using `render`, and keep the title inside `Modal` so its accessible naming relationship is available.
## Props
<DialogReference />

View File

@@ -0,0 +1,235 @@
---
title: Menu
description: Present commands, checked options, radio choices, and nested menus.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import MenuReference from '/snippets/ui/generated/surfaces/menu.mdx';
Use `Menu` for actions opened from a trigger. Its items supply keyboard navigation and the visual layout of [ListItem](/ui/primitives/navigation/list-item).
<StoryEmbed
storyId="ui-surfaces-menu--documentation"
title="Menu example"
height={440}
/>
## Anatomy
Keep the trigger and popup inside the same `Menu.Root`. Put action or selection items inside the popup.
```text
Menu.Root
├── Menu.Trigger
└── Menu.Popup
├── Menu.Item
├── Menu.Separator
└── Menu.Item
```
| Part | Requirement | Purpose |
| ------------------------------- | --------------------------------- | -------------------------------------------------------------------------- |
| `Root` | Required | Owns open state and coordinates menu interaction. |
| `Trigger` | Required for a trigger-based menu | Opens the menu and provides its accessible name. |
| `Popup` | Required | Renders the positioned, portaled menu with keyboard navigation. |
| `Item` | One per action | Runs a command when selected. |
| `CheckboxItem` | Optional | Toggles an independent boolean option. |
| `RadioGroup`, `RadioItem` | Optional | Coordinate mutually exclusive options. Put radio items inside their group. |
| `Group`, `GroupLabel` | Optional | Group related items under a label. |
| `Separator` | Optional | Separates groups visually. |
| `SubmenuRoot`, `SubmenuTrigger` | Optional | Add a nested menu with its own popup. |
`Popup` includes the portal and positioner. Menu items include the row layout and selection indicators. Supply labels as children and use `startIcon`, `endIcon`, and `description` for supporting content.
## Uncontrolled state
Use `defaultOpen` to set the initial visibility. The menu owns subsequent changes from its trigger, item selection, and dismissal interactions.
```tsx
import { IconCopy, IconTrash } from 'twenty-ui/icon';
import { Menu } from 'twenty-ui/primitives/surfaces';
type RecordMenuProps = { onDuplicate: () => void; onDelete: () => void };
export const RecordMenu = ({ onDuplicate, onDelete }: RecordMenuProps) => (
<Menu.Root defaultOpen={false}>
<Menu.Trigger>Record actions</Menu.Trigger>
<Menu.Popup>
<Menu.Item startIcon={<IconCopy />} onClick={onDuplicate}>
Duplicate
</Menu.Item>
<Menu.Separator />
<Menu.Item startIcon={<IconTrash />} color="danger" onClick={onDelete}>
Delete
</Menu.Item>
</Menu.Popup>
</Menu.Root>
);
```
`defaultOpen` defaults to `false`, so it can be omitted. Action items close the menu when selected by default. `disabled` prevents selection. `hotkeys` displays shortcut hints; register shortcut handlers in your application.
## Controlled state
Pass `open` and update it with `onOpenChange` to own the same menu's visibility in React state. The callback receives the next boolean and event details, including dismissal and item selection.
```tsx
import { useState } from 'react';
import { IconCopy, IconTrash } from 'twenty-ui/icon';
import { Menu } from 'twenty-ui/primitives/surfaces';
type RecordMenuProps = { onDuplicate: () => void; onDelete: () => void };
export const RecordMenu = ({ onDuplicate, onDelete }: RecordMenuProps) => {
const [open, setOpen] = useState(false);
return (
<Menu.Root open={open} onOpenChange={setOpen}>
<Menu.Trigger>Record actions</Menu.Trigger>
<Menu.Popup>
<Menu.Item startIcon={<IconCopy />} onClick={onDuplicate}>
Duplicate
</Menu.Item>
<Menu.Separator />
<Menu.Item startIcon={<IconTrash />} color="danger" onClick={onDelete}>
Delete
</Menu.Item>
</Menu.Popup>
</Menu.Root>
);
};
```
## Selection and submenus
Item selection is independent of popup visibility. Checkbox items use `defaultChecked` or `checked` with `onCheckedChange`. Radio groups use `defaultValue` or `value` with `onValueChange`.
For a submenu, place `SubmenuRoot` inside the parent popup, with its own trigger and popup. Each nested popup retains its own positioning and keyboard behavior.
```text
Menu.Popup
└── Menu.SubmenuRoot
├── Menu.SubmenuTrigger
└── Menu.Popup
└── Menu.RadioGroup
├── Menu.RadioItem
└── Menu.RadioItem
```
### Uncontrolled item state
Each checkbox item and radio group owns its selection. Both the menu and submenu also manage their own visibility.
```tsx
import { Menu } from 'twenty-ui/primitives/surfaces';
export const ViewMenu = () => (
<Menu.Root>
<Menu.Trigger>View options</Menu.Trigger>
<Menu.Popup>
<Menu.CheckboxItem defaultChecked={false}>
Show archived records
</Menu.CheckboxItem>
<Menu.Separator />
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger>Layout</Menu.SubmenuTrigger>
<Menu.Popup>
<Menu.RadioGroup defaultValue="list">
<Menu.RadioItem value="list">List</Menu.RadioItem>
<Menu.RadioItem value="board">Board</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Popup>
</Menu.SubmenuRoot>
</Menu.Popup>
</Menu.Root>
);
```
### Controlled item state
The application owns the same selections in React state. Popup visibility remains uncontrolled because neither root receives `open`.
```tsx
import { useState } from 'react';
import { Menu } from 'twenty-ui/primitives/surfaces';
export const ViewMenu = () => {
const [showArchived, setShowArchived] = useState(false);
const [view, setView] = useState('list');
return (
<Menu.Root>
<Menu.Trigger>View options</Menu.Trigger>
<Menu.Popup>
<Menu.CheckboxItem
checked={showArchived}
onCheckedChange={setShowArchived}
>
Show archived records
</Menu.CheckboxItem>
<Menu.Separator />
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger>Layout</Menu.SubmenuTrigger>
<Menu.Popup>
<Menu.RadioGroup value={view} onValueChange={setView}>
<Menu.RadioItem value="list">List</Menu.RadioItem>
<Menu.RadioItem value="board">Board</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Popup>
</Menu.SubmenuRoot>
</Menu.Popup>
</Menu.Root>
);
};
```
Checkbox and radio items keep the popup open by default so users can adjust options. Use `closeOnClick` to change that behavior. Nest a `SubmenuRoot`, `SubmenuTrigger`, and `Popup` inside a parent popup for submenus. Each `SubmenuRoot` has its own `defaultOpen`, `open`, and `onOpenChange` props, independent of the parent menu.
## Compose a trigger
Use `render` to make a Twenty UI `Button` the trigger. The menu supplies the button's activation handlers and accessibility attributes.
```tsx
import { IconCopy, IconTrash } from 'twenty-ui/icon';
import { Button } from 'twenty-ui/primitives/input';
import { Menu } from 'twenty-ui/primitives/surfaces';
type RecordMenuProps = { onDuplicate: () => void; onDelete: () => void };
export const RecordMenu = ({ onDuplicate, onDelete }: RecordMenuProps) => (
<Menu.Root>
<Menu.Trigger render={<Button variant="outline">Record actions</Button>} />
<Menu.Popup>
<Menu.Item startIcon={<IconCopy />} onClick={onDuplicate}>
Duplicate
</Menu.Item>
<Menu.Separator />
<Menu.Item startIcon={<IconTrash />} color="danger" onClick={onDelete}>
Delete
</Menu.Item>
</Menu.Popup>
</Menu.Root>
);
```
Put `Button` in the trigger's `render` prop to preserve a single interactive control. For custom wrapper components, [forward the supplied props and ref](/ui/primitives/overview#custom-components).
## Keyboard and placement
Arrow keys move through the items, including disabled ones, which receive focus but cannot be selected; typeahead finds items by their text. Enter or Space selects an item. Escape closes the menu and returns focus to its trigger. Use clear text labels even when items have icons.
`Popup` owns its portal and accepts placement props and a `container` override. Placement defaults depend on whether it is a top-level menu or a submenu:
| Popup | `side` | `align` | `sideOffset` | `alignOffset` |
| -------------- | ------------ | ------- | ------------ | ------------- |
| Top-level menu | `bottom` | `start` | `8` | `0` |
| Submenu | `inline-end` | `start` | `0` | `-4` |
It uses the [theme's portal container](/ui/theming) by default.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<MenuReference />

View File

@@ -0,0 +1,127 @@
---
title: Popover
description: Display contextual content anchored to a trigger.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import PopoverReference from '/snippets/ui/generated/surfaces/popover.mdx';
Use `Popover` for contextual information or lightweight controls. `Popup` includes positioning and a portal, so you can compose content directly inside it.
<StoryEmbed
storyId="ui-surfaces-popover--documentation"
title="Popover example"
height={440}
/>
## Anatomy
Keep the trigger and popup inside `Popover.Root`. The popup can contain text, form controls, and actions.
```text
Popover.Root
├── Popover.Trigger
└── Popover.Popup
├── Popover.Title
├── Popover.Description
└── Popover.Close
```
| Part | Requirement | Purpose |
| ------------- | --------------------------------------- | --------------------------------------------------------------------- |
| `Root` | Required | Coordinates open state and interactions. |
| `Trigger` | Required for a trigger-anchored popover | Opens or closes the popup and supplies its anchor. |
| `Popup` | Required | Renders positioned content with focus management. |
| `Title` | Recommended | Supplies a visible popup name. An accessible name is always required. |
| `Description` | Optional | Associates explanatory text with the popup. |
| `Close` | Optional | Provides an explicit close action within the popup. |
`Popup` includes the portal and positioner. Set `arrow` on `Popup` to add its built-in arrow. All parts share their root's context even though the popup renders in a portal; the tree describes component nesting, not the final DOM placement.
## Uncontrolled state
Use `defaultOpen` to set the initial visibility. The popover owns subsequent changes from its trigger, close controls, and dismissal interactions.
```tsx
import { Popover } from 'twenty-ui/primitives/surfaces';
export const ImportHelp = () => (
<Popover.Root defaultOpen={false}>
<Popover.Trigger>Import help</Popover.Trigger>
<Popover.Popup arrow>
<Popover.Title>Import a CSV file</Popover.Title>
<Popover.Description>
Use a header row to identify your columns.
</Popover.Description>
<Popover.Close>Got it</Popover.Close>
</Popover.Popup>
</Popover.Root>
);
```
`defaultOpen` defaults to `false`, so it can be omitted. Use `Title` and `Description` to identify the popup. The trigger renders a button by default; `render` lets you supply a compatible element.
## Controlled state
Pass `open` and update it with `onOpenChange` to own the same popover's visibility in React state.
```tsx
import { useState } from 'react';
import { Popover } from 'twenty-ui/primitives/surfaces';
export const ImportHelp = () => {
const [open, setOpen] = useState(false);
return (
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger>Import help</Popover.Trigger>
<Popover.Popup arrow>
<Popover.Title>Import a CSV file</Popover.Title>
<Popover.Description>
Use a header row to identify your columns.
</Popover.Description>
<Popover.Close>Got it</Popover.Close>
</Popover.Popup>
</Popover.Root>
);
};
```
`onOpenChange` receives the next boolean and event details, including changes requested by the trigger, close controls, Escape, and outside interaction.
## Compose with buttons
Pass a Twenty UI `Button` through `render` to use it as the trigger or close control. The popover keeps its open state, keyboard behavior, and focus handling.
```tsx
import { Button } from 'twenty-ui/primitives/input';
import { Popover } from 'twenty-ui/primitives/surfaces';
export const ImportHelp = () => (
<Popover.Root>
<Popover.Trigger render={<Button variant="outline">Import help</Button>} />
<Popover.Popup arrow>
<Popover.Title>Import a CSV file</Popover.Title>
<Popover.Description>
Use a header row to identify your columns.
</Popover.Description>
<Popover.Close render={<Button>Got it</Button>} />
</Popover.Popup>
</Popover.Root>
);
```
`render` composes behavior onto the supplied button. Custom wrapper components must [forward the supplied props and ref](/ui/primitives/overview#custom-components) to preserve that behavior.
## Placement and focus
`Popup` defaults to `side="bottom"`, `align="center"`, and `sideOffset={8}`. Set `arrow` to show an arrow. Set `container` to override the theme's portal container; see [theming](/ui/theming).
Escape closes the popup and focus returns to the trigger. Outside interaction dismisses it by default. Use the focus props on `Popup` when its content needs a specific initial or return focus target.
## Props
The reference is generated from the public component types. Native attributes, including accessible names and event handlers, are also accepted on parts that render elements.
<PopoverReference />

View File

@@ -0,0 +1,125 @@
---
title: Tooltip
description: Show supplementary information when a trigger is hovered or focused.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import TooltipReference from '/snippets/ui/generated/surfaces/tooltip.mdx';
Use `Tooltip` for a short explanation of a control or truncated text. Keep essential information visible, give controls an accessible name, and use [Popover](/ui/primitives/surfaces/popover) when the content contains links, buttons, or other controls.
<StoryEmbed storyId="ui-surfaces-tooltip--default" title="Tooltip example" />
## Single trigger
```tsx
import { Button } from 'twenty-ui/primitives/input';
import { Tooltip } from 'twenty-ui/primitives/surfaces';
export const ExportHelp = () => (
<Tooltip content="Download the visible records as a CSV file" side="top">
<span>
<Button>Export records</Button>
</span>
</Tooltip>
);
```
The convenience component accepts one trigger element as `children`. Native elements receive the trigger attributes, event handlers, and ref directly. Custom trigger components must forward all of them. A wrapper element also works with a component that does not forward every trigger prop.
Hover opens the tooltip after `delay` milliseconds, which defaults to `600`. Keyboard focus opens it immediately. `closeDelay` defaults to `0`; hovering over the popup keeps it open unless `disableHoverablePopup` is set. Escape dismisses it.
## Controlled state
Use `open` and `onOpenChange` together when the application owns visibility. Use `defaultOpen` only to set the initial visibility of an uncontrolled tooltip.
```tsx
import { useState } from 'react';
import { Button } from 'twenty-ui/primitives/input';
import { Tooltip } from 'twenty-ui/primitives/surfaces';
export const ControlledExportHelp = () => {
const [open, setOpen] = useState(false);
return (
<Tooltip
content="Download the visible records as a CSV file"
open={open}
onOpenChange={setOpen}
delay={300}
>
<span>
<Button>Export records</Button>
</span>
</Tooltip>
);
};
```
## Rich content and composition
`Tooltip.Content` provides a title, optional description, and optional leading icon. Use it as `content` or inside `Tooltip.Popup`.
```tsx
import { IconInfoCircle } from 'twenty-ui/icon';
import { Button } from 'twenty-ui/primitives/input';
import { Tooltip } from 'twenty-ui/primitives/surfaces';
export const ExportDetails = () => (
<Tooltip.Root>
<Tooltip.Trigger render={<span />}>
<Button>Export records</Button>
</Tooltip.Trigger>
<Tooltip.Popup side="right" align="start" arrow>
<Tooltip.Content
startIcon={<IconInfoCircle />}
description="Only the records matching your current filters are included."
>
Export visible records
</Tooltip.Content>
</Tooltip.Popup>
</Tooltip.Root>
);
```
| Part | Purpose |
| ---------- | ------------------------------------------------------------- |
| `Root` | Coordinates visibility and trigger interactions. |
| `Trigger` | Renders the element that opens the tooltip on hover or focus. |
| `Popup` | Renders positioned content in a portal. |
| `Content` | Styles primary text, a description, and an icon. |
| `Provider` | Coordinates opening delays across a group of tooltips. |
`Tooltip.createHandle()` connects detached triggers to a shared `Root` through their `handle` prop. Keep each popup's content supplementary even when several triggers share it.
## Disabled controls
Native disabled buttons cannot receive keyboard focus. Wrap one in a focusable trigger so users can discover why the action is unavailable.
```tsx
import { Button } from 'twenty-ui/primitives/input';
import { Tooltip } from 'twenty-ui/primitives/surfaces';
export const UnavailableExport = () => (
<Tooltip content="Select at least one record to export">
<span tabIndex={0} aria-label="Export records unavailable">
<Button disabled>Export records</Button>
</span>
</Tooltip>
);
```
Setting `disabled` on `Tooltip` suppresses the tooltip itself. It does not disable the control.
## Placement and overflowing text
The popup defaults to `side="top"`, `align="center"`, `sideOffset={10}`, and a maximum width of `300` pixels. Placement adjusts to available space. Use `arrow` for an arrow and `container` to override the theme's portal destination; see [theming](/ui/theming).
Use `OverflowingTextWithTooltip` when a text label should show its full value only when truncated. Its existing `text`, `tooltipContent`, and `tooltipPlace` props remain available.
## Props
The reference is generated from the public component types. Native popup attributes and event handlers are also accepted.
<TooltipReference />

View File

@@ -0,0 +1,43 @@
---
title: Heading
description: Render semantic headings with independent visual sizes.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import HeadingReference from '/snippets/ui/generated/typography/heading.mdx';
`Heading` renders an `h2` by default. Use `children` for its content, `level` for document structure, and `size` for appearance.
<StoryEmbed
storyId="ui-typography-heading--documentation"
title="Heading example"
height={180}
/>
## Choose a heading level and size
Choose levels in document order. A smaller visual size does not change the level announced by assistive technology.
```tsx
import { Heading } from 'twenty-ui/primitives/typography';
export const WorkspaceTitle = () => (
<Heading level={1} size="lg">
Workspace settings
</Heading>
);
```
Sizes `xs`, `sm`, `md`, and `lg` use the matching font tokens. Headings have no outer margin; their surrounding layout owns spacing.
## Color and composition
Use `color="secondary"` for less prominent headings. `tertiary` is also available, but its contrast depends on the font size, theme, and background. Check readability when using it.
Native attributes, styles, class names, and refs pass to the heading. Use `render` to compose with a custom element or component; that element is responsible for preserving heading semantics.
For a heading with supporting text or actions, use [Section](/ui/components/section).
## Props
<HeadingReference />

View File

@@ -0,0 +1,65 @@
---
title: Text
description: Render text with single-line truncation or a limited number of lines.
---
import { StoryEmbed } from '/snippets/ui/StoryEmbed.mdx';
import TextReference from '/snippets/ui/generated/typography/text.mdx';
`Text` provides overflow handling. It renders a `div` by default and inherits typography from its surroundings.
<StoryEmbed
storyId="ui-typography-text--documentation"
title="Text example"
height={240}
/>
## Truncate one line
Give the text a constrained width, then set `truncate` to show an ellipsis when it overflows:
```tsx
import { Text } from 'twenty-ui/primitives/typography';
export const ContactName = () => (
<Text truncate style={{ maxWidth: 160 }}>
Alexandra Montgomery
</Text>
);
```
## Limit the number of lines
Use `lineClamp` with a positive integer. Choose either `truncate` or `lineClamp`; their props are mutually exclusive.
```tsx
import { Text } from 'twenty-ui/primitives/typography';
export const Summary = () => (
<Text lineClamp={2} style={{ maxWidth: 240 }}>
Follow up with the customer about their onboarding milestones and upcoming
renewal.
</Text>
);
```
## Choose an element
Use `render` when the text needs another semantic element:
```tsx
import { Text } from 'twenty-ui/primitives/typography';
export const Description = () => (
<Text render={<p />}>Account description</Text>
);
```
Truncation affects the visual presentation; it does not shorten the text in the DOM. Provide a way to read the full content when the visible portion is insufficient.
## Props
Native attributes, children, refs, class names, and styles are also accepted.
<TextReference />

View File

@@ -0,0 +1,30 @@
---
title: Server rendering
description: Understand Twenty UI's current server-rendering constraints and theme initialization.
---
Twenty UI is still in alpha and does not yet provide a package-wide server-rendering guarantee. Verify the specific entry points and components you use with your framework's server build and hydration flow.
## Load CSS through your framework
The library ships prebuilt stylesheets. Import them from the application entry point or layout that your framework permits to load global CSS. A server that imports JavaScript without a CSS loader cannot load a stylesheet import directly.
Components that use browser APIs, layout measurements, or effects belong in a client boundary when your framework distinguishes server and client components.
## Initialize the theme
Theme stylesheets define their tokens under `.light` and `.dark`. Apply the initial class in the server-rendered document, then initialize `ThemeProvider` with the same scheme on the client:
```html
<html class="light"></html>
```
This makes the palette available before hydration. Decide the initial scheme from information available to both renders, such as a saved preference passed from the server.
## Keep browser measurements on the client
`ThemeProvider` resolves values using computed styles in the browser. During server rendering, or without computed-style support, theme values can remain CSS variable references. Do not assume that a numeric token returned during server rendering is already a JavaScript number.
Use CSS variable references for styles and defer calculations that need resolved dimensions until the client has loaded the theme. Test hydration with the same components, stylesheets, and initial scheme that you deploy.
The [front component renderer](/developers/extend/apps/layout/front-components#current-limitations) is a separate sandbox with its own DOM limitations; successful browser rendering does not establish sandbox support.

View File

@@ -0,0 +1,80 @@
---
title: Theming
description: Use Twenty UI theme tokens and scope theme overrides to part of your application.
---
Twenty UI components read public CSS custom properties prefixed with `--t-`. Prefer semantic tokens such as `background.secondary` and `font.color.primary` when styling your own components; these values adapt to the active palette.
## Use tokens in styles
`themeCssVariables` maps token paths to CSS variable references. Use it for values that CSS resolves:
```tsx
import { themeCssVariables } from 'twenty-ui/theme-constants';
export const Summary = () => (
<section
style={{
background: themeCssVariables.background.secondary,
color: themeCssVariables.font.color.primary,
padding: themeCssVariables.spacing[4],
borderRadius: themeCssVariables.border.radius.md,
}}
>
Three tasks are due today.
</section>
);
```
The equivalent CSS uses variables directly:
```css
.summary {
background: var(--t-background-secondary);
color: var(--t-font-color-primary);
padding: var(--t-spacing-4);
border-radius: var(--t-border-radius-md);
}
```
## Read resolved values
`useTheme()` reads the active provider's computed tokens. Use it when a component expects a JavaScript number, such as an icon size. The provider needs the theme stylesheet to be loaded in the browser to resolve CSS variables.
```tsx
import { IconCheck } from 'twenty-ui/icon';
import { useTheme } from 'twenty-ui/theme-constants';
export const CompleteIcon = () => {
const theme = useTheme();
return <IconCheck size={theme.icon.size.md} aria-hidden />;
};
```
Every entry in `themeCssVariables` is a CSS reference string, including numeric tokens. Do not perform arithmetic on those strings. On the server, resolved browser values are unavailable; see [Server rendering](/ui/ssr).
## Scope an override
Pass `applyToRoot={false}` to apply a theme to a subtree. Override public CSS variables by their full name:
```tsx
import { type ReactNode } from 'react';
import { ThemeProvider } from 'twenty-ui/theme-constants';
export const EmbeddedPanel = ({ children }: { children: ReactNode }) => (
<ThemeProvider
colorScheme="light"
applyToRoot={false}
overrides={{ '--t-background-secondary': '#f4f6ff' }}
>
{children}
</ThemeProvider>
);
```
A scoped provider adds a wrapper with `display: contents`; it does not create a layout box. It also supplies a container for components that support themed portals. Portals created by other libraries need their own container configuration.
Variables prefixed with `--tw-` are private component details. Use public props and `--t-` tokens instead of depending on those variables.
See the [token reference](/ui/tokens) for the available names and palette values.

View File

@@ -0,0 +1,15 @@
---
title: Design tokens
description: Browse the generated CSS variables and light and dark values used by Twenty UI.
---
import TokenReference from '/snippets/ui/generated/tokens.mdx';
This reference is generated from the same token definitions as Twenty UI's theme stylesheets. Token paths correspond to entries in `themeCssVariables` and `useTheme()`; CSS variable names can be used directly in stylesheets.
- Prefer semantic tokens such as `background.primary` and `font.color.primary` for application surfaces and text.
- Numeric tokens are unitless in CSS. Multiply them by a length when needed, for example `calc(var(--t-icon-size-md) * 1px)`. Under a mounted `ThemeProvider` with the theme stylesheet loaded, `useTheme()` resolves them to JavaScript numbers; without a provider it returns the CSS variable references.
- Values containing `var(...)` or `calc(...)` are CSS expressions, not precomputed measurements.
- The values below are the base stylesheet values. Theme overrides and supported squircle styling can change computed values. Squircle styling doubles selected radius tokens; the explicit round and pill tokens retain their base radii.
<TokenReference />