@martmull v2.0 ;) --------- Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
18 KiB
When to Use
Use this when the user wants to design or polish a Twenty front component UI.
Examples:
- Make a front component look better.
- Create a clean layout for a front component.
- Improve loading, empty, error, or disabled states.
- Make a front component responsive and easier to scan.
Boundaries
Do not scaffold a new app here. Use create-app first when the app does not exist.
Do not use this reference for source files, registration, runtime imports, data access, CLI commands, or browser verification.
Design Rules
Design front components as compact CRM product UI, not marketing pages. The goal is a surface that scans quickly, feels native inside Twenty, and keeps actions predictable.
Use Figma measurements as evidence for the current Twenty rhythm, not as implementation literals. Source code should use Twenty primitives, component props, and themeCssVariables instead of hardcoded visual values.
Twenty UI Defaults
Prefer Twenty UI primitives for CRM-native front component UI:
- Use
H2TitleorH3Titlefor compact section headings. - Use
Calloutwith a matching icon for loading, empty, error, and blocked states. - Use
Buttonfor primary and secondary actions. - Use
Tag,Status,Chip,Label, andAvatarfor metadata, state, people, and small summaries. - Use
themeCssVariablesfor spacing, colors, border radius, typography, borders, icon sizing, shadows, and backgrounds.
Use local inline styles for layout containers and custom data displays, but keep them aligned with Twenty tokens. Use front-components.md for exact imports and runtime rules.
Use Twenty UI icons before custom SVG. Use themeCssVariables.icon.size.md for default row and action icons, themeCssVariables.icon.size.sm for quiet chevrons, and themeCssVariables.spacing[6] for icon-only action targets.
Token-First Implementation
When turning a Figma observation into code, translate the visual target into a token:
- Typography uses
themeCssVariables.font.size.*,themeCssVariables.font.weight.*, andthemeCssVariables.text.lineHeight.*. - Spacing uses
themeCssVariables.spacing[...]andthemeCssVariables.betweenSiblingsGap. - Radius uses
themeCssVariables.border.radius.smfor compact controls andthemeCssVariables.border.radius.mdfor cards or panels. - Colors use
themeCssVariables.font.color.*,themeCssVariables.background.*,themeCssVariables.border.color.*, or component props. - Icons use
themeCssVariables.icon.size.*or native Twenty UI icon sizing. - Borders use
themeCssVariables.border.color.*; avoid raw border colors. - Shadows use
themeCssVariables.boxShadow.*; avoid custom shadow stacks unless mirroring an existing Twenty overlay exactly.
Do not create source constants named after raw visual measurements. Use semantic names that point to tokens:
const compactControlHeight = themeCssVariables.spacing[6];
const denseRowHeight = themeCssVariables.spacing[8];
const panelRadius = themeCssVariables.border.radius.md;
Avoid source constants named after raw measurements, such as rowHeight or cardRadius, when they hide a token choice. Prefer semantic names like denseRowHeight and panelRadius.
Base Scale
Use the same small scale across the whole component:
- Body text:
themeCssVariables.font.size.md, regular weight, medium line height. - Section titles and row titles:
themeCssVariables.font.size.md, medium weight, medium line height. - Descriptions and helper text:
themeCssVariables.font.size.sm, regular weight, medium line height. - Metadata labels and table headers: medium body text with
themeCssVariables.font.color.tertiary. - Compact controls: use Twenty
Buttonsizing orthemeCssVariables.spacing[6]for custom control height. - Navigation and menu rows: use the native component when available; otherwise use the nearest theme spacing token that matches the host row rhythm.
- Dense table, list, and input-like rows: use
themeCssVariables.spacing[8]for custom row height. - Toolbars and view bars: use
themeCssVariables.spacing[10]for custom toolbar height.
Do not increase type size to create hierarchy inside compact widgets. Use weight, color, alignment, spacing, and order first.
Applied example:
const titleStyle = {
fontSize: themeCssVariables.font.size.md,
fontWeight: themeCssVariables.font.weight.medium,
lineHeight: themeCssVariables.text.lineHeight.md,
letterSpacing: 0,
color: themeCssVariables.font.color.primary,
};
const descriptionStyle = {
fontSize: themeCssVariables.font.size.sm,
fontWeight: themeCssVariables.font.weight.regular,
lineHeight: themeCssVariables.text.lineHeight.md,
letterSpacing: 0,
color: themeCssVariables.font.color.secondary,
};
Radius And Shape
Use a small, consistent radius system:
- Cards, popovers, notification containers, and framed panels use
themeCssVariables.border.radius.md. - Buttons, chips, toolbar controls, menu hover fills, skeletons, and small fields use
themeCssVariables.border.radius.sm. - Checkbox shape and target sizing should come from the native Twenty checkbox pattern when available.
- Do not use pill or rounded radius for cards, rows, or toolbar buttons unless the existing Twenty primitive already does.
- Do not mix several custom radii in one component. Pick the matching Twenty radius token for each element role.
Applied example:
const cardStyle = {
borderRadius: themeCssVariables.border.radius.md,
padding: themeCssVariables.spacing[2],
background: themeCssVariables.background.primary,
};
const toolbarButtonStyle = {
minHeight: themeCssVariables.spacing[6],
padding: `0 ${themeCssVariables.spacing[2]}`,
borderRadius: themeCssVariables.border.radius.sm,
};
Spacing
Use the theme spacing scale instead of raw dimensions:
themeCssVariables.betweenSiblingsGap: tightly grouped toolbar actions or small separators.themeCssVariables.spacing[1]: icon-to-label gaps, chip internals, compact row gaps.themeCssVariables.spacing[2]: default card padding, row horizontal padding, toolbar padding, title-row gaps.themeCssVariables.spacing[4]: section gaps inside larger panels or between unrelated groups.themeCssVariables.spacing[6]: left indentation for description text that belongs to a row with a leading icon and label.
Do not jump to large padding inside ordinary CRM controls. If a compact widget starts needing generous whitespace, the layout probably needs fewer sections or better grouping.
Applied example:
const titleRowStyle = {
display: 'flex',
alignItems: 'center',
gap: themeCssVariables.spacing[2],
minHeight: themeCssVariables.spacing[6],
};
const descriptionRowStyle = {
paddingLeft: themeCssVariables.spacing[6],
paddingBottom: themeCssVariables.spacing[2],
};
Information Hierarchy
Make hierarchy visible without making the component loud:
- Put the record, object, or task name first. Use medium body text with primary color.
- Put the explanation second. Use small regular text with secondary color.
- Put metadata third. Use
Tag,Status,Chip,Label,Avatar, or tertiary text. - Put counters and quiet totals in light text, not primary text.
- Use one primary action per small surface. Put secondary actions beside it or behind a menu.
- Use color for state and meaning, not decoration.
- Keep descriptions aligned under the title text, not under the leading icon.
- In a card with a leading icon, use a medium icon, a standard spacing gap, then text. The description starts at the same x-position as the title text.
- In a list or table, reserve the left edge for the most important identifier and keep it stable across every row.
Good hierarchy:
<div style={{ display: 'grid', gap: themeCssVariables.spacing[1] }}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: themeCssVariables.spacing[2],
}}
>
<Avatar name={ownerName} />
<span style={titleStyle}>{companyName}</span>
<Status color="green" text="Active" />
</div>
<span style={descriptionStyle}>Renewal due in 12 days</span>
</div>
Avoid hierarchy that relies on oversized type, all caps, bright backgrounds, or extra padding. That breaks the compact CRM rhythm and makes dense data harder to scan.
Alignment
Twenty UI looks precise because repeated edges line up:
- Align every custom surface to the theme spacing grid.
- Use
themeCssVariables.spacing[2]horizontal padding for compact rows and cards. - Use
themeCssVariables.spacing[6]for icon-only action targets so icons share a stable center. - Align row icons, avatars, labels, and actions vertically center.
- Keep list text left-aligned. Do not center-align row content.
- Keep toolbar context on the left and actions on the right.
- Keep repeated rows the same x-position, width, height, and action position.
- In menus, use a small inset from the menu edge and keep rows full width inside that inset.
- In tables, use the table theme tokens when available, especially
themeCssVariables.table.horizontalCellPadding. - Do not let a long value push action buttons out of alignment. Truncate text before actions move.
Toolbar pattern:
const toolbarStyle = {
minHeight: themeCssVariables.spacing[10],
padding: themeCssVariables.spacing[2],
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: themeCssVariables.spacing[2],
};
const toolbarActionsStyle = {
display: 'flex',
alignItems: 'center',
gap: themeCssVariables.betweenSiblingsGap,
};
Table/list row pattern:
const rowStyle = {
minHeight: themeCssVariables.spacing[8],
padding: `0 ${themeCssVariables.table.horizontalCellPadding}`,
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr) auto',
alignItems: 'center',
columnGap: themeCssVariables.spacing[2],
};
const cellLabelStyle = {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
Consistency
Keep visual decisions repeatable:
- Use the same row height token for every row in the same list.
- Use the same action placement in loading, empty, error, and ready states.
- Use the same radius token for the same kind of element everywhere in the component.
- Use the same icon size token for the same role.
- Use the same label for the same action everywhere. Do not mix "Open", "View", and "See record" for one action.
- Use the same empty-state structure across related components: icon, short title, one sentence, one action.
- Use the same loading skeleton geometry as the final content. Do not replace compact rows with centered spinners unless the whole panel is blocked.
- Use Twenty tokens for color and spacing. Use raw values only for semantic data visualization that cannot be represented by existing tokens.
State backgrounds should follow the existing Twenty pattern:
- Default: transparent or
themeCssVariables.background.primary. - Hover:
themeCssVariables.background.transparent.lightorthemeCssVariables.background.secondary. - Pressed/clicked:
themeCssVariables.background.tertiary. - Selected: same shape and height as the default row, with selected color treatment from the nearest Twenty primitive.
- Disabled: same layout, lower contrast, no new decorative color.
- Loading: same dimensions as ready content, with skeleton blocks using the compact radius token.
Cards And Panels
Use cards only for individual repeated items, notifications, popovers, or genuinely framed tools. Do not put cards inside cards.
Card defaults:
- Radius:
themeCssVariables.border.radius.md. - Padding:
themeCssVariables.spacing[2]for compact cards. - Title-to-body gap:
themeCssVariables.spacing[2]only when the body is visually separate. - Divider:
themeCssVariables.border.color.lightwhen sections need separation. - Shadow: prefer
themeCssVariables.boxShadow.lightor the native host surface treatment. Do not combine a strong border with a strong shadow.
Overlay feedback pattern:
const overlayFeedbackStyle = {
borderRadius: themeCssVariables.border.radius.md,
padding: `${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[1]}`,
display: 'flex',
flexDirection: 'column',
gap: themeCssVariables.spacing[1],
background: themeCssVariables.background.overlayPrimary,
backdropFilter: `blur(${themeCssVariables.blur.medium})`,
boxShadow: themeCssVariables.boxShadow.strong,
};
Use overlay styling for small feedback surfaces only. Normal embedded widgets should usually use the host background, a subtle border, or no frame.
Buttons And Actions
Use actions that feel like Twenty toolbar controls:
- Use
Buttonfor actual commands. - Use Twenty button
sizeandvariantprops before custom button styling. - For custom compact controls, use
themeCssVariables.spacing[6]as the control height token. - Use
themeCssVariables.spacing[2]for horizontal padding. - Use
themeCssVariables.spacing[1]for icon-label gaps. - Use
themeCssVariables.border.radius.smfor custom compact action radius. - Use medium body text for labels.
- Use medium icons for primary actions and small icons for quiet chevrons.
- Use
themeCssVariables.spacing[6]for icon-only action targets.
Applied example:
<div
style={{
display: 'flex',
alignItems: 'center',
gap: themeCssVariables.betweenSiblingsGap,
}}
>
<Button variant="secondary" size="small" title="Open record" />
<Button variant="secondary" size="small" title="Add note" />
</div>
Tables And Dense Lists
Use table-like rhythm for CRM data:
- Header and row height: use
themeCssVariables.spacing[8]for custom dense rows. - Header text: medium body text with tertiary color.
- Cell text: regular body text with primary or secondary color.
- Cell padding: use
themeCssVariables.table.horizontalCellPadding. - Icon-label gap: use
themeCssVariables.spacing[1]. - Checkbox column: use
themeCssVariables.table.checkboxColumnWidthwhen building table-like custom rows. - First data column: make it the flexible
minmax(0, 1fr)column so names get priority and actions stay aligned. - Truncate long names with ellipsis. Do not wrap names in dense rows unless the row is intentionally two-line.
Applied example:
const headerCellStyle = {
minHeight: themeCssVariables.spacing[8],
padding: `0 ${themeCssVariables.table.horizontalCellPadding}`,
display: 'flex',
alignItems: 'center',
gap: themeCssVariables.spacing[1],
fontSize: themeCssVariables.font.size.md,
fontWeight: themeCssVariables.font.weight.medium,
color: themeCssVariables.font.color.tertiary,
};
Empty, Loading, Error, Disabled, Success
Design the visible states before polishing the ready state:
- Loading: preserve final layout dimensions with skeletons that use the same row, text, and radius tokens as the ready content.
- Empty: use
Calloutor a compact empty block with one short title, one sentence, and one clear action. - Error: use
Calloutwith an error icon, explain what failed, and offer retry or recovery. - Disabled: keep the same control size and placement. Lower contrast and explain the blocking condition if it is not obvious.
- Success: use a small status, toast-style card, or inline confirmation. Do not redesign the whole panel for success.
Loading example:
const skeletonStyle = {
height: themeCssVariables.spacing[4],
width: '60%',
borderRadius: themeCssVariables.border.radius.sm,
background: themeCssVariables.background.transparent.light,
};
Empty example:
<Callout
variant="secondary"
title="No activity yet"
description="Activity will appear here once this record has timeline events."
/>
Text And Copy
Make text fit in real Twenty widths:
- Use sentence case.
- Use short labels.
- Use short button text.
- Use one-sentence descriptions.
- Put dynamic values in stable containers with ellipsis.
- Never let a record name, email, URL, or amount overlap an action.
- Prefer exact object names over generic labels. Use "Open company", not "Open item", when the object is known.
- Keep units and counts close to the value:
12 opportunities,$4,200,3 tasks.
Text truncation pattern:
const truncateStyle = {
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
Responsive Rules
Front components often render in narrow widgets:
- Design compact widths first.
- Use a single column by default.
- Move secondary actions into a menu before wrapping the whole toolbar.
- Keep the primary action visible when space is tight.
- Let content columns shrink with
minmax(0, 1fr). - Use ellipsis for long identifiers instead of horizontal scroll in small widgets.
- Do not use hero-scale text, marketing sections, or wide decorative imagery in embedded CRM widgets.
Narrow layout pattern:
const compactGridStyle = {
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr) auto',
alignItems: 'center',
gap: themeCssVariables.spacing[2],
};
Quick Checks
Before considering a UI done, check this list:
- The component uses Twenty typography tokens, not custom text sizes.
- Cards use the medium radius token or a smaller existing primitive radius.
- Buttons and action controls use Twenty
Buttonprops or compact spacing tokens. - Table/list rows use a consistent row-height token.
- Icons use
themeCssVariables.icon.size.*or native Twenty icon sizing. - Spacing uses
themeCssVariables.spacing[...]andthemeCssVariables.betweenSiblingsGap. - Ready, loading, empty, error, and disabled states share the same structure.
- Important text has a stable left edge and truncates before it overlaps actions.
- Repeated rows have identical height, padding, icon size, action placement, and state behavior.
- The UI uses Twenty primitives and
themeCssVariablesbefore custom styling.
Workflow
First, identify the target front component file and the user workflow it supports.
Design the visible states the user will naturally encounter: loading, empty, error, disabled, success, and the primary working state.
Then apply the rules in this order:
- Set the information hierarchy: title, description, metadata, actions.
- Lock the structural dimensions with Twenty primitives and
themeCssVariables. - Align repeated edges and action positions.
- Apply Twenty primitives and tokens.
- Add hover, pressed, selected, loading, disabled, empty, error, and success states.
- Test with long names, empty values, narrow widths, and multiple records.