Skip to content

03 - Layout

Shell, page anatomy, and named page archetypes. If you’re building a new page, this is the doc you live in.

Prereqs: 01-principles.md for the why, 02-foundations.md for the tokens.


Named page archetypes

Every page in every app is exactly one of these seven types. If you’re not sure which, pick the closest and ask in review.

ArchetypePurpose — why this shape existsWhen to usePrimary componentsReal examples
ListBrowse and manage a collection of entitiesCollection of entities (table, kanban, list view)ListPageBar + PageContent + DataTable / CardGridAgents, Tools, Review queue
DetailInspect and act on a single entity. Three shapes — see belowSingle entity — three shapes: 3a Split, 3b Tabbed, 3c Drawer (see below)DetailPageBar + PageContent + (optional RightPanel)/review/:id, /agents/:id
FormCreate, edit, or configure. Document-style reading rhythmCreate / edit / settings — flat single-column following Attio’s Settings archListPageBar + PageContent narrow + FormSection + Separator + ActionBarNew Agent form, /agents/:id Agent tab, Billing
WorkbenchThe user’s primary daily workspace. Maximum density and efficiencyUser does their primary work here all dayCustom within shell, dense layoutChat, Playground
DashboardAggregated metrics at a glance. Monitoring, not editingAggregated metrics + charts + summary tablesListPageBar + CardGrid (metrics) + flat tablesUsage Overview
CanvasVisual-first editing where spatial arrangement mattersLarge visual-first editor (graph, map, timeline)Full-viewport canvas + overlay controlsWorkflow editor
FocusMulti-step linear flow that needs full attention. Sidebar stays visibleMulti-step wizard (import, onboarding, complex create) — main-content takeover, sidebar stays visibleFocusLayout + 2-row header + Stepper + centered content + sticky bottom action barKB bulk import (future), Agent creation wizard, Onboarding

The archetype dictates the structure. Two detail pages in two different apps must feel like siblings. A workbench and a form must feel distinct. No ambiguity.

Detail sub-shapes — choosing the right one

The Detail archetype has three shapes. Choosing wrong is the most common layout mistake.

ShapeVisualWhen to useWhen NOT to useAttio example
3a SplitMain content (left, scrollable) + Property panel (right, scrollable)The entity has ongoing activity (timeline, notes, emails) AND first-class properties that benefit from a persistent panel. The main area’s job is activity feed; the right panel’s job is metadata inspector.When the “main area” would just be more form fields. If both sides are properties, you have a Form, not a Split.Company detail, People detail
3b Tabbed full-pageTabs switch full-page content. Each tab is a self-contained surfaceThe entity has many distinct concerns (settings, resources, workflow, distribution) where each tab is conceptually separate. Tab content varies wildly in shape.When you need a persistent property panel visible while browsing activity. Use 3a Split instead.(Attio uses this for Settings pages with sidebar nav)
3c DrawerList remains visible; clicking a row opens a slide-over panelThe list is the primary context and the user triages items sequentially. The preview is ephemeral — not a full record page.When the detail content is rich enough to deserve its own URL and full viewport. Use 3a Split or 3b Tabbed instead.Notifications, task inbox

Key decision rule: If your entity has properties + activity, use 3a Split. If your entity has many configuration surfaces, use 3b Tabbed. If the list is primary and detail is a glance, use 3c Drawer.

Result and Exception are templates within existing archetypes, not archetypes themselves. A “payment complete” page is a Form (in result state); a 404 is an Exception template rendered inside the current archetype. Both live under Page type templates below — not here.

For when to pick each Detail shape (Split vs Tabbed vs Drawer) see references/attio-layout-patterns.md §3. For when Form belongs over Detail (settings is not a detail-split) see references/attio-layout-decision-guide.md.

Step 0 for every new surface — card or flat?

Before you reach for any component, answer this for each section of your page:

Use a <Card> only if the element is one of:

  1. A dashboard metric / KPI tile (<StatRow> contents).
  2. A clickable entity preview in a grid (agent card, tool card).
  3. An empty-state CTA — large centered card with icon + title + button.
  4. The Danger Zone at the bottom of a settings page (red-tinted, one per page).

Use flat — no card, no border wrapper — for everything else. Especially:

  • Form field groups (use <FormSection> + <Separator>).
  • Right-panel property panels (use <PropertyList> inside <CollapsibleSection>).
  • Settings sections.
  • Tables (edge-to-edge, no wrapper div — see §Tables below).
  • Progress banners, alerts, status strips (use <Alert> or a flat role="status" div).

This is the most-violated rule in the spec. The compliance script catches <div className="rounded-lg border"><Table> but it cannot catch a <Card> that wraps a <FormSection>. Reviewer judgment required.

Full decision tree with worked nx-agent examples: references/attio-layout-decision-guide.md §4.

Responsive behavior per archetype

Admin is desktop-first. Below md (768px) most admin surfaces degrade gracefully rather than re-flowing elegantly — a stated tradeoff, not an accident. Chat is mobile-aware (the chat app is a consumer surface; expect 50%+ mobile usage).

Archetypesm (<640)md (≥768)lg (≥1024)
ListSidebar hides; table horizontal-scrolls within <PageContent>. No column collapsing (tables are dense by design).Sidebar collapses to rail; table fits without scroll if ≤6 columns.Full sidebar + table.
Detail-3a SplitRight panel hidden; toggle via kebab menu to open as bottom sheet. Main area full-width.Right panel opens, 280px narrow.Right panel 320px (narrow) or 400px (wide).
Detail-3b TabbedTabs horizontal-scroll. Content reflows to full width.Normal layout.Normal.
Detail-3c DrawerDrawer becomes full-viewport sheet.Drawer 400-600px overlay.Drawer 400-600px overlay.
Form<PageContent narrow> max-width 100%. ActionBar sticks to bottom.2xl max-width.2xl max-width.
WorkbenchCustom per surface — usually hide secondary panels, focus main content.Full layout.Full layout.
Dashboard<CardGrid columns={1}> — one metric per row.<CardGrid columns={2}>.<CardGrid columns={3-4}>.
CanvasCanvas takes full viewport; floating controls stack vertically.Full canvas + controls.Full canvas + controls.
FocusSidebar collapses behind drawer (per shell rule). Stepper collapses to “Step N of M” text. Content centered, max-width fits viewport.Sidebar visible. Full stepper on its own row + centered content.Sidebar visible. Full stepper on its own row + centered content.

What we deliberately don’t do on mobile: stack workflow-canvas nodes (canvas becomes pan/zoom only), render the right-panel properties on a phone-sized screen (hidden by default), or reflow a Detail-3a Split into tabs (use the archetype’s own responsive behavior — if a screen genuinely needs mobile-first rethinking, pick Drawer or Form instead).


Shell anatomy

The app viewport is 100vh. No whole-page scroll — ever.

ShellLayout
├── IconRail (48px, hidden for single-app users)
├── AppSidebar (collapsible, 200px default)
└── Main Area (flex-1, flex-col, overflow-hidden)
├── [ListPageBar | DetailPageBar] (h-12, shrink-0) ← NEVER scrolls
└── <PageContent> ← ONLY scroll zone

Dimensions

ElementSizeComponent / classBehavior
Icon Rail48pxw-12Full height. Auto-hides if 1 app only.
App Sidebar200pxRadix collapsibleCollapses to icons. Toggle: Cmd+B.
Sidebar Hdr48pxh-12 border-bAligns with page bar.
Page Bar48px<ListPageBar> / <DetailPageBar>Fixed above scroll.
Contentremaining<PageContent>The one and only scroll container.

Viewport contract

  • The entire app fits in 100vh. The browser chrome shows; the app never scrolls as a whole.
  • SidebarInset uses overflow-hidden.
  • Only <PageContent> scrolls.
  • Page bar sits above the scroll area and is never affected by it.

Icon rail behavior

  • Hidden entirely when permittedApps.length === 1 — a single-app user must not feel the multi-app system.
  • Active indicator: 4px accent bar on left edge (bg-primary, 20px tall, rounded).
  • Hover: background tint.
  • Width 48px, icons 20px (h-5 w-5), centered in 40px hit target.

User avatar home

  • Multi-app: rail footer, bottom-left.
  • Single-app: sidebar footer, bottom-left.

Same corner, always.


Layout components

Every page uses a fixed set of primitives. No raw divs for layout.

Page-level

import { ListPageBar } from '@na/ui/components/ListPageBar';
import { DetailPageBar } from '@na/ui/components/DetailPageBar';
import { PageContent } from '@na/ui/components/PageContent';

Content-level (inside PageContent)

import { ContentSection } from '@na/ui/components/ContentSection';
import { ContentToolbar } from '@na/ui/components/ContentToolbar';
import { SplitContent } from '@na/ui/components/SplitContent';
import { CardGrid } from '@na/ui/components/CardGrid';
import { StatRow } from '@na/ui/components/StatRow';
import { FormSection } from '@na/ui/components/FormSection';
import { ActionBar } from '@na/ui/components/ActionBar';

Attio-native primitives (Plan 2)

Net-new primitives required to match Attio’s layout vocabulary. Specified here for reference; implementation tracked in docs/plans/2026-04-24-attio-layout-migration.md. Do not roll your own — wait for the primitive.

// Right-side detail panel on genuine 3a Split screens.
// Second allowed scroll zone (beyond <PageContent>). Flat, no card wrapper.
// Has its own tab bar (e.g., Details / Comments).
import { RightPanel } from '@na/ui/components/RightPanel';
// Flat key-value list inside a RightPanel or settings section.
// Icon + label + inline-editable value per row.
import { PropertyList, PropertyRow } from '@na/ui/components/PropertyList';
import { InlineEditField } from '@na/ui/components/InlineEditField';
// Section with ▾ chevron, persists collapsed state per user in localStorage.
// Extension of ContentSection; prefer over a raw <div><h3></h3></div>.
import { CollapsibleSection } from '@na/ui/components/CollapsibleSection';
// Segmented control for table / kanban / calendar on task/flow screens only.
// URL-backed state via ?view=<id>. Not for registries or audit logs.
import { ViewSwitcher } from '@na/ui/components/ViewSwitcher';
// Pill-style filter chips above a collection.
// Replaces button-rows-as-filters. AND semantics; clicking a chip re-opens its picker.
import { FilterChips } from '@na/ui/components/FilterChips';
// Anchored popover on entity-name hover in a table row.
// 200ms open / 100ms close; contains avatar, key fields, quick actions.
import { HoverCardPreview } from '@na/ui/components/HoverCardPreview';
// Inbox-archetype primitive: list left + preview right with j/k/e keyboard triage.
import { InboxList } from '@na/ui/components/InboxList';
// Kanban board: columns = status values, drag-drop + multi-select.
import { BoardView } from '@na/ui/components/BoardView';
// Focus archetype shell: main-pane takeover (sidebar stays visible), two-row header, ✕ top-left.
import { FocusLayout, Stepper } from '@na/ui/components/FocusLayout';

These aren’t “maybe someday” — each closes a specific gap where engineers currently reach for raw <div> + Tailwind or misuse <Dialog> / <Card> / <SplitContent>. Use the existing primitives until Plan 2 ships, but flag the gap in review so the migration doesn’t stall.

<ListPageBar> — top bar for List + Form pages

<ListPageBar
title="Contacts"
actions={<Button size="sm">+ New Contact</Button>}
>
<SearchInput placeholder="Search..." /> {/* optional center slot */}
</ListPageBar>
  • title is the entity plural name (“Contacts”, “Agents”, “Tools”).
  • Max 1 primary action, size="sm".
  • No subtitle. Descriptions go in the content area.

<DetailPageBar> — top bar for Detail pages

<DetailPageBar
backTo="/contacts"
title={contact.name}
basePath={`/contacts/${id}`}
tabs={[
{ label: 'Overview', path: '' },
{ label: 'Activity', path: '/activity' },
{ label: 'Notes', path: '/notes' },
]}
actions={<Button size="sm">Edit</Button>}
/>
  • Back button always goes to parent list URL — never browser back.
  • Entity name truncates at max-w-48.
  • Max 8 tabs. More than that means this should be two detail pages.
  • Tabs: text only, no icons. Active tab = 2px underline bg-foreground.
  • Max 1 primary action, size="sm".

<PageContent> — the only scroll zone

<PageContent> // default padding p-4 md:p-6
<PageContent spaced> // adds space-y-6 between children
<PageContent narrow> // constrains to max-w-2xl (Form archetype)
<PageContent spaced narrow>// both

This is the ONLY element on the page that scrolls. No nested scroll containers.

<ContentSection> — grouped content

<ContentSection
title="Tools"
description="External APIs this agent can call"
actions={
<Button
variant="outline"
size="sm"
>
Add Tool
</Button>
}
>
<CardGrid>{/* ... */}</CardGrid>
</ContentSection>

Section actions are variant="outline" size="sm"never primary (Save is owned by ActionBar). Section actions are “add item” level, not “commit the page” level.

<ContentToolbar> — filters + bulk actions

<ContentToolbar actions={<Button size="sm">Upload</Button>}>
<SearchInput />
<Select>...</Select>
</ContentToolbar>

Left slot: filters, search, sort. Right slot: bulk actions or upload.

<SplitContent> — two-column layout

<SplitContent sidebar={<Card>Model Settings</Card>}>
<FormSection>Main fields</FormSection>
</SplitContent>

Stacks on mobile (below md). Sidebar widths: 25% | 30% | 35% | 40% | 50%. Default 30%.

<CardGrid> — responsive card grid

<CardGrid columns={3}>
<Card>A</Card>
<Card>B</Card>
<Card>C</Card>
</CardGrid>

columns: 2 | 3 | 4. Single column on mobile, grid at md: and up.

<StatRow> — horizontal KPI row

<StatRow
items={[
{ label: 'Total', value: '128' },
{ label: 'Indexed', value: '120' },
{ label: 'Failed', value: <span className="text-destructive">8</span> },
]}
/>

<FormSection> — form field group

<FormSection
title="Basic Info"
description="Shown on the agent card"
>
<div className="space-y-1.5">
<Label>Name</Label>
<Input />
</div>
<div className="space-y-1.5">
<Label>Description</Label>
<Textarea />
</div>
</FormSection>

Internal field spacing is space-y-4. Label-to-input is space-y-1.5.

<ActionBar> — sticky form actions

<ActionBar>
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</ActionBar>
<ActionBar align="between">
<Button variant="destructive">Delete</Button>
<div className="flex gap-2">
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</div>
</ActionBar>

CRITICAL: ActionBar is a sibling of <PageContent>, not inside it. If it’s inside, it scrolls away with content. Cancel on the left, primary action on the right.


Action placement — the 6 zones

Every action button has exactly one correct zone. Not optional.

┌──────────────────────────────────────────────────────────┐
│ ① PAGE BAR — page-level primary action (max 1) │
├──────────────────────────────────────────────────────────┤
│ ② SECTION HEADER — "add item" level actions │
│ │
│ ③ CONTENT TOOLBAR — bulk / selection actions │
│ │
│ ④ INLINE ROW — edit/delete per-item (ghost icon-xs) │
│ │
│ ⑤ ACTION BAR — form Save/Cancel (sticky bottom) │
└──────────────────────────────────────────────────────────┘
⑥ DIALOG/SHEET FOOTER — modal confirm/cancel
⑦ RIGHT-PANEL PROPERTY ROW — inline edit on click, no action button
⑧ FOCUS-VIEW ACTION BAR — Cancel / Previous / Next, sticky bottom of focus body (NOT a floating pill)
Action typeZoneVariantSize
Create entity① Page barprimarysm
Preview / Launch① Page barprimarysm
Add item to section② Section hdroutlinesm
Save form⑤ Action barprimarydefault
Cancel form⑤ Action baroutlinedefault
Delete with confirm⑤ Action bardestructivedefault
Bulk delete③ Toolbardestructivesm
Row edit④ Inlineghosticon-xs
Row delete④ Inlineghosticon-xs (wrap in ConfirmPopover)
Dialog confirm⑥ Dialog ftrprimarydefault
Dialog destructive confirm⑥ Dialog ftrdestructivedefault
Right-panel property edit⑦ Rowinlineclick-to-edit, blur commits
Wizard Next⑧ Focus barprimarydefault
Wizard Previous⑧ Focus baroutlinedefault
Wizard Cancel⑧ Focus barghostdefault

Forbidden action patterns

NEVERDO
Save button in sidebar or section header<ActionBar> as sibling of <PageContent>
<ActionBar> inside <PageContent>Sibling outside the scroll area
> 1 primary action in page barMove secondary actions to section headers
”OK” or “Confirm” as dialog button labelUse the verb: “Delete”, “Create”, “Save Changes”
Delete without confirmWrap in <ConfirmPopover> or <ConfirmDialog>
> 3 icon buttons per table rowUse <DropdownMenu> for overflow
Full-width buttons outside dialogsOnly cards + dialog footers use full-width
Back button inside contentBack belongs in <DetailPageBar>
Mixed button sizes in same zoneOne size per zone (table above)
Floating-pill wizard actionsUse actions prop on <FocusLayout> — sticky bar, full-width container, right-aligned cluster
Stepper crammed onto title row of focusTwo-row <FocusLayout> header — stepper gets its own row
Focus route mounted outside AppLayoutMount inside AppLayout so the Sidebar stays visible (focus is main-pane, not viewport)

Page type templates

The three basic page types every app ships.

Type 1 — List page

<>
<ListPageBar
title="Contacts"
actions={
<Button size="sm">
<Plus className="h-4 w-4" /> New Contact
</Button>
}
>
<SearchInput placeholder="Search contacts..." />
</ListPageBar>
<PageContent spaced>{/* table, cards, or grid */}</PageContent>
</>

Rules: title = entity plural, primary action top-right size="sm", optional center search, content handles own empty/loading.

Type 2 — Detail page with tabs

<>
<DetailPageBar
backTo="/contacts"
title={contact.name}
basePath={`/contacts/${id}`}
tabs={TABS}
actions={<Button size="sm">Edit</Button>}
/>
<PageContent>
<Outlet />
</PageContent>
</>

Rules: back goes to parent list, max 8 tabs, text-only tabs, max 1 primary action.

Type 3 — Full page / form

{
/* Settings form */
}
<>
<ListPageBar title="Settings" />
<PageContent narrow>{/* form fields */}</PageContent>
<ActionBar>
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</ActionBar>
</>;
{
/* Dashboard */
}
<>
<ListPageBar title="Dashboard" />
<PageContent spaced>{/* charts, widgets */}</PageContent>
</>;

Rules: settings/forms use narrow, dashboards use spaced (full width).


Advanced templates

Beyond the three basic types, these compositions cover the common complex cases. For the use-case-indexed catalog (which template to pick for which screen) and net-new templates (Calendar, Map, Tree, Audit Log, Permissions Matrix, Auth, Command Palette, Notifications inbox, Status page, etc.), see 11-templates-catalog.md.

Template A — Dashboard (Dashboard archetype)

Metrics → charts → detail table. Top-to-bottom priority.

<>
<ListPageBar
title="Dashboard"
actions={
<Button
size="sm"
variant="outline"
>
Export
</Button>
}
>
<DateRangePicker />
</ListPageBar>
<PageContent spaced>
{/* ① Metrics — max 4 per row, equal height */}
<CardGrid columns={4}>
<MetricCard
label="Total Users"
value="1,234"
trend="+12%"
/>
<MetricCard
label="Active"
value="567"
trend="-3%"
/>
<MetricCard
label="Conversion"
value="89.2%"
trend="0%"
/>
<MetricCard
label="Revenue"
value="$12k"
trend="+8%"
/>
</CardGrid>
{/* ② Charts — 2 per row, equal height */}
<CardGrid columns={2}>
<Card>
<CardContent>
<LineChart />
</CardContent>
</Card>
<Card>
<CardContent>
<BarChart />
</CardContent>
</Card>
</CardGrid>
{/* ③ Detail table — full width, always last */}
<ContentSection title="Recent Activity">
<DataTable
columns={columns}
data={data}
/>
</ContentSection>
</PageContent>
</>

Template B — Detail 3a Split (record with activity)

Purpose: The canonical record detail — the page you land on when clicking an entity name in a list. Shows the entity’s ongoing life (activity, timeline, notes) in the main area and its static metadata (properties) in a persistent right panel.

Attio reference: This is exactly how Attio renders Company, People, and Deal records. The structure below is measured from app.attio.com (April 2026).

Anatomy (Attio-measured):

┌─ DetailPageBar ─────────────────────────────────────────────────────┐
│ Row 1 (h-10): ← Back · "1 of N in List" · [action icons] │
│ Row 2 (h-14): [avatar] Entity Name ☆ · description │
│ Row 3 (h-10): Overview | Activity | Emails | Notes | +2 more │
├─ Main (flex-1, scrollable, ~70%) ──┬─ Right Panel (~30%) ──────────┤
│ │ │
│ § Highlights (3×2 stat card grid) │ Tabs: Details | Comments │
│ ┌────┐ ┌────┐ ┌────┐ │ ▾ Record Details │
│ │ │ │ │ │ │ │ icon + label → value │
│ └────┘ └────┘ └────┘ │ icon + label → value │
│ ┌────┐ ┌────┐ ┌────┐ │ icon + label → [tag chips] │
│ │ │ │ │ │ │ │ ▸ Show all values │
│ └────┘ └────┘ └────┘ │ ▾ Lists / Tags │
│ │ (secondary grouping) │
│ § Activity › (collapsible) │ │
│ [timeline entries] │ │
│ │ │
│ § Notes (1) › [+] │ │
│ [note entries] │ │
└─────────────────────────────────────┴───────────────────────────────┘

Split ratio: ~70% main / ~30% right panel. Right panel width: w-80 (320px) narrow, w-[400px] wide.

Code pattern:

<DetailPageBar
backTo="/entities"
title={entity.name}
basePath={`/entities/${id}`}
tabs={[{ label: 'Overview', path: '' }, { label: 'Activity', path: '/activity' }]}
/>
<div className="flex flex-1 overflow-hidden">
<PageContent>
{/* ① Highlights — stat cards are the ONE card use on this page */}
<CardGrid columns={3}>
<HighlightCard label="Status" value={status} icon={Circle} />
<HighlightCard label="Score" value={score} icon={Target} />
<HighlightCard label="Owner" value={owner} icon={User} />
</CardGrid>
{/* ② Activity / timeline — the primary main-area content */}
<ContentSection title="Activity"></ContentSection>
{/* ③ Notes — optional, collapsible */}
<ContentSection title="Notes" actions={<Button size="sm">+</Button>}></ContentSection>
</PageContent>
<RightPanel tabs={[{ label: 'Details' }, { label: 'Comments' }]}>
<CollapsibleSection title="Record Details" defaultOpen>
<PropertyList>
<PropertyRow icon={User} label="Agent"><InlineEditField /></PropertyRow>
<PropertyRow icon={Clock} label="Duration"><InlineEditField /></PropertyRow>
</PropertyList>
<button className="text-sm text-muted-foreground">Show all values ›</button>
</CollapsibleSection>
<CollapsibleSection title="Tags" defaultOpen>
</CollapsibleSection>
</RightPanel>
</div>

Main area content hierarchy:

  1. Highlights — stat cards in a 3-column grid. Max 2 rows (6 cards). These are the ONLY cards on this page.
  2. Activity — reverse-chronological timeline. Each entry: avatar + actor + verb + object + timestamp.
  3. Notes — optional. Collapsible section with + add button in section header.

Right panel rules:

  • Has its own tab bar (Details | Comments). Tab content scrolls independently.
  • Uses flat <PropertyList> with <CollapsibleSection> groups. No <Card>.
  • Property rows: icon + label (left) + inline-editable value (right). ~32px row height.
  • “Show all values” toggle hides low-priority properties by default.
  • Is the second allowed scroll zone (exception to “only <PageContent> scrolls”).

When to use 3a Split:

  • ✅ Entity has ongoing activity (timeline, notes, communication history)
  • ✅ Entity has first-class metadata that benefits from a persistent panel
  • ✅ Main area content type is feed/timeline, not form fields

When NOT to use 3a Split:

  • ❌ Main area would just be more form fields → use Template D (Settings)
  • ❌ Entity has many distinct configuration surfaces → use 3b Tabbed
  • ❌ Right panel would be empty or contain only 2-3 properties → skip the split, use full-width

Template C — Monitoring / live status (Dashboard archetype, real-time)

ListPageBar: title + auto-refresh toggle
① Status banner (conditional — only on alert)
② Status grid — CardGrid with colored dot indicators
③ Live chart — full width, updating
④ Event log — reverse chronological

Rules: never show “all good” banners (wastes space); color-code with left border or dot, not full card bg; event log shows relative times (”30s ago”).

Template D — Settings / Configuration (Form archetype)

Purpose: Document-style settings that the user reads top-to-bottom, modifies specific fields, and saves. This is the template for all configuration surfaces — agent settings, workspace preferences, billing, security, integrations.

Attio reference: Attio’s Objects, Members, Billing pages all follow this pattern: a large standalone title in the content area (not in the page bar), centered ~600px max-width, generous 40–60px whitespace between sections, and no <Separator> lines between sections.

ListPageBar: title (slim, breadcrumb-style for global settings)
PageContent narrow
[Optional: H1 page title (24px) inside content for standalone settings pages]
FormSection "Voice Settings"
description: "How the agent sounds"
[form fields, space-y-4 between fields]
[40px whitespace gap — space-y-10, NO <Separator>]
FormSection "RAG Settings"
description: "Knowledge base and retrieval"
[form fields]
[40px whitespace gap]
FormSection "Model"
[form fields]
[40px whitespace gap]
CollapsibleSection "Advanced" (collapsed by default)
[rarely-touched fields]
ActionBar: [Cancel] [Save]

Spacing rules (Attio-measured):

  • Section title → description: space-y-2 (8px)
  • Description → first field: 24px gap
  • Between fields within a section: space-y-4 (16px)
  • Between <FormSection> blocks: space-y-10 (40px) — NOT space-y-6
  • Use whitespace to separate sections. <Separator> lines are allowed but secondary — Attio prefers whitespace-only separation.

Standalone settings page title: When building a global settings page (not a tab within a detail page), render the page title as <H1> (24px) inside <PageContent>, not as the PageBar title. The PageBar shows only a slim breadcrumb. This creates the Attio-style document reading feel where the title is part of the content.

When this IS a tab (e.g., agent Settings tab within a Detail page): The DetailPageBar already has the entity title (20px H2). Section headings (H3 16px) are sufficient — no need for a standalone H1.

Progressive disclosure: Group fields into:

  • Primary — always visible. The fields a user touches on first setup and occasionally adjusts (Language, Voice, LLM Model).
  • Advanced — collapsed by default via <CollapsibleSection>. Fields that are set-and-forget or expert-only (speaking rate, pitch, reranking, max tokens).

This matches Attio’s “Show all values” pattern — common properties are visible, rare ones are behind a disclosure control.

Rules: always PageContent narrow; 40px gaps between sections; danger zone always last, uses <ConfirmDialog>.

Template E — Split form (list-nav + settings) (Form archetype)

Used when a tab has a list of items (tools, knowledge-base docs, channels) and the user selects one to configure. The “split” is list-for-navigation + settings-for-content, not main-and-properties. Do not mistake this for 3a Split.

DetailPageBar
SplitContent (30/70 — list left, settings right)
Sidebar: list of items (selection only, no actions per row)
Main: PageContent narrow
FormSection (for selected item's config) + Separator between groups
OR PropertyList + InlineEditField (if every field is independently savable)
ActionBar: [Cancel] [Save] — only if form-mode (not inline-edit)

Rules:

  • Sidebar is selection only. No bulk actions, no filters here; those go in a <ContentToolbar> above the sidebar if needed.
  • Right side is flat<FormSection> + <Separator>. No card wrappers, no right-panel-inside-right-side.
  • If every field is independently savable → <PropertyList> + <InlineEditField>, no <ActionBar>.
  • If fields are related and commit together → <FormSection> + <ActionBar> at bottom.
  • Both sides scroll independently — this is allowed because it’s the sidebar-as-nav pattern, not a general “two scroll zones are fine” permission. The right side is the only place the user reads/edits; the left is just navigation.

Template F — Focus View wizard (Focus archetype)

Main-content takeover for multi-step linear flows. Sidebar stays visible, app chrome stays addressable, centered content, two-row header, sticky bottom action bar. Body uses bg-background — no surface tint. See references/attio-layout-patterns.md §3d for full anatomy.

<FocusLayout
closeTo="/agents" // ✕ top-left returns here, not browser-back
breadcrumb={['Agents', 'Create']}
stepper={{
current: 2,
steps: ['Template', 'Configure', 'Connect tools', 'Review'],
}}
actions={
<>
<Button
variant="ghost"
onClick={cancel}
>
Cancel
</Button>
<Button
variant="outline"
onClick={prev}
>
Previous
</Button>
<Button onClick={next}>Next</Button>
</>
}
>
{/* step-specific content, centered, max-width-constrained */}
<div className="mx-auto max-w-2xl space-y-6 py-12">
<FormSection title="Choose a template"></FormSection>
</div>
</FocusLayout>

Mount inside AppLayout so the Sidebar renders as a sibling — do not register the route outside the app shell.

// router.tsx — CORRECT
{
element: <AppLayout />,
children: [
{ path: 'agents/new', element: <AgentCreatePage /> }, // FocusLayout fills the main pane
{ path: 'agents', element: <AgentListPage /> },
// ...
],
}
// router.tsx — WRONG (overlays the Sidebar)
{ path: '/agents/new', element: <AgentCreatePage /> }

Rules:

  • Mount inside AppLayout so the Sidebar stays visible. The focus view is scoped to the main pane, not the viewport.
  • top-left closes and returns to closeTo. Not a back button. Esc same.
  • Two-row header — row 1 close + breadcrumb (h-12), row 2 stepper (h-12). Single-row crowding is forbidden — the stepper needs its own row to breathe.
  • Linear stepper — numbered ① ② ③ ④, future steps muted, no random-access.
  • Body bg is bg-background — same as the rest of the app. The focus is established by structure (two-row header + sidebar context + sticky bottom action bar), NOT by surface tint. Earlier spec drafts called for a tinted body; that was a misread of Attio (the tinted color in references is the old sidebar bg, not the focus body).
  • Centered, max-width content — never edge-to-edge.
  • Sticky bottom action bar via the actions prop — border-t bg-background px-3 py-2, right-aligned button cluster. Same shape as the Form archetype’s ActionBar. Floating-pill action clusters are forbidden — they look misplaced and clip on small viewports.
  • No app-level actions at top. Actions are step-local (bottom action bar).
  • Not a modal — no role="dialog", no focus trap. The user can click into the Sidebar to navigate away (with discard confirm if dirty). This is a focused page, not an overlay.
  • Use when: 3+ sequential steps, user must finish to commit (import, onboarding, complex creation).
  • Do NOT use when: single-step form (Dialog or Form page), config the user dips in/out of (Sheet or settings page), record detail (Split or Tabbed).

Metric Card pattern

<Card>
<CardContent className="py-4">
<p className="text-muted-foreground text-sm">{label}</p>
<div className="flex items-baseline gap-2">
<span className="text-2xl font-semibold tabular-nums">{value}</span>
{trend && (
<span
className={cn(
'text-xs font-medium',
trend.startsWith('+') ? 'text-primary' : 'text-destructive'
)}
>
{trend}
</span>
)}
</div>
</CardContent>
</Card>

Rules: label (minor) → value (major, text-2xl tabular-nums) → trend (colored small). Max 4 per row. Never 5+.

Result template (within Form / Focus archetypes)

Not a standalone archetype — a final-state render that belongs at the end of a Form or Focus flow. For post-action feedback (import complete, payment received, export started).

PageContent (centered)
┌─────────────────────────┐
│ [big icon, 64px] │
│ Primary outcome title │
│ Secondary details line │
│ [Primary CTA] [Back] │
└─────────────────────────┘

Rules: max two actions, clear next step, not a dead end. Don’t use it for field-level “saved” feedback — that’s inline (see 05-patterns.md).

Exception template (rendered inside the current archetype)

Not a standalone archetype — an error state that replaces <PageContent> children while keeping the shell. For 404 / 403 / 500 / offline.

Full viewport, centered
┌─────────────────────────┐
│ [muted large icon] │
│ "Page not found" │
│ "The page you're looking for doesn't exist" │
│ [Go home] [Go back] │
└─────────────────────────┘

Rules: never show an error code without a plain-language title. Always offer at least one recovery action.


Visual hierarchy inside pages

Three contrast tiers

TierRoleTextBg
MajorPrimary content, primary actiontext-foreground ± font-semiboldbg-background
MinorSecondary info, secondary actiontext-muted-foregroundbg-muted / transparent
BackgroundChrome, borders, disabledtext-muted-foreground/50bg-muted/50

One major per row. 30% major / 70% minor.

Zone-inherited action weights

ZoneVariant
① Page barprimary (major)
② Section headeroutline sm (minor)
③ Content toolbarvaries (minor by default)
④ Inline rowghost icon-xs (bg tier)
⑤ Action barprimary (major)
⑥ Dialog footerprimary or destructive

Putting a primary Save button in a section header breaks the zoning. The page bar already has the primary action; sections are secondary.


Three levels. Each has a distinct visual weight.

LevelElementBgActive indicator
L1 — AppIcon Railbg-sidebar darkest4px accent bar, left edge
L2 — SectionSidebarbg-sidebar mediumBg tint + font-medium
L3 — Sub-sectionDetailPageBar tabsTransparent2px underline bg-foreground

Rules:

  1. Never mix levels. Sidebar doesn’t contain tabs; tabs don’t contain sidebars.
  2. Active state at every level simultaneously.
  3. One active item per level.
  4. Deeper levels are dismissible (L3 only on detail pages; L1 hides for single-app users).
  5. Navigation never scrolls horizontally on desktop.
  6. Clicking a higher level resets lower levels (switching sidebar items resets to default tab).

Tables

All tables use <Table> from @na/ui/components/table. Never raw <table>.

Default is edge-to-edge flat — no card wrapping, no rounded-lg border div. Attio’s tables span the content width with a subtle bottom-border on the header row only. This is the live-inspected rule from references/attio-design-language.md L25–31.

<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Count</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow
key={item.id}
className="hover:bg-muted/50 transition-colors"
>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>
<Badge>{item.status}</Badge>
</TableCell>
<TableCell className="text-right tabular-nums">{item.count}</TableCell>
</TableRow>
))}
</TableBody>
</Table>

Rules:

  • No wrapper div with border + rounded corners. The table is flat, edge-to-edge within <PageContent>. If you need visual separation from surrounding content, use spacing or <Separator>, not a box.
  • Column header. 12px, muted foreground, font-medium — not uppercase, not bold (matches the live-inspected Attio column header).
  • Numeric columns: text-right tabular-nums on both header and cell.
  • Interactive rows: hover:bg-muted/50 transition-colors.
  • Non-interactive rows: no hover.
  • Empty state: single <TableCell colSpan={n}> with centered muted text or <EmptyState>.
  • For built-in search/sort/pagination/bulk-select/filter-chips: use <DataTable>. Its default variant="flat" matches the Attio edge-to-edge rule; opt into variant="bordered" only for rare cases (e.g., a table nested inside a card-based dashboard widget).
  • Footer row is Attio-native: count on the left, optional per-column calculations. Sticky to the bottom of the scroll zone when the table is long. Use <DataTable footer> prop or the <TableFooter> primitive.

Forbidden: wrapping the table in <Card>, <div className="rounded-lg border">, or any box that imposes a boundary other than the table’s own header/footer. See also attio-layout-decision-guide.md § “Settings is not a Detail-Split” — the same flat-first rule applies to settings sections.


Cards

All cards use <Card> from @na/ui/components/card.

<Card>
<CardContent>{/* default p-4 — do not override */}</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm">Section Title</CardTitle>
</CardHeader>
<CardContent className="space-y-4">{/* ... */}</CardContent>
</Card>

Rules:

  • CardContent default padding is p-4 — do not override.
  • Cards at rest: border only, no shadow.
  • Selected/active card: ring-2 ring-primary (not border-primary).
  • No hover state unless the whole card is clickable.

When to use a card (Attio card-vs-flat rule)

From references/attio-design-language.md L152–162 — Attio’s flat-first philosophy. Cards are for content containers — self-contained, potentially interactive entities. Everything else is flat.

Use <Card> when…Use flat <FormSection> / <PropertyList> / <Separator> when…
Dashboard metric tile (label + big number + trend)Form field groups inside a settings page
Clickable entity preview (agent, tool, channel in a grid)Right-side property panel (<RightPanel> content)
Empty-state CTA (centered “create your first X” block)Sequential form sections (name → description → permissions)
Self-contained widget (iframe preview, embedded player)Settings / configuration sections
Danger zone (red-tinted card, always last)Right side of a split-form layout

Forbidden uses — these are violations:

  • <Card> wrapping a <Table> — tables are edge-to-edge flat.
  • <Card> around form field groups — use <FormSection> + <Separator>.
  • <Card> around a right-side property panel — use flat <PropertyList>.
  • <Card> wrapping a progress bar, alert, or toast-like status banner — banners are flat alerts (role="status" / role="alert"), not content containers.
  • ❌ Nested cards (card inside card).
  • <Card> around a collapsible section — the section chevron owns the boundary.

See references/attio-layout-decision-guide.md § “Card vs flat” for nx-agent worked examples.


Loading and empty states

Loading

Skeleton inside PageContent. Never a full-page spinner.

<>
<ListPageBar title="Contacts" />
<PageContent spaced>
<Skeleton className="h-8 w-64" />
<Skeleton className="h-64 w-full" />
</PageContent>
</>

Empty

<EmptyState> inside PageContent. Every empty state has: icon, title, description, primary action.

<PageContent>
<EmptyState
icon={<Inbox className="h-10 w-10" />}
title="No contacts yet"
description="Create your first contact to get started."
action={<Button>Add Contact</Button>}
/>
</PageContent>

Forbidden layout patterns (the big table)

NEVER do thisDo this instead
<div className="flex-1 overflow-auto p-4 ..."><PageContent>
<div className="max-w-2xl"> for forms<PageContent narrow>
Raw div for content wrapper<PageContent>
Breadcrumb barBack button in <DetailPageBar>
<h1> outside page bartitle prop in <ListPageBar> / <DetailPageBar>
position: sticky inside contentPage bar is already fixed; nothing else needs to stick
position: fixed inside contentPut it in page bar or layout-level
Full-page loading spinnerSkeleton loaders inside <PageContent>
Hero section / bannerContent starts immediately
Nav bar below page bartabs prop in <DetailPageBar>
Modal for forms > 4 fields<Sheet> or dedicated Form page
Nested scroll containers<PageContent> is the only scroll zone
overflow-auto on custom divsOnly <PageContent> scrolls
Raw colors (text-gray-500)Semantic tokens (text-muted-foreground)
Custom layout chrome<ListPageBar> or <DetailPageBar> only
> 8 tabsGroup into fewer tabs with sections
Subtitle in page barMove description to content area
min-h-screen / whole-page scrollViewport is 100vh, <PageContent> scrolls
Fourth navigation tierTwo sidebar tiers + inline tabs. No deeper.
<div><h2>Title</h2>...</div> for sections<ContentSection title="...">
<div className="flex items-center gap-3"> toolbar<ContentToolbar>
<div className="flex flex-col lg:flex-row"> split<SplitContent sidebar={...}>
<div className="grid grid-cols-3 gap-4"> cards<CardGrid columns={3}>
<div className="flex items-center gap-4"> stats<StatRow items={[...]} />
<div className="space-y-4"> for form groups<FormSection>
Raw sticky div for submit buttons<ActionBar>
font-bold (700 weight)font-semibold (600) max — two weights only
Left-aligned numbers in table columnsRight-align with text-right tabular-nums
Odd spacing (p-5, gap-3.5, p-7)8px ladder: p-2 (8), p-4 (16), p-6 (24)
Shadows on resting cardsBorder only; shadow only on hover for clickable cards
Both buttons primary in a neutral decisionUse outline for both
<ActionBar> inside <PageContent>Sibling, outside the scroll area
Two active items at same nav levelOne active per level
Section action as primary variantSection actions are outline sm
Row actions as default / primaryRow actions are ghost icon-xs
<div className="rounded-lg border"><Table> wrapperEdge-to-edge flat — no wrapper at all (Attio rule)
<Card> around a <Table>Edge-to-edge flat — tables never wrap in cards
<Card> wrapping a progress bar / alert / status bannerFlat alert (role="status" or role="alert" div) — banners are not content containers
<Card> around form field groups<FormSection> + <Separator> between groups
<Card> around a right-side property panelFlat <PropertyList> in <RightPanel>
Nested <Card> inside another <Card>Flatten — one of them isn’t really a content container
<Dialog> at h-[80vh]+ / max-w-4xl+ / with custom header replacing <DialogHeader>Outgrown Dialog — promote to <Sheet> (triage), dedicated page (shareable), or Focus View (wizard)
<Dialog> containing a stepper, tab bar, or “Step N of M” inside the bodyWizard crammed into Dialog — promote to Focus View (3d) with <FocusLayout> + <Stepper>
Buttons-as-filters (<Button variant="outline"> row with count badges)<FilterChips> — pill-style, clickable to edit, AND semantics
<Button variant="link"> with underline on entity names in a table rowPlain text link with hover underline; hover opens <HoverCardPreview>
Settings sub-tab rendered as Detail 3a SplitSettings is Form archetype — flat FormSection + Separator, no right panel, no nested scroll

Exemptions

These surfaces are exempt from the shell system — they intentionally break out.

Standalone pages

Preview, Playground, Embed widget. Simulate end-user experience. Must live outside AppLayout:

  • Own full-screen layout (h-screen).
  • Still use @na/ui components (Button, Input, etc.) and semantic color tokens.
  • Only the shell itself is exempt; everything inside still follows the spec.

Intra-tab sub-navigation

Inside a tab (L3), you can add a narrow horizontal sub-toolbar for intra-tab switching. This is not a fourth nav tier — it’s a switcher within tab content. Keep it lightweight (text segments, not tabs).


Chat panel

The chat panel is a floating layer available from any app, not part of the page layout.

ModeBehaviorTrigger
Slide-out400px panel slides from right, overlaysDefault open
SplitContent splits 60/40, resizableDrag panel edge left
Full viewPanel takes the entire content areaClick expand
MinimizedSmall floating bubble, bottom-rightClick minimize
DetachedOpens in a new browser windowClick pop-out

Rules:

  • Panel state (mode, width) persists in localStorage per user.
  • Panel is accessible from any app — not CRM-specific.
  • Minimized bubble shows unread count badge.
  • Cross-app deep link: ?chat=<contactId> opens panel on that conversation.
  • Panel never pushes the page bar — it overlays or splits content only.

Repetition principle (from 01-principles.md)

Every table looks like every other table. Every card has the same padding. Every form section uses FormSection. Originality in layout is a bug. Reuse, don’t invent.


Checklist for a new page

Before you submit:

Archetype + shape

  • Page is one of the 7 archetypes: List / Detail / Form / Workbench / Dashboard / Canvas / Focus.
  • Detail picks one of 3a Split / 3b Tabbed / 3c Drawer (see patterns doc §3).
  • 3a Split only used when main area has genuine activity/timeline content — not just more properties (if it’s properties-only, it’s Form, not Detail).
  • Multi-step wizard uses Focus archetype (<FocusLayout> + <Stepper>), not a giant <Dialog>.
  • Detail page has max 8 tabs. (If more, split into two detail pages.)

Overlay choice

  • Dialog only for ≤4-field short commits / confirmations. Max sm:max-w-lg.
  • No <Dialog> at h-[80vh]+, max-w-4xl+, or with tabs/steppers inside.
  • Drawer / Sheet used for triage preview (URL-backed ?id=<x>) or config side-tasks.
  • Never stacking overlays (no drawer-in-dialog, no dialog-over-drawer).

Chrome + scroll

  • Uses <ListPageBar> or <DetailPageBar> — no custom chrome.
  • Content uses <PageContent> — no raw divs with overflow-auto.
  • Second scroll zone only allowed inside <RightPanel> (3a Split) or Template E split-form.
  • No <h1> or title outside the page bar.
  • No breadcrumbs.
  • No sticky/fixed inside content.
  • Whole page fits in 100vh — no page-level scroll.
  • Back button goes to parent list (not browser back).

Flat-first composition

  • Tables are edge-to-edge flat — no <div className="rounded-lg border"> wrapper, no <Card> around table.
  • Form sections use <FormSection> + <Separator> — no <Card> around field groups.
  • Settings sub-tabs are Form archetype (single-column narrow), not Detail 3a Split.
  • Right-side property panels use flat <PropertyList>, not <Card>.
  • Progress bars / alerts / status banners are flat (role="status" / role="alert"), not wrapped in <Card>.
  • No nested <Card> inside <Card>.

Attio-native primitives

  • Filter rows use <FilterChips>, not <Button variant="outline"> rows with count badges.
  • Table row entity names use plain links with hover underline + optional <HoverCardPreview>, not <Button variant="link">.
  • Property editing uses <InlineEditField> inside <PropertyList>, not per-row Edit buttons.
  • Collapsible sections use <CollapsibleSection>, not a hand-rolled <details> or <div> + chevron.

Actions

  • <ActionBar> is a sibling of <PageContent> — not inside it.
  • Page bar has max 1 primary action.
  • Section actions are outline sm. Row actions are ghost icon-xs (max 3).
  • Destructive actions wrapped in <ConfirmPopover> or <ConfirmDialog>.
  • Dialog confirm labels use the verb (“Delete”, “Create”) — never “OK”.
  • Focus-view step actions are bottom-right (zone ⑧), not in a sticky <ActionBar>.
  • Navigation shows active state at every level (L1, L2, L3).
  • Only one active item per level.
  • Section headers use <ContentSection>, toolbars use <ContentToolbar>, grids use <CardGrid>.

View switching

  • <ViewSwitcher> only on task/flow screens (review queue, test results). Not on registries, settings, audit logs.

Visuals

  • No raw colors — all semantic tokens.
  • Spacing on the 8px ladder (2, 4, 6).
  • Numbers in tables are text-right tabular-nums.
  • One focal point per row — not everything bold.
  • Empty state uses <EmptyState> with icon + title + description + action.
  • Loading uses <Skeleton> — never a full-page spinner.

Next: 04-components.md. See also references/attio-layout-decision-guide.md for per-screen mapping and decision trees.