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.
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.
@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.Each component doc page shows its runtime tag in the eyebrow — pick the right source by it:
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.
3. Hard rules — non-negotiable
/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.@8maverik8/twenty-design/twenty-ui in front-component code, or use our Radix-based equivalent in Normal DOM.--t-* token. If the token you need does not exist, propose adding it to @8maverik8/twenty-tokens — do not paper over with a literal.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.Field wires htmlFor, aria-invalid, hint and error semantics. Skipping it breaks a11y.SegmentedControl + NumberInput + Field. Don't write a one-off pricing component.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).
5. Surfaces
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:
/foundations/colors, /foundations/spacing, etc. before writing a literal — and propose adding the token if it is genuinely new.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.
8. Full catalogue — by runtime
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)
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)
9. Anti-patterns we keep catching
[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.secondary or tertiary.Field. It already handles label, hint, error, htmlFor.Popoverin Normal DOM. In Remote DOM, render inline (overlays via portals don't cross the bridge cleanly).Button, IconButton, or Chip with onRemove.DataTable (paginates) or VirtualList. Past ~50 rows DOM thrash starts to show; past 200 it is noticeable; past 1000 the page jitters.10. Adding a new component
If the decision tree leads nowhere AND twenty-sdk/ui doesn't have it, you need a new component:
- Confirm there is no existing component (check the catalogue + the bridge surface at
/twenty-ui). - 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 inpackages/design/src/recipes/<Name>.tsx. - If Normal-DOM-only is fine, follow the existing Radix-based pattern in
packages/design/src/components/<Name>.tsx. Interactive states inpackages/tokens/src/components.css. - Re-export from
packages/design/src/index.ts. - Write the doc spec in
apps/docs/src/content/components/<slug>.tsx— extendComponentDocfrom@/lib/registry. Setruntimeand (if relevant)bridgeAlias. - Add to
componentDocsinsrc/lib/registry.tsand to the right sidebar section insrc/lib/nav.ts. - Live-verify:
pnpm --filter docs dev, open/components/<slug>. - Run
pnpm typecheckat the root before opening a PR.
11. Self-check before you commit
@8maverik8/twenty-design or twenty-sdk/ui) instead of recreating it.@8maverik8/twenty-design/twenty-ui where a bridge alias exists.--t-* token.Field when it has a label.useState-driven, not data-* CSS.