packages/twenty-codex-plugin/references/design/front-component-ui.md
Use this when the user wants to design or polish a Twenty front component UI.
Examples:
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 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.
Prefer Twenty UI primitives for CRM-native front component UI:
H2Title or H3Title for compact section headings.Callout with a matching icon for loading, empty, error, and blocked states.Button for primary and secondary actions.Tag, Status, Chip, Label, and Avatar for metadata, state, people, and small summaries.themeCssVariables for 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.
When turning a Figma observation into code, translate the visual target into a token:
themeCssVariables.font.size.*, themeCssVariables.font.weight.*, and themeCssVariables.text.lineHeight.*.themeCssVariables.spacing[...] and themeCssVariables.betweenSiblingsGap.themeCssVariables.border.radius.sm for compact controls and themeCssVariables.border.radius.md for cards or panels.themeCssVariables.font.color.*, themeCssVariables.background.*, themeCssVariables.border.color.*, or component props.themeCssVariables.icon.size.* or native Twenty UI icon sizing.themeCssVariables.border.color.*; avoid raw border colors.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.
Use the same small scale across the whole component:
themeCssVariables.font.size.md, regular weight, medium line height.themeCssVariables.font.size.md, medium weight, medium line height.themeCssVariables.font.size.sm, regular weight, medium line height.themeCssVariables.font.color.tertiary.Button sizing or themeCssVariables.spacing[6] for custom control height.themeCssVariables.spacing[8] for custom row height.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,
};
Use a small, consistent radius system:
themeCssVariables.border.radius.md.themeCssVariables.border.radius.sm.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,
};
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],
};
Make hierarchy visible without making the component loud:
Tag, Status, Chip, Label, Avatar, or tertiary text.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.
Twenty UI looks precise because repeated edges line up:
themeCssVariables.spacing[2] horizontal padding for compact rows and cards.themeCssVariables.spacing[6] for icon-only action targets so icons share a stable center.themeCssVariables.table.horizontalCellPadding.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',
};
Keep visual decisions repeatable:
State backgrounds should follow the existing Twenty pattern:
themeCssVariables.background.primary.themeCssVariables.background.transparent.light or themeCssVariables.background.secondary.themeCssVariables.background.tertiary.Use cards only for individual repeated items, notifications, popovers, or genuinely framed tools. Do not put cards inside cards.
Card defaults:
themeCssVariables.border.radius.md.themeCssVariables.spacing[2] for compact cards.themeCssVariables.spacing[2] only when the body is visually separate.themeCssVariables.border.color.light when sections need separation.themeCssVariables.boxShadow.light or 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.
Use actions that feel like Twenty toolbar controls:
Button for actual commands.size and variant props before custom button styling.themeCssVariables.spacing[6] as the control height token.themeCssVariables.spacing[2] for horizontal padding.themeCssVariables.spacing[1] for icon-label gaps.themeCssVariables.border.radius.sm for custom compact action radius.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>
Use table-like rhythm for CRM data:
themeCssVariables.spacing[8] for custom dense rows.themeCssVariables.table.horizontalCellPadding.themeCssVariables.spacing[1].themeCssVariables.table.checkboxColumnWidth when building table-like custom rows.minmax(0, 1fr) column so names get priority and actions stay aligned.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,
};
Design the visible states before polishing the ready state:
Callout or a compact empty block with one short title, one sentence, and one clear action.Callout with an error icon, explain what failed, and offer retry or recovery.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."
/>
Make text fit in real Twenty widths:
12 opportunities, $4,200, 3 tasks.Text truncation pattern:
const truncateStyle = {
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
Front components often render in narrow widgets:
minmax(0, 1fr).Narrow layout pattern:
const compactGridStyle = {
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr) auto',
alignItems: 'center',
gap: themeCssVariables.spacing[2],
};
Before considering a UI done, check this list:
Button props or compact spacing tokens.themeCssVariables.icon.size.* or native Twenty icon sizing.themeCssVariables.spacing[...] and themeCssVariables.betweenSiblingsGap.themeCssVariables before custom styling.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:
themeCssVariables.