for AI agents

Building with twenty × oapps from an LLM

Canonical instruction set for any AI assistant writing code against @8maverik8/twenty-design, @8maverik8/twenty-tokens, @8maverik8/twenty-icons, and the /twenty-ui bridge to twenty-sdk/ui. Read end-to-end before touching a consumer repo.

the most important rule

1. Two import paths — pick by runtime

Twenty CRM extension apps render UI inside a Remote DOM bridge (a Web Worker proxies the components to the host). The bridge strips all data-* attributes except data-testid — so any CSS rule that looks like .foo[data-state="open"]never matches in production. Our Radix-based primitives rely on those rules. Twenty's own components don't.

If you are inside a Twenty front-component, use the bridge import
@8maverik8/twenty-design/twenty-ui re-exports twenty-sdk/ui(Twenty's own Linaria-extracted design system, verified inside front-component'ы by Twenty's UILibraries Storybook stories). Same API as ours for the overlapping ~19 primitives (Button, Tag, Avatar, Status, Modal, etc.) — just a different import path.
tsx
// ✓ inside a Twenty front-component:
import { Button, Tag, ThemeProvider } from '@8maverik8/twenty-design/twenty-ui';
// ✓ inside a standalone Next.js / Vite app:
import { Button, Tag } from '@8maverik8/twenty-design';

Each component doc page shows its runtime tag in the eyebrow — pick the right source by it:

runtime: normalRadix-based. Works in Normal DOM (browser) only.
runtime: bothPure inline-styles + useState. Works everywhere (recipes).
runtime: remote (alias)Has a `bridgeAlias` — import from /twenty-ui inside a front-component.
for front-component content

2. Recipes — composed in-panel blocks

Seven blocks under @8maverik8/twenty-design built specifically for the right-panel content area inside the CRM. They use inline-styles + useState-based hover — no Radix, no data-* — so the same code renders identically in this docs preview and inside a Twenty front-component.

Section title + right-side actions→ PanelHeader
Subtotal / total recap row→ SummaryRow
Compact preset-value chips→ PresetRow
2-4 mutually exclusive segments→ SegmentedControl
Discount editor (% / $ / off)→ DiscountController
One row of a priced list (with delete)→ LineItemCard
Subtotal + discount + total footer→ TotalsBlock
never break these

3. Hard rules — non-negotiable

Never recreate a component that already exists in this library.Read /components first. If a slug matches what you need, import from @8maverik8/twenty-design (or its /twenty-uisubpath for front-component content). Even a "small wrapper" duplicates work and forks the look.
Never recreate components that twenty-sdk/ui already ships.Button, IconButton, Avatar, Tag, Status, Modal, Tooltip, Toggle, Checkbox, Radio, SearchInput, Loader, ProgressBar, Card, Callout, Banner, JsonVisualizer, HorizontalSeparator, Label — these are Twenty's own. Import from @8maverik8/twenty-design/twenty-ui in front-component code, or use our Radix-based equivalent in Normal DOM.
Never inline hex colors, pixel sizes, or font sizes.Every value resolves through a --t-* token. If the token you need does not exist, propose adding it to @8maverik8/twenty-tokens — do not paper over with a literal.
Hover/active states INSIDE a Twenty front-component go through useState + onMouseEnter/onMouseLeave.Twenty Remote DOM allows mouseenter/mouseleave events but strips data-*; CSS :hover works only if the rule is purely class-based and the CSS got injected via a <style> tag at the top of the front-component. The recipes in this library show the pattern. In Normal DOM code (docs, standalone apps) the Radix-based primitives use data-* selectors in components.css— that's fine, just don't mix the two.
Never bypass Field for a labelled input.Field wires htmlFor, aria-invalid, hint and error semantics. Skipping it breaks a11y.
Always read the component's doc page before using it for the first time.The eyebrow tells you the runtime. The bridge banner (when present) tells you to switch the import in front-component code. Every page documents the variant matrix, anatomy, do/don't rationale, and live examples.
Always prefer composition over fork.Need a "Pricing Picker"? Build it from SegmentedControl + NumberInput + Field. Don't write a one-off pricing component.
decision tree

4. Component selection — pick once, pick right

Walk the tree top-down. We only list components that exist in this library — we do not ship Tabs, Kanban, TreeView, ResizablePanels, Breadcrumbs (host CRM owns those layout surfaces).

User picks one of ≤ 6 fixed options→ RadioGroup, Select, or SegmentedControl
User picks one of many options they know by name→ Combobox (single) or Select (≤ 7 items)
User picks many→ MultiSelect
User toggles a single boolean that applies right away→ Switch
User toggles a boolean that waits for Save→ Checkbox
User enters a short string→ Input wrapped in Field
User enters multi-line text→ Textarea
User enters a number with bounds→ NumberInput
User filters a visible list→ SearchInput at the top
User opens "go to anything" by keyboard→ CommandPalette (⌘K)
Show one of many actions on a click→ DropdownMenu
Show one of many actions on right-click→ ContextMenu
Hover-revealed label on an icon button→ Tooltip
Hover-revealed rich preview of a record→ HoverCard
Click-revealed mini-surface (form, list, popover)→ Popover
Full-attention workflow with backdrop→ Modal
Confirm an irreversible action→ ConfirmDialog with confirmAccent="danger"
Transient confirmation of a background save→ Toast
Surface-level dismissible notice→ Banner
Inline hint in content→ Callout
Tabular data, sortable + selectable→ Table
Tabular data needing search + pagination→ DataTable
Tabular data > 1k rows→ VirtualList or server-paginated DataTable
Chronological events on one record→ Timeline
Source code or JSON payload for inspection→ CodeBlock or JsonViewer
Section header inside a panel→ PanelHeader (recipe)
Priced list row with delete→ LineItemCard (recipe)
Subtotal / discount / total recap→ TotalsBlock (recipe)
where the code runs

5. Surfaces

Inside a Twenty front-component (Remote DOM)
Importsfrom '@8maverik8/twenty-design/twenty-ui' // overlapping primitives from '@8maverik8/twenty-design' // recipes (runtime: both)
ConstraintsBridge strips all data-* except data-testid; <button>/<input>/etc. are whitelisted; events: click + keyboard + mouseenter/leave only (NO :hover via inline). useState-based hover in recipes is the documented pattern.
Standalone Next.js / Vite app (Normal DOM)
Importsfrom '@8maverik8/twenty-design' // anything goes
ConstraintsFull browser. Wrap children with <TooltipProvider> and <ToastProvider>. Toggle .light / .dark on <html> to switch themes. Radix-based primitives use data-state selectors which work natively here.
--t-*

6. Tokens — the contract

Every visual decision in the system collapses to a token. The TypeScript dictionary is the source of truth — read it from your editor:

ts
import { themeCssVariables as t } from '@8maverik8/twenty-tokens';
// Spacing
t.spacing[3] // "var(--t-spacing-3)"
t.spacing['1.5'] // "var(--t-spacing-1_5)"
// Colors — 30 Radix scales × 12 steps
t.color.blue // primary blue ramp
t.color.blue10 // step 10 (hover)
t.color.transparent.blue4 // transparent overlay
// Semantic
t.background.primary // page surface
t.font.color.danger // error text
t.border.color.medium // default border
// Typography
t.font.size.md
t.font.weight.semiBold
// Radii / shadows / blur
t.border.radius.sm
t.boxShadow.strong
Token discovery
If you cannot find a token for the value you need, you are probably about to invent inconsistency. Check /foundations/colors, /foundations/spacing, etc. before writing a literal — and propose adding the token if it is genuinely new.
for human reviewers

7. When you write code, link the docs

When you generate non-trivial code involving a component, include a comment with the doc URL above the import. Reviewers (and future agents) will follow it before changing behaviour.

tsx
// https://twenty.design.oapps.io/components/data-table
// https://twenty.design.oapps.io/components/field
import { DataTable, Field, Input } from '@8maverik8/twenty-design';
// For panel content inside a Twenty front-component:
// https://twenty.design.oapps.io/twenty-ui
// https://twenty.design.oapps.io/components/line-item-card
import { Button, ThemeProvider } from '@8maverik8/twenty-design/twenty-ui';
import { LineItemCard, TotalsBlock } from '@8maverik8/twenty-design';
what's where

8. Full catalogue — by runtime

runtime: both11 components

PanelHeader (panel-header), SegmentedControl (segmented-control), PresetRow (preset-row), DiscountController (discount-controller), MoneyInput (money-input), RecordPicker (record-picker), InputShell (input-shell), SummaryRow (summary-row), LineItemCard (line-item-card), TotalsBlock (totals-block), EntityChip (entity-chip)

runtime: normal52 components

Box (box), Stack (stack), Text (text), Button (button), IconButton (icon-button), ButtonGroup · ToggleGroup (button-group), Input (input), Textarea (textarea), NumberInput (number-input), Label (label), Field (field), Checkbox (checkbox), Switch (switch), Radio · RadioGroup (radio), Select (select), SearchInput (search-input), Combobox (combobox), MultiSelect (multi-select), Slider (slider), Rating (rating), DatePicker · TimePicker · DateRangePicker (date-picker), ColorPicker (color-picker), FormSection · FormRow · FormGroup (form-composer), Tooltip (tooltip), Popover (popover), Modal · ConfirmDialog (modal), DropdownMenu (dropdown-menu), ContextMenu (context-menu), HoverCard (hover-card), Toast (toast), Tag (tag), Avatar · AvatarGroup · Chip (avatar), Status (status), Badge (badge), Table (table), DataTable (data-table), Timeline (timeline), VirtualList (virtual-list), CodeBlock (code-block), JsonViewer (json-viewer), Card (card), Accordion (accordion), Divider (divider), EmptyState (empty-state), Skeleton (skeleton), Spinner (spinner), Progress (progress), ScrollArea (scroll-area), Pagination (pagination), Stepper (stepper), CommandPalette · KeyboardShortcut (command-palette), Banner · Callout (banner)

common mistakes

9. Anti-patterns we keep catching

Importing our Radix-based Button inside a front-component.The CSS rules use [data-variant="primary"] — Twenty Remote DOM strips the attribute. Use @8maverik8/twenty-design/twenty-ui instead. Bridge banner on the doc page will say so.
Stacking three primary-blue buttons next to each other.One filled blue per surface. Demote the rest to secondary or tertiary.
Using `accent="danger"` on a confirm dialog cancel button.Cancel is the safe path; only the destructive action takes a danger accent.
Wrapping an Input in your own labelled div.Use Field. It already handles label, hint, error, htmlFor.
Building a quick-edit popover with a bare div + absolute positioning.Use Popoverin Normal DOM. In Remote DOM, render inline (overlays via portals don't cross the bridge cleanly).
Putting a tooltip on an interactive thing the user needs to click to access.Tooltips disappear on touch and during keyboard nav. Visible text on the trigger or a hover-card.
Using a Tag for an action.Tags read as values. For an action use Button, IconButton, or Chip with onRemove.
Rendering 500 rows directly through <Table>.Use DataTable (paginates) or VirtualList. Past ~50 rows DOM thrash starts to show; past 200 it is noticeable; past 1000 the page jitters.
extension path

10. Adding a new component

If the decision tree leads nowhere AND twenty-sdk/ui doesn't have it, you need a new component:

  1. Confirm there is no existing component (check the catalogue + the bridge surface at /twenty-ui).
  2. Decide the runtime. If it must work in a front-component, write it like a recipe (inline-styles + useState, no Radix, no data-*). Put it in packages/design/src/recipes/<Name>.tsx.
  3. If Normal-DOM-only is fine, follow the existing Radix-based pattern in packages/design/src/components/<Name>.tsx. Interactive states in packages/tokens/src/components.css.
  4. Re-export from packages/design/src/index.ts.
  5. Write the doc spec in apps/docs/src/content/components/<slug>.tsx — extend ComponentDoc from @/lib/registry. Set runtime and (if relevant) bridgeAlias.
  6. Add to componentDocs in src/lib/registry.ts and to the right sidebar section in src/lib/nav.ts.
  7. Live-verify: pnpm --filter docs dev, open /components/<slug>.
  8. Run pnpm typecheck at the root before opening a PR.
checklist

11. Self-check before you commit

I used the existing component (in @8maverik8/twenty-design or twenty-sdk/ui) instead of recreating it.
For front-component code I imported from @8maverik8/twenty-design/twenty-ui where a bridge alias exists.
Every colour / spacing / font value resolves to a --t-* token.
Every input is wrapped in Field when it has a label.
In Normal DOM, overlay providers (TooltipProvider, ToastProvider) sit in the app root.
In Remote DOM, hover/active states are useState-driven, not data-* CSS.
I linked the doc URL in a comment above non-trivial imports.
I ran typecheck and the dev server before declaring done.