Quiet, dense, and square.
Built from parts we don't maintain.
This is how Acua's product UI is put together. It is not a component library — it is a set of decisions about which off-the-shelf parts to use and how to use them, plus rules for the few parts we made ourselves. The reference bar is Linear-level precision, Attio-level density, Vercel-level restraint.
Principles
Five decisions that settle most arguments before they start.
- 01Ink does the work
One accent: near-black. Color is reserved for status and data — never for decoration, never for hierarchy.
- 02Structure over chrome
Hierarchy comes from type, spacing and 1px borders. If a card can be a border, it's a border. If a border can be whitespace, it's whitespace.
- 03Dense but calm
Operators live here for hours. Compact rows, small controls, restrained copy — and enough air that nothing shouts.
- 04The table is the product
Most screens are a list being worked. Toolbar, columns, alignment and footer are first-class design surfaces.
- 05Buy boring, own little
Use maintained open parts unmodified. Every component we own ourselves is a liability with a named justification.
Use the shelf
We have no designers and no appetite for a bespoke library. For each need, this table names the part we use — as-is. Restyling happens through tokens, not forks.
| Need | Use | Notes |
|---|---|---|
| Buttons, inputs, dialogs, dropdowns, tabs, tooltips… | shadcn/ui | Generated into the repo; styled only via tokens (radius 0, ink primary). |
| Data tables | TanStack Table | shadcn is the visual layer; TanStack is the engine (sorting, visibility, selection). |
| Charts | bklit (visx) | The registry source lives in components/charts. No other chart library. |
| Command palette | cmdk | One global ⌘K menu; labels reuse the sidebar's i18n strings. |
| Icons | lucide | 16px in controls and nav. One library, never mixed. |
| Type | Geist + Geist Mono | Mono for document numbers, amounts, codes. |
| Motion | framer-motion | Only where listed in section 10. |
The rule: before writing any component, check the shelf. If shadcn has it, use it and move on. Our own parts are listed in section 12 — everything else is somebody else's maintenance problem.
Color
Neutral surfaces, ink actions, color only where it means something. The old brand navy (#142860) and blue-as-accent are retired.
Neutrals — the whole interface
- Canvas white · subtle panels gray-50 · borders gray-200
- Text gray-900 · secondary gray-500 · disabled gray-400
- Primary actions gray-900, hover gray-700
Status — the only permitted color
Status renders as colored text with a small glyph — no filled chips, no loud pills. Strong red is reserved for destructive actions and real errors. Blue survives only as an informational tone and link color in prose.
Chart ramp — data only
Seven hues at near-constant lightness and chroma, so series carry equal weight; a series keeps its hue across every chart type. chart-neutral is what “Other” is drawn in. The ramp never appears in UI chrome.
Typography
Geist for everything; Geist Mono wherever a human compares digits. Type — not color, not boxes — carries the hierarchy.
- ·Right-align money; always tabular figures
- ·One semibold weight per screen region
- ·Sentence case everywhere (see Writing)
- ·Bold as a substitute for structure
- ·Uppercase label headers (legacy)
- ·Fluid type tricks in data screens
Shape & depth
Radius zero, everywhere. The only curves are circles.
border-radius: 0 on every control, panel, dialog and chart bar. rounded-full is the single exception — avatars, status dots, count pills.
Depth is 1px of gray-200. Shadows exist only on floating layers (menus, dialogs, tooltips) and even there stay barely visible. Nothing else floats.
Spacing & density
A 4px grid, and control heights chosen for hours of daily use.
| Element | Size | Rule |
|---|---|---|
| Toolbar controls | h-8 | Search, filters, icon buttons in list toolbars |
| Form controls & primary actions | h-9 | Buttons, inputs, selects in forms and page headers |
| Table rows | ~52px | Two-line cells (name + muted meta) allowed; three never |
| Page gutter | 24–32px | One consistent gutter per page; no nested cards |
| Section gap | 40–56px | Sections separated by whitespace or a single border |
Components
Everything below is shadcn/ui, unmodified — these examples only show which variant to pick and how it lands with our tokens.
Buttons
useButton — variant="solid" color="primary" | outline | ghost | destructive
One ink-solid primary per view. Everything else is outline or ghost. Green/blue solids no longer exist — approve, confirm and pay are primary actions, so they are ink.
Inputs & pickers
useInput · Select · Combobox (searchable) · DatePicker — labels above, never placeholder-only
Dropdown menu
Rendered open for reference — this is shadcn DropdownMenu with system tokens.
useDropdownMenu — destructive items last, separated; icons optional at 16px
Tabs
useTabs (underline variant) — counts as quiet gray chips, not colored badges
Status & badges
useStatusPill (ours, see 12) for document status · Badge soft tints for annotations
Toolbar
Search sits left; view controls are icon-only with tooltips and sit together at the right edge.
useToolbarIconButton (ours) — active state = ink fill + count dot; label lives in the tooltip
Overlays
Short, focused interactions: confirm, name something, pick one thing.
Document detail (bill, PO, receipt): covers the viewport in ~200ms; Esc closes; content keeps list context in a left rail.
⌘K — navigation and create actions from anywhere.
Side drawers are retired for document detail — a bill deserves the whole screen. Modals never stack more than one level.
Tables
The main workspace. Curated default columns; everything else one click away behind the columns picker.
| Bill | Vendor | Total amount | Due date | Status |
|---|---|---|---|---|
| INV-2026-4581 | Sukhumvit Print House Co., Ltd. Head office | 9,630.00 THB | 17/09/2026 | Ready for review |
| INV-2026-4562 | Rayong Industrial Parts Co., Ltd. Head office | 38,841.00 THB | 08/10/2026 | Pending approval |
| INV-2026-4549 | Chao Phraya Logistics Co., Ltd. Head office | 75,756.00 THB | 21/08/2026 | Paid |
Anatomy: quiet headers · vendor cell with muted meta line · right-aligned mono amounts · status as colored text · footer with count and sum.
- ·6–9 default columns; the rest opt-in via the picker
- ·Doc numbers in ink, underline on hover
- ·Skeleton rows while loading — chrome never blanks
- ·Amount columns hidden by default (ever)
- ·Full-page spinners over a table
- ·Uniform-width bars as loading state
Charts
bklit (visx) primitives from components/charts — the same source Compass uses. Restraint over decoration.
- Square bars (lineCap 0), quiet 1px grid, muted axis labels.
- Fills are flat ramp tokens. Gradients only on a single-series area.
- Tooltips list series rows with a total last; center of a donut answers the question the ring asks.
- Money ticks compact (150K); full amounts only in tooltips.
- ~650ms enter animation, then still. Nothing pulses.
Motion
Animation exists to explain a spatial change, then get out of the way.
Each stage runs the real curve and duration. Replay them — the point of these numbers is how they feel, not what they read like.
Selection, hover and state changes snap. Nothing to wait for.
| Surface | Motion | Duration |
|---|---|---|
| Menus, tooltips, popovers | fade + 2% scale | 120–150ms |
| Full-screen takeover | fade + slight rise | 200ms |
| Chart enter | grow once on mount | ~650ms |
| Skeletons | gentle pulse | continuous |
| Everything else | none | — |
Writing
Sentence case, everywhere: headers, buttons, labels, tabs, menus. Acronyms and product names keep their capitals.
| Write | Not |
|---|---|
| Total amount | Total Amount |
| Create purchase order | Create Purchase Order |
| Linked PO · Linked GRs | linked po |
| Bill Pay (product name) | Bill pay |
| Mark as paid | MARK AS PAID |
Buttons say what happens (“Upload bill”, “Send reminder”) in three words or fewer. Empty states are one sentence and one action — no illustrations that outweigh the data.
Our own parts
The complete list of components we own. Each exists because the shelf had nothing — and each carries its rules here. Adding to this list requires the same justification.
Document status as glyph + colored text. No fill, no border. One glyph per status, ever.
Icon-only toolbar trigger with tooltip label; engaged state fills ink and shows an applied-count dot.
Loading rows that keep table chrome in place; widths vary per row so it reads as content.
The ⌘K palette. Destinations reuse sidebar i18n keys so the two never drift.
Money in tables: right-aligned, tabular, converted amount stacked muted underneath.
Vendored registry source (shared with Compass). Edited like our own code; chrome pointed at semantic tokens.
Business-critical flows behind these surfaces (bill lifecycle, payment guards) are additionally modeled and proved in Lean — see verification/ in the docs repo. Design changes may not weaken a proved invariant.