Asteroid Design System

Primitives, surfaces and tokens. The spec is DESIGN.md.

Colors

Semantic tokens — OKLCH color space, light & dark mode

Surfaces

background

card

light-muted

muted

heavy-muted

Brand

primary

primary-light

secondary

secondary-light

accent

Status

success

success-light

warning

warning-light

queued

queued-light

destructive

destructive-light

Typography

Two voices — InterVariable for UI, Freight Big Pro for brand & editorial. IBM Plex Mono for coordinates.

Voice 1 · UI · Inter

The operator's console.

Dense UI — tables, forms, buttons, labels. ~95% of the platform and all landing supporting copy.

The quick brown fox jumps over the lazy dog. 1,284 executions completed in 3m 42s average.

Voice 2 · Brand & Editorial · Freight Big Pro

Build agents that run the web.

Landing hero & section titles. In-product hero moments, empty states, onboarding, quotes. One expressive voice across both surfaces.

“A polished power tool that doesn't intimidate.”

Coordinate · IBM Plex Mono

Not a voice — a coordinate system. Identifiers, versions, model slugs, metadata k:v pairs, cron expressions, URLs & paths, raw enum values, and tabular numerics. Where precision beats emotion. Use <Mono> from @workspace/ui/components/mono outside of DataTable (which has meta.mono).

Mono

92890146v3gpt-5claude-opus-4-7-thinking*/5 * * * *env:prodNODE_TRANSITIONED1,248 tokjoe@example.com

Not mono

Acme CorpCompleted$1,204.50(stat-card rule wins)Created a moment agoSave changes

Type roles: pick the role, not a size

type-titleInvoice reconciler
type-sectionBrowser
type-nameRefund window check
type-labelStart URL
type-helpRuns start from this URL.
type-captionEdited 3h ago
type-capsStatus
type-overlineProfile
type-proseSign in with the profile's secrets, then open the invoices tab.
Monoexec_a1b2c3d4e5f6

Editorial tier — Freight Big Pro (use sparingly)

HeroWelcome to Asteroid.
TitleNo executions yet.
Quote“It just works — and then it keeps working.”

In-context example — empty state

Executions

Nothing to see here — yet.

Kick off your first workflow run and this page will fill up with the interesting stuff.

Serif headline + Inter supporting copy + mono overline. The three voices playing together.

Buttons

Button face is lifted from flow, shadow at flow position — sinks on hover, presses on active

Variants × Sizes

variant \\ sizesmdefaultlg
default
destructive
outline
ghost
link

With Icons

Icon Only

States

rest
disabled
loading pattern
rest
disabled
rest
disabled

Common Patterns

Dialog Footer — confirm / cancel

Destructive Confirmation

Toolbar — mixed actions

Tab Toggle — ButtonGroup variant switching

Page Header — primary + secondary actions

My workflows

Split Button — primary action + dropdown

Iconography

Icons are a supporting voice — they reinforce an affordance or classify a type. Four working sizes, six semantic colors, and a strict anti-pattern list. See DESIGN.md §Iconographyfor the canon.

Time components never wear leading icons — the mono typography carries the signal. Cross-link: §Time & Duration.

1 · Size scale

Four working sizes carry 99% of the system. size-3 is only for tiny glyphs inside chips, pills and the drawer Next pill. size-6 is reserved for rare page-header chrome.

size-312px
Tiny inline glyph in a 10–12px element: EntityChip, Pill, the drawer Next pill arrow
size-3.514px
Inside a Button · Badge · table cell · card meta · TabsTrigger default · ChromeTab · entity section header · stat card header
size-416px
Inside a DropdownMenuItem · CommandItem · SidebarMenuButton · TabsTrigger lg
size-520px
Page-header action · section anchor · large standalone control · entity card full tile
size-624px
Page-header brand chrome (rare — logo lockups, 1.5px stroke)
size-832px
Empty-state hero (inside EmptyMedia variant=icon, wrapped in size-10 tile)

2 · In buttons

Button hands every nested icon size-3.5 and gap-1.5. Never pass size-* or mr-* class overrides.

3 · In tabs

TabsTrigger size="default" and ChromeTab use size-3.5. size="lg" steps up to size-4 because the tab IS the page title.

size default
size lg
ChromeTabs

4 · In menus

DropdownMenuItem and CommandItem both hand icons size-4 and gap-2. Delete rows use text-destructive; everything else inherits text-muted-foreground from the primitive.

DropdownMenu
Command

5 · In entity and stat cards

Entity full tile (size-10 rounded-lg) holds size-5; compact (size-7 rounded-md) holds size-3.5. A stacked drawer header uses the same tile as the full card. Stat card header icon is size-3.5, right-aligned in the muted label row.

Entity full tile
Entity compact tile
Active runs
12
4 held · 2 errored

6 · Time components never wear icons

Mono typography + tabular-nums + the two-line tooltip carry the signal. The column header / row label / prefix prop name the field.

Do
Last run

Bare time component, or prefix prop for the label. Mono font does the work.

Don't

Clock/Calendar/Timer prefixing a time component. The column header already says 'Last run'.

7 · Color canon

Icons inherit currentColor. Use semantic tokens. Never text-green-600, text-red-500, and friends.

text-successtext-destructivetext-warningtext-primarytext-admintext-muted-foreground

8 · Empty state hero

EmptyMedia variant="icon" renders a size-10 muted tile around a size-8 icon. Do not pass a size class on the icon itself.

No agents yet
Create your first agent to start running workloads.

9 · Inputs with a leading icon

No built-in SearchInput primitive yet — use an absolutesize-3.5 Search icon with muted foreground and pad the input to match.

10 · Anti-patterns

Four classes of drift the canon forbids. See §Iconography for the full list.

Do
Active

Status badge tint IS the signal — one glyph or none.

Don't
Active

Status-tinted badge AND a same-meaning status glyph next to it.

Do

Let the Button primitive supply size-3.5 and gap-1.5.

Don't

Overriding size-4 fights the canon. The Button no longer uses size-3 by default — trust it.

Do
Succeeded

Semantic tokens: text-success, text-destructive, text-warning, text-primary, text-admin.

Don't
Succeeded

Raw palette colors. No text-green-*, no text-red-*, no hex literals.

Do
12mConfig

One size per visual group — size-3.5 here.

Don't
12mConfig

Mixed size-3 and size-4 in the same row. Pick one.

11 · Admin affordance

The element IS the signal — amber button / amber text instead of an extra badge. AdminBadge scales its Shield glyph (xs: size-2.5, sm: size-3, md: size-3.5).

AdminAdminAdmin

12 · Loaders

Loader2 animate-spin in a status position inherits the surrounding size canon. Keep it the same size as the icon it replaces.

Loading Reconnecting…

Button Groups

Grouped actions with shared offset shadow

Horizontal — outline

Horizontal — alignment

Vertical

With Text & Separator

Page 1 of 10

Variant groups

Badges

Tinted backgrounds with colored text — quiet labels, not loud fills

Variants

DefaultSecondaryErrorOutline

Run status · StatusBadge with the run-status map

QueuedStartingRunningCompletedPausedAwaitingFailedCancelled

Tint and label carry the state. The only glyph is the live dot on states where work is happening now.

Admin Indicators

The affordance IS the signal — admin elements paint themselves amber. Badge stays as the page-header chip and the fallback for elements that already carry color. See DESIGN.md → Admin Indicators.

Token

--admin

Solid amber. Use on raised admin buttons; rarely as a surface.

bg-admin/10

Tinted background — strip, hover/active states, ghost surface.

border-admin/30

Border tint for the strip and outlined admin elements.

Element coloring (primary pattern)

Buttons

Sidebar items

Admin◇Users◇Models◇Usage

Properties & links

Internal cost$0.0418
Bifrost tracebf-7d2c…
Routing overrideasteroid-max

Form-control primitives (admin prop)

<Input admin />
<SelectTrigger admin />
Tracing

Playwright traces for every session.

<FeatureToggle admin /> (threads to <Switch admin />)

Controls inside <AdminOnly> accept an admin prop instead of hand-rolling amber classes. The palette, focus ring, placeholder tone, and switch track all flip together. Size and disabled behavior are unchanged.

AdminBadge — fallback & page-header

Adminsize="xs" — fallback only
Adminsize="sm" — rare
Adminsize="md" — page header

Three contexts

Property

Element coloring is the primary signal — render the value in text-admin.

Internal cost$0.0418
Latency p99412 ms

Control

The button paints itself amber. No badge needed.

Page header

One md badge marks the whole page identity.

Model Management

Admin

Admin mode toggle (live demo)

Admin mode
Platform◇Overview◇Agents◇ExecutionsAdmin◇Users◇Models◇Usage

Agents

Cross-org agents visible — internal toolbar above
Toggle Admin mode for this preview

AdminPageGate (toggle-off interstitial)

Admin mode is off

You turned Admin mode off — internal tools are hidden so it's safe to demo. Re-enable to view this page.

Anti-patterns

Don't bolt a badge onto every admin element

The element's own admin coloring is the signal. Use variant="admin", not <AdminBadge /> stapled next to a default-styled control.

Don't use --admin for warnings

Status colors stay status colors. For "might be dangerous," reach for --warning or --destructive. Admin is a category, not a status.

Don't gate access with color or the badge

Both are signals for admins, not gates. Server-side isAdminUser() route guards still enforce real access.

Don't show admin elements to non-admins

Hide the entire element via <AdminOnly>. Disabled admin features leak product surface.

Tabs

Chrome-style tabs — active tab uses bg-background against a muted bar surface, with inverted SVG corners

One Chrome-tab visual, two component APIs for different semantic models. Both render identically — pick based on what the tabs represent, not how they should look.
  • Tabs (Base UI) — sub-navigation within a page or resource (Overview / Analytics / Reports). Self-contained, keyboard-accessible, internal state.
  • ChromeTabs — browser-style document tabs (the agent workspace: builder, executions, open execution tabs). Controlled, supports drag-to-reorder, close, sticky pins, add-dropdown.

Default framed look — Tabs / ChromeTabs

The canonical chrome-tab visual: transparent strip, active tab with border + stroked inverted corners, content panel with full border + rounded-b-lg, and one shared drop-shadow over the whole tongue-card silhouette. Both Tabs and ChromeTabs default to framed; the panel border auto-paints so consumers don't repeat border border-t-0 rounded-b-lg. Suppress the drop-shadow when the strip and workspace can't share a DOM subtree (agent header pattern) via framedShadow={false}.

Tabs framed (sub-navigation, on bg-light-muted)

Border traces the active tab via the inverted corners and wraps the whole content card. One shared drop-shadow.

ChromeTabs framed (one shared drop-shadow)

Active: exec-92890146 — ChromeTabs wraps strip + workspace and applies the shared drop-shadow over the whole tongue-card silhouette when framed.

Flat variant — framed={false} on inline tabs

Opt out via framed={false} for tabs that live inside an existing card, modal, or section, where a bordered tongue card around the content would double up. The strip gains a bg-muted plate; the panel is opinion-free (consumer decides shape).

With icons

Executions content

Text only

Overview content

With badges

All logs

With disabled tab

Active content

Browser-style tabs — ChromeTabs

Active: exec-92890146 — Drag non-sticky tabs to reorder, close with the X button (or middle-click), open more from the + menu. The first three tabs are sticky and cannot be moved or closed.

Custom layouts — ChromeTab standalone

The same visual, used as a standalone primitive. Reach for this only when neither Tabs nor ChromeTabs fits — e.g. a custom router integration that needs full control over rendering.

Standalone ChromeTab (manual layout)

Content for: executions

On light-muted surface

Executions — uses bg-light-muted for the tab bar, matching the agent header.

Minimal

Users content

Full width

All items

Forms and type roles

A form is sections of fields; a field is one label and one control. FormField encodes the anatomy and the aria wiring. Type roles replace ad-hoc sizes and weights. See DESIGN.md §Forms and §Type roles.

Field states

We POST each event here.

(optional)

Admin only.

Placeholders

Yesjane@acme.comFormat example
Yeshttps://portal.acme.comFormat example
YesSearch workflows…Search field, Unicode ellipsis
NoWorkflow nameRepeats the label
NoEnter your API tokenInstruction; use the description
Noe.g. 25 (optional)No e.g.; optional is a prop

FormSections · FormSection · FormField

Browser

Where the agent runs.

Runs start from this URL.

(optional)

Execution

(optional)

How many runs start at once.

Posts to the connected Slack channel.

Label, optional marker and info tip share one row. The description sits between label and control, so the error stays next to the value. Try a concurrency above 50.

Surfaces

Drawers, the graph inspector and large dialogs share one panel anatomy. Drawers stack numbered sections. Status uses one badge with a map per domain. Pick the surface with DESIGN.md §Surfaces.

The rules · first match wins

  1. R1

    Is the user confirming something destructive, irreversible or costly?

  2. R2

    Is the system showing a value the user can see only once?

  3. R3

    Is the user changing exactly one value already on screen?

  4. R4

    Is the user in a workspace, configuring the object they selected?

  5. R5

    Is the user creating, viewing or editing a configuration entity?

  6. R6

    Is it a one-screen task that is not the page's main job?

  7. R7

    Nothing above matched.

The seven surfaces

SurfaceBlocksURLCommits
Inline editNoNoEnter, blur or change
PopoverNoNoChange or Apply
Alert dialogYesNoOne verb
DialogYesNoOne verb
DrawerYes?new= ?id= &section= &edit=1One save bar, drafts kept
Docked panelNoSelectionDraft, Publish commits
Route—YesPer section

One blocking layer

Only an alert dialog may open over a Dialog or drawer, to confirm a destructive action inside it. Creating a dependency slides in within the drawer section, or swaps a Dialog's body. Menus and popovers are not layers.

Entity assignments

EntityViewCreateEditDestroyToday
WorkflowRouteRoute /createInline name, route tabsAlert dialogOK
Workflow nodeDocked panelGraph menuDocked panelAlert dialogMigrates
ExecutionRouteRun: Dialog lg—Alert dialogOK
ScheduleDrawerDrawerDrawerAlert dialogMigrates
BatchRouteRoute wizard—Alert dialogOK
Profile, profile groupDrawerDrawerDrawerAlert dialogOK
SecretProfile sectionSlides in a sectionSlides in a sectionAlert dialogOK
Secret templateDrawerDrawerDrawerAlert dialogOK
EnvironmentDrawerDrawerDrawerAlert dialogOK
IntegrationDrawerDrawerDrawerAlert dialogMigrates
API keyListDrawer—Alert dialogMigrates
Org memberListDrawerInline roleAlert dialogMigrates
TagPopoverInlineInlineAlert dialogMigrates
ClassPass venueRouteDrawerRoute tabsAlert dialogMigrates

Full table with the extra actions (run, retry, show once) in DESIGN.md §Surfaces → Entity assignments.

Docked panel · graph inspector

Browser agent

Runs start from this URL.

PanelHeader with back, type overline, docs link and an inline editable name. Hover the name to edit; Enter commits, Escape reverts.

Drawer · numbered sections, edit mode

Profile
Billing portal · US
Used by 4 workflows
Country
United States
OS
Windows

One list, one section open at a time. Header Edit turns every section into its form; one save bar. Create uses the same sections, with a Next pill on the divider below the open section.

Drawer · view

Same header grammar as the inspector, inside a right sheet.

Steps · routes only

For route wizards such as Batch upload. Completed steps are clickable; upcoming ones are not. Never inside a drawer.

DetailGrid · hint, info, description

Region
us-east
Sticky IP
On· one dedicated US IPFixed at creation
Block ads
Off
Skip media
Off
Shared use
One run at a time
Timeout
min
Advanced tracing
Close popups
Needs Block ads
Profiles
2 profiles
Shared with 3 other workflows on this environment.

Hint: a few words on the value, or why a control is disabled. Info: what the field means, behind the ?. Description: the rare line a user must read. A boolean shows its state dot before the key. A control replaces the value through trailing; units sit beside it.

StatusBadge · status map

QueuedRunningNeeds reviewCompletedFailedCancelledretrying later

One map per domain. Only live states carry a glyph. Unknown values render neutral, humanised.

Empty · size sm

No schedules yet
Schedules run this workflow on a timer.

Concept icons

add
edit
delete
close
more
copy
externalLink
search
filter
settings
back
disclosure
dropdown
drag
refresh
download
upload
run
stop
history
show
hide
locked
help
info
warning
error
success
done
loading

Import from @workspace/ui/lib/icon-concepts. Entity glyphs (for example profile) live in the platform entity registry.

Entity drawer: Logins

The complete R5 drawer for one entity, built like the Logins page in the product. Create, view and edit share four numbered sections. Edit in the header edits them all behind one save bar. The address bar shows the URL each mode writes.

/logins
2 profiles
NameSecretsSticky IPUsed by
Billing portal · US2On4
Claims desk · UK1Off1

Modes and URLs

ModeURLTry it
Create?new=profile&section=…Create a login profile. Numbered sections. A Next pill on the divider below the open section opens the next one. Open any section directly. No Review step.
Validate—Clear the name and open another section: Details turns red. Create opens the first section with a problem.
View?id=…&section=…Click a row. Every section reads as a summary. One section is open at a time.
Edit…&edit=1Edit in the header turns every section into its form. One save bar at the bottom.
Instant section—Secrets act at once, in view and edit. New secret slides in over the list and slides back when done.
Create-only field—Sticky IP is set at create. Afterwards its section shows it read-only with the reason.
Destroy—Delete lives in the header menu, hidden while editing. It confirms with an alert dialog over the drawer.

Same sections, every mode

#SectionSaves
1Details / OverviewDraft
2SecretsInstant
3Email inboxDraft
4Sticky IPCreate-only; read-only after

Surfaces in practice

The rules applied to real entities. Each frame is a working slice of the platform built only from the primitives above. Sheets, dialogs and alert dialogs open over the whole page, as they do in the product.

R4 Docked panelR3 Inline editR5 Drawer from elsewhere

Select a node; the panel follows. Rename it in the header and watch the canvas. The folder button swaps the panel body to files. New environment opens the environment drawer in create mode and selects the result.

Invoice reconciler · builder
Browser agent

What the agent does on this step.

Dialogs

How to build a Dialog once the rules chose one (R1 confirms, R2 show once, R6 tasks). One shell, four sizes set by content: sm 0–1 input, md 2–8 fields, lg 9–16 fields in 2–4 sections or tabs, full files, diff and debugger only. See DESIGN.md §Modals.

Alert dialog · R1

sm · 448

Destructive, irreversible or costly. Title asks the question, body states the blast radius, the action names the verb. Cancel keeps the thing.

Task · R6

md · 672

A one-screen task that is not the page's job. 2–8 fields, one section, no scroll. FormField runs the body; Cancel + verb in the footer.

Tabbed task · R6

lg · 896

An R6 task with 2–4 sections: header, persistent identity strip, chrome tabs, body, footer. Tasks only. Entities open in their drawer (R5), never in a tabbed dialog.

List-pick

md · 672

Search + scroll + multi-select inside a DialogBody. The toolbar pins to the top, the list scrolls, the count summary lives in the footer.

Loading + error

md · 672

Same shell, two states. Loading uses skeleton inside the body. Blocking errors are an in-body banner — never a toast.

Right-action + split footer

lg · 896

The Run workflow shape: Pre-fill Select sits in the header right-action slot; Schedule and Advanced cluster left in the footer; Cancel + Run cluster right.

Size by content

  • sm · 448px. 0–1 input. Show-once values, one choice with a sentence.
  • md · 672px. 2–8 fields, one section, no tabs, no scroll.
  • lg · 896px. 9–16 fields in 2–4 sections or tabs, or a list-pick with preview.
  • full. Files, diff, debugger. Nothing else.

Anything else (sm:max-w-[600px], sm:max-w-5xl, custom max-h-[Nvh]) is a smell.

Stat cards

Compact metric tiles — label + icon, value, helper. Used on /overview and /executions and reserved for future summary strips (billing, usage, scheduled). One row, 3 or 4 tiles, top of page.

Anatomy

Executions
2,840
2,738 completed · 68 failed · 34 canceled
label
sentence case · text-[11px] font-medium text-muted-foreground
icon
size-3.5 · neutral by default; one status accent allowed
value
text-xl font-semibold tabular-nums · always tabular so values align across the strip
helper
breakdown / context · text-xs text-muted-foreground · color words, not the whole tile
shell
gap-1 border py-3 shadow-none · flat, no offset shadow — tiles are inert

Strip — 4 tiles · /overview

Executions
2,840
2,738 completed · 68 failed
In progress
7
3 held for review
Outcomes
booked 412held 38no-stock 14
Workflows
12
3 updated today · 5 saves

Strip — 3 tiles · /executions

Executions
412
412 completed
In progress
0
nothing live
Outcomes
no outcomes yet

Loading — full strip skeleton

Future use — billing strip (same shell, different content)

Spend
$1,284.40
this period · 62%of cap
Plan
Scale
renews 14 May
Top workflow
Insurance Scraper
$612.18 · 48% of spend

Same shell, swap the helper. Numeric metric → number value + breakdown helper. Set of related items → no value, badge children. Trend tile → same shell, badge as the helper (see kitchen-sink dashboard below).

Entity cards

How a domain entity (Profile, Integration, Notification, API Key, Schedule, …) renders anywhere in the platform. The same identity — icon, name, type, meta — at three densities: full on index pages, compact in side panels and drawers, and chip as an inline reference with a popover. Detail lives in a vertical stack of named body sections — and only those sections — so two cards for different entity types still read as members of the same family.

Anatomy — full variant

  • Icon tile — neutral muted square. The only place a brand color or product logo is allowed in the header.
  • Overline (EntityCardType) — small uppercased mono label sitting above the title. The icon already telegraphs the type via shape; the overline is the belt-and-braces text label.
  • Title —text-base font-semibold, truncating. The dominant element of the header now that identity info is split across overline + title only.
  • Meta (optional) — supplemental identity line below the title. Only used when the entity has a non-audit fact more identifying than its name (e.g. Schedule's cron description). Most entities skip this entirely.
  • Actions — right-aligned dropdown trigger.
  • Body — a vertical stack (space-y-4) of named section primitives. This card uses PropertyList + FilterRow.
  • Footer — thin top border + small muted line at the bottom. All audit lives here: EntityCardTimestamps with showCreated renders Created always and Edited when changed (60s threshold). Empty footers don't render at all.

Variants — full / compact / chip (hover the chip)

Full

Compact

Chip + popover

Full

Compact

Chip + popover

Body grammar & footer — six body sections plus the audit footer

Every full or popover body is a stack of named sections. Picking from this set is mandatory; ad-hoc divs in an entity body are out. Two densities of PropertyList for key/value, four cluster sections for everything else.

PropertyList density="grid"

URL
https://webhook.site/dddddddd
Version
v2
Headers
0

PropertyList density="inline"

version
v2
headers
0
url
webhook.site/ddd…

PillRow

Features
Captcha SolverPersistent IPCacheAd BlockerPopup BlockerPopups as TabsMedia Blocker

DividedList

Secrets3
REPLY••••••••
Login
MFA Token••••••••
2FA
API_KEY••••••••
Secret

DividedList (editable, with actionSlot)

Secrets3
REPLY••••••••
Login
MFA Token••••••••
2FA
API_KEY••••••••
Secret

FilterRow (with overflow)

Monitoring:Execution CompletedExecution FailedExecution Started

EmptyRow

Secrets0
No secrets attached

EntityCardFooter (default)

Edited >60s after create — meta already shows Created.

Edited

EntityCardFooter (showCreated)

Custom-meta entity (e.g. Schedule) — footer carries Created too.

EditedCreated

Composed example — three sections in one body

The same shell with a stacked body. Sections sit at space-y-4; headers and content within a section sit at space-y-2.

Chip references — hover or click to expand

Chips are entity references inside other content (table cells, log lines, toasts, breadcrumbs). Identity only — icon, type, name. The popover shows the entity's compact card plus an allow-listed subset of body sections, and a footer link that opens the entity in a new tab.

Linked to:

Page shells

Two underlying surface modes — canvas(used by Plain, Hero, and Start) and chrome(used by Tabbed and Detail) — produce the platform page shapes. Canvas pages get a bg-light-muted well with bg-card surfaces lifting off it. Chrome pages drop the canvas and let a bg-sidebar strip + tabs sit directly above a single bg-background content panel — the active tab is the only seam between chrome and content. Sidebar carries the page label; the title slot is reserved for the active tab (Tabbed/Detail), the editorial composition (Hero/Start), or nothing at all (Plain).

Anatomy — canvas mode vs chrome mode

Canvas mode · Plain & Hero

Overview
API keys
Billing
New key

Three surfaces: chrome → canvas → cards. Page bar is transparent on the canvas, optional, slim. Cards lift off the canvas via border + ~1.5% lightness delta.

Chrome mode · Tabbed & Detail

Overview
Logins
Integrations
Profiles
Profile groups
Secrets
Requests
Help
+ Create

Two surfaces: chrome → content panel. Tabs sit onbg-sidebar; the active tab is the only seam between chrome and content. Actions live inside the tab content because they're tab-scoped.

chrome
bg-sidebar · the page frame · used by sidebar AND chrome-mode page strips
canvas
bg-light-muted · the canvas-mode page surface · cards lift off it via border + ~1.5% lightness delta
content
bg-background /bg-card · the white surface · used by chrome-mode panels and by every card on canvas
tabs
ride on the chrome strip · TabsListoverride!bg-transparent !px-0 so the strip itself is the bar (no nested-bar effect)
actions
on canvas pages: in the slim page bar · on chrome pages: inside each TabsContent (they're tab-scoped)
title
drop when tabs or hero present · sidebar carries the label
subtitle
deprecated · use InfoCardat top of content (canvas) or top of TabsContent (chrome)

The core shell shapes

A · Plain

Top-level routes with one purpose · sidebar carries the label

Overview
Workflows
API keys
Billing
New key
NameCreated
prod-api-key
staging-api-key
ci-runner

No title. The sidebar already says "API Keys" — repeating it in a header strip is wasted real estate. Page bar carries only the primary action; everything else is the content card.

B · Tabbed

Chrome strip + tabs · no canvas layer · tab is the only seam

Overview
Workflows
Logins
Integrations
Profiles
Profile groups
Secrets
Requests
Help
+ Create

What are profiles?

Who your agents sign in as. A profile holds the secrets for one or more platforms, shown by their keys, plus an inbox for email codes.

No canvas layer. The page chrome (bg-sidebar) extends up over the tabs; the active tab is the only seam between chrome and the white content panel below. Help toggle + Create button move into a tab-scoped action row at the top of each tab content — they swap with the active tab anyway. Strip stays pure navigation.

C · Hero

Dashboard / summary routes · editorial moment replaces the title

Overview
Workflows
Executions
Billing

Mon morning · 14 Apr

A quiet weekend — everything held.

8 workflows ran 2,840 executions while you were away.

Runs

2,840

Success

96.4%

Avg dur

3m 42s

Active

8

No page bar at all. The editorial hero is the first thing you see — a serif headline that adapts to state, mono overline for context, Inter copy with live numbers. Stat strip and content cards stack below.

D · Detail

Resource pages · custom header carries the resource name

Overview
Workflows
Executions
Logins
A
Insurance Scraper
Builder
Executions
92890146
Run
Builder canvas (flat — no card padding)

Custom resource header. The page bar grows up onto the chrome (bg-sidebar) and carries: workflow selector (resource title) → ChromeTabs (sticky views + open documents) → primary action (Run). Detail pages may opt into surface="flat" when the canvas IS the application (Builder, execution canvas).

Title rule

Page has…Title slotSubtitle slotExamples
TabsDrop. Tabs are the identity.Drop. UseInfoCard inside tab content if needed.Logins, Admin → Models / Usage
Editorial heroDrop. Hero replaces it.Drop. Hero supporting copy carries it.Overview
Start compositionDrop. Split editorial prompt replaces it.Drop. Primary composer and rail copy carry the context.Create / get started
Custom resource headerResource name · dynamic contentDrop.Workflow, Execution, Batch detail pages
Plain top-level route, no tabsOptional. Prefer dropping — sidebar already says it.Optional. Treat as a smell.API keys, Integrations, Manage users

Tabs — pick the API by semantic, not by look

Tabs (Radix)

Internal sub-sections

Self-contained. The user can't open new ones, drag them, or close them. Internal state owns the active tab.

  • Logins (Profiles | Profile groups | Secrets | Requests)
  • Admin → Models / Usage / Users
  • Settings sections

ChromeTabs

Documents / routes

Browser-like. Tabs are openable, draggable, closable. Outer state owns the list.

  • Workflow header (Builder + Executions + open execution tabs)
  • Anywhere the user is expected to manage the tab list itself

Both render the same chrome-tab visual — same surface, same shoulder, same inverted corners on the active tab. The split is about what the tabs mean, not how they look.

Create / Start Surface

The Start page shape: a canvas-first decision surface for creating a workflow, onboarding a user, or choosing a first template. One editorial composition may span the primary composer and the supporting template rail, but everything below returns to compact Inter UI.

Canonical ingredients: composer first, primary-tinted interactive controls, neutral/card surfaces, compact learning cards, and a collapsible supporting rail. The rail dimensions are composition constraints, not global spacing tokens.

Build a new agent...

Describe a repeatable workflow, then let Astro help turn it into a reliable browser or computer-use agent.

Astro

Find every Bay Area dental practice accepting new patients, check appointment availability, and summarize the best options.

or start blank

Learn Asteroid

Get the most out of Asteroid

2/4
01Build first agent
02Run a test execution
03Add credentials

Pro tip

Start broad

Describe the job in plain English first. Astro can help split it into faster scripted steps.

Empty states

Say what is missing and what to do. Page scale on an empty page or tab;size="sm" inside tables, cards and panels. Filtered-to-nothing offers to clear filters.

No executions yet
Run a workflow to see its executions here.
No schedules yet
Schedules run this workflow on a timer.
No executions match these filters
Status: Failed · Last 24 hours

Skeletons

Loading chrome that mirrors the final layout. Bars use bg-muted and a 1.5s opacity pulse. Never a single tall blob. Never primary-tinted placeholders. See DESIGN.md §Skeletons.

Primitive. bg-muted, 1.5s opacity pulse

Option card. Same shell as OptionCard

Data table. Card, muted header, compact rows, pagination footer

Loading

Collection page. Action row, launcher doors, table

Loading
Loading

Kitchen Sink — Workflow Dashboard

A realistic dashboard combining all components

Workflow executions

Monitor and manage your workflow runs

Monday morning · 14 Apr 2026

A quiet weekend — everything held.

Eight workflows ran through2,840 executionswhile you were away. Three flagged themselves for review, one finished early.

Total Runs

2,840
12%vs last week

Success Rate

96.4%
1.2%vs last week

Avg Duration

3m 42s
8%faster

Active workflows

8
of 12 deployed
ExecutionWorkflowStatusDurationStartedActions
exec_a1b2c3
Insurance Scraper
Completed2m 14s
exec_d4e5f6
Form Filler v3
Running1m 08s
exec_g7h8i9
Data Extractor
Failed4m 52s
exec_j0k1l2
QA Automation
Completed5m 33s
Showing 1–4 of 2,840 executions
1

Filter · Failed · last 24h

status=failed 24h
ExecutionWorkflowStatusDurationStartedActions

Nothing failed yesterday.

Every one of the 412 runs in the last 24 hours completed clean. You should take the rest of the afternoon off.

Kitchen Sink — Execution Activity

A realistic execution progress feed with collapsible turns, actions, user messages, and chat input

Execution Activity
Running
exec_f8a2c1d9

Kitchen Sink — Astro Chat

The AI builder assistant conversation with messages, function calls, reasoning blocks, and rich input

Astro
Improve login flow reliability
12.4k chars

Time & Duration

Single rule, applied everywhere a moment-in-time is shown. Always relative on screen, single-letter compact, monospace, tabular-nums. Hover for the absolute moment in the user's timezone plus UTC.

See DESIGN.md §Time & Durationfor the convention table and rationale.

Relative time · past

now
5 seconds ago
5 minutes ago
3 hours ago
2 days ago
3 weeks ago
4 months ago
2 years ago

Relative time · future

in 5 minutes
in 2 days
in 3 months

Absolute time

short
long

Tooltip contract — local-with-offset + UTC, mirrored on every time component:

4 hours agoFri, Oct 09, 2026, 19:47:18 UTC2026-10-09 19:47:18 UTC

Duration

42 s42s
3760 s · 1h 02m 40s1h 02m 40s
7245 s · 2h 00m 45s2h 00m 45s
precision="minute"1h 02m
live ticker · started 7 s ago7s
ms scale · 120 ms120ms

Prefix prop · one phrase, one font

Pass a `prefix` to render a label inline with the time, in the same mono coordinate font. The whole phrase reads as one unit and the screen-reader label includes the prefix automatically. Prefer this over writing the prefix as plain text next to the component.

CreatedEditedStartedNext runJoined

Do · Don't

Do

  • Mono, tabular-nums, single-letter compact:
  • Months usemo(neverm):
  • Use theprefixprop so prefix + value share one font:Edited
  • Hover any timestamp to confirm tooltip shows local-with-offset + UTC.

Don't

  • Render time in proportional sans:5 min ago
  • Mix fonts on the prefix:Edited 12m ago(use theprefixprop)
  • Drop theagosuffix:5m(no tense, reads as a duration)
  • Usemfor 4 months ago:4m ago— that's literally 4 minutes ago
  • Skip the tooltip — every visible time must expose the absolute moment on hover.

Truncated text

Single-line labels that can grow arbitrarily long — workflow names, profile names, integration names, file names, schedule labels — get the same treatment everywhere: cap the visible width, ellipsize, and mount a tooltip with the full label only when the visible text is actually clipped. The conditional tooltip is the whole point — a tooltip on an already-visible label is noise.

Use <TruncatedText> from @workspace/ui/components/truncated-text for the standard case. Drop down to useIsTruncated from @workspace/ui/hooks/use-is-truncated only when you need to control the tooltip's open state externally (e.g. suppress it while a sibling popover is open). See DESIGN.md §Tables → Truncated text for the full rules.

Standalone · single width cap

10 charsReturns QA
37 charsRefund window verification — backfill
94 charsReplatform launch QA · checkout · returns · loyalty · billing · all locales · regression sweep

Tooltip mounts only on the third row — the first two fit inside their cap. Hover any row to confirm; tooltips on already-visible labels are noise.

Inside a TableLink with trailing affordance

Pair with min-w-0 flex-1 on the text and shrink-0 on the trailing icon so the affordance always renders, even when the name is clipped.

Inside a table cell — the canonical use

ExecutionAgentNotes
exec_1234Returns QAWidth is set by the column, not by the label.
exec_1235Refund window verification — backfillWidth is set by the column, not by the label.

The third row's tooltip carries the full label without forcing the column wider. Resize the page narrower — the second row eventually starts truncating too, and its tooltip mounts the moment the measurement crosses over.