DESIGN_PRINCIPLES.md — human.work
The design decisions behind the platform UI. Read this before adding a screen. Cross-reference: GLOSSARY.md, INFORMATION_ARCHITECTURE.md, VIEWS_BY_ROLE.md, COMPONENT_INVENTORY.md,
frontend/src/app/globals.css. Last updated: 2026-05-04.
1. Light/dark mode strategy
Both modes are first-class. Light is the default.
- Light mode (Intercom-inspired): white surfaces, warm gray canvas. The platform's home aesthetic — what new users see.
- Dark mode (Linear-inspired): warm near-black, lighter desaturated claret accent. The aesthetic for power users who live in the queue.
Mechanism
[data-theme="dark"] on <html> flips the entire token system. No .dark class proliferation. Set by DarkModeProvider, which:
- Reads
localStorage.hw_themeon mount. - Falls back to
prefers-color-scheme: darkif not stored. - Updates the attribute and persists choice on toggle.
Token system (truncated — see globals.css)
:root { /* light mode */
--surface-0: #FFFFFF; /* cards / panels / elevated */
--surface-1: #F7F8FA; /* page canvas */
--surface-2: #F0F1F5; /* sidebar */
--surface-3: #E8E9EE; /* hover */
--text-0: #111827;
--text-2: #6B7280;
--border-0: #E5E7EB;
--accent-0: #7E1D1D; /* claret */
}
[data-theme="dark"] {
--surface-0: #1C1C1E; /* elevated */
--surface-1: #0F0F10; /* canvas */
--surface-2: #161618; /* sidebar */
--surface-3: #242426; /* hover */
--text-0: #F5F5F5;
--text-2: #8E8E93;
--border-0: #2C2C2E;
--accent-0: #C94B4B; /* lighter claret — readable on dark */
}
Then semantic aliases sit on top: --bg-canvas, --bg-surface, --bg-sidebar, --bg-hover, --text-primary, --text-secondary, --text-muted, --border, --accent, --accent-subtle, --accent-hover, --accent-fg. Components consume the aliases, never the raw scale. This way a future palette change touches one block in globals.css.
Key rule
- Hex colors don't appear in components. Every color is a token. Exception: brand SVG icons that need a literal brand color (e.g. Slack purple) — but even then, prefer brand tokens defined once.
- Risk badges and status pills have their own theme-aware token pairs (
--risk-critical-bg/text, etc.). They flip automatically. Noif (theme === 'dark')checks in components.
2. Color usage rules
Claret — used sparingly
The accent (#7E1D1D light / #C94B4B dark) is the brand color. It is reserved for:
- Primary CTAs — "Send", "Send Code", "+ New Thread", "Approve", "Send invite".
- Active navigation — left border on sidebar items, bottom border on top-nav tabs, active text color.
- Specialist persona avatars — the workspace identity (slug avatar, Specialist persona, user avatars where the user belongs to that workspace).
- Selected-state affordances — selected ticket row, selected thread, focus ring (3px
--accent-subtlehalo). - Branding tile — the H logo square.
It is not used for:
- Generic icons (use
--text-muted). - Body text (use
--text-primary). - Generic borders (use
--border). - Errors (use
--danger). - Hover affordances on neutral elements (use
--bg-hover).
A useful rule: if you can replace the claret with a neutral and the meaning is preserved, you should. Reserve the color for moments where it carries semantic weight.
Status semantics
| Token | Use |
|---|---|
--success (green) | Connected channels, resolved tickets, "Online now", high SLA |
--warning (amber) | Trial banner, medium-risk SLA, pending agentic step |
--danger (red) | Critical risk badge, broken channel, destructive action affirmation |
--info (blue) | "AI" handling badge, "You" assignee pill |
--teal-0 | "Assigned" pill (other expert), low-risk badge |
Risk levels are not red-amber-yellow-green:
- critical → danger (red)
- high → warning (orange/amber)
- medium → warning (lighter)
- low → teal (intentionally not green; green is reserved for "good" states like online/connected, and a green "low risk" badge would mistakenly read as positive when it's actually about uncertainty)
Light vs dark variation
The accent shifts (#7E1D1D → #C94B4B) for legibility on dark surfaces. Status colors also shift slightly:
| Status | Light | Dark |
|---|---|---|
| success | #16A34A | #22C55E |
| warning | #D97706 | #F59E0B |
| danger | #DC2626 | #EF4444 |
Designers don't think about this — tokens handle it.
3. Typography
Font stack — do not change
Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif
Mono: 'Geist Mono', 'JetBrains Mono', ui-monospace, monospace (used for code, IDs, audit log entries).
Sizes (px)
| Size | Use |
|---|---|
| 10 | Section labels (uppercase 0.08em tracking 700 weight), tiny status badges |
| 11 | Metadata (timestamps, captions), button labels, footer links |
| 12 | Secondary body, badges, sidebar nav, inputs in dense panels |
| 13 | Default body, primary table rows, primary nav |
| 14 | Message bubbles (chat), card titles, primary inputs in forms |
| 16–18 | Section headings (h2/h3) |
| 20–24 | Page titles (h1), stat values |
Default body is 13px. This is intentionally compact — power users (experts especially) value information density. Chat messages bump to 14px because conversation tone benefits from a more comfortable reading size.
Weights
- 400 — body text
- 500 — sidebar items, semi-emphasized rows
- 600 — headings, labels, primary nav active state
- 700 — uppercase section labels, big numbers (24px stat values), badges
Line height
- Body: 1.5
- Bubbles: 1.55 (slightly more breathing room)
- Headings: 1.2–1.3
Tabular numerals
Use font-variant-numeric: tabular-nums for any column of numbers (queue counts, timestamps, SLA percentages). Prevents column wobble.
4. Component patterns
Cards
--bg-surfacebackground,--border1px border, 12px border-radius (Linear/Intercom convention).- 16–20px internal padding for compact cards, 24–28px for hero cards.
- Shadows: avoid by default. Use only for floating elements (modals, popovers).
Tables
- 13px body,
--text-primary. - Header row:
--bg-surface+ 11/600 uppercase--text-mutedlabels +--borderbottom. - Row hover:
--bg-hover. - Row selected:
--accent-subtle+--accentleft bar (3px). - Sticky header on scroll.
Inputs
--bg-surfacebackground,--border1px border, 8px border-radius (slightly tighter than cards).- Focus:
--accentborder + 3px--accent-subtlehalo (box-shadow). - 14px font for primary inputs, 13px for dense.
- 10/12 vertical padding × 12/14 horizontal.
Buttons
- See
ButtoninCOMPONENT_INVENTORY.mdfor variants. - Heights: sm 28 / md 36 / lg 44. Touch targets ≥44px for any tap-able primary action.
- Primary always uses
--accentbackground; never use--successor--infoas a button background.
Badges
- 11px / 600 weight / uppercase / 0.04em tracking / 4px radius.
- Each variant pairs
--{semantic}-subtlebg with--{semantic}`` text. Never raw hex.
Bubbles (chat)
- 14px body / 1.55 line-height / 9px vertical 13px horizontal padding / 18px radius with directional 4px tail.
- Outgoing (user/customer):
--accentbg + white text. Tail in bottom-right (18px 18px 4px 18px). - Incoming (Specialist/Expert):
--bg-surfacebg + 1px border +--text-primary. Tail in top-left (4px 18px 18px 18px). - Internal note: dashed amber border + 🔒 icon, never visible to clients (gated by
EXPERT_ROLES).
Active nav
Two patterns, depending on orientation:
| Layout | Active state |
|---|---|
| Vertical sidebar (AM, Superadmin, Portal admin, Portal chat threads) | 3px --accent left border + --accent text + --accent-subtle bg |
| Horizontal top tabs (Workspace, status tabs in queue, Org tabs in queue) | 2px --accent bottom border + --accent text |
These are the only two active-nav patterns. Don't invent a third.
Empty states
- Centered,
--text-mutedcolor, soft tone. - Eyebrow label (10/700 uppercase muted) + h2 (20/600 primary) + caption (13/muted) + primary CTA.
- For the chat empty state: a single Specialist card with green online dot — the entire visual weight is on the next action, not on the absence of content.
Loading states
- Skeleton placeholders matching real content shape — never spinners on content surfaces.
- Spinner only on buttons during async actions.
Modals
- Centered,
--bg-surface, 12px radius, 1px border, 24–32px padding. - Backdrop:
rgba(0,0,0,0.5)light /rgba(0,0,0,0.7)dark. - Close icon top-right (16px, muted, hover lifts).
- Primary action right, secondary left.
5. Voice and copy guidelines
These come from GLOSSARY.md and from product values. They apply to UI copy, error messages, empty states, and email templates.
The platform name
- Always lowercase:
human.work— including at the start of sentences ("human.work helps you…"). - The dot is part of the name. Do not write "human work" or "Human Work".
Words to use
- "humans" — when referring to the people on the team. Not "users", not "members" (unless specifically referring to org membership).
- "workspace" — user-facing term for an org. Use this in UI, URLs (
acme.human.work), emails. "Org" is internal/code-only. - "Specialist" (capitalized as a role/persona name like "Bob is your Specialist") — the named client-facing identity.
- "Expert" — internal humans behind the Specialist. Never shown to clients.
- "AI Agent" / "agent" — automated component. Internal-only language. Clients see "AI" badge inline; they never see the word "agent" in copy.
- "Send" — the verb for outbound messages, not "submit".
- "Resolve" — the verb for closing a ticket, not "close".
- "Defer" — for snoozing (24h default), not "snooze".
Words to avoid
- ❌ "User" — replace with "human", "team member", or the specific role.
- ❌ "Submit" — replace with "Send" or the specific action verb ("Save", "Invite", "Resolve").
- ❌ "Ticket" in client-facing copy where conversation works ("thread" is fine in chat sidebar, "ticket" is okay for trackable work items).
- ❌ Capitalized "User", "Customer" in body copy. Exception: official entity names (Specialist, Expert).
- ❌ Marketing speak: "amazing", "powerful", "revolutionary". This is a B2B SaaS — the audience knows what's amazing.
Tone
- Direct, calm, helpful. Treat the reader as a capable colleague.
- Short sentences. Three short ones beat one compound sentence.
- No emoji in product UI. Reserve emoji for status chips that already have semantic meaning (✓ for confirmation, 🔒 for internal note). Never decorative.
- Errors describe what went wrong + what to do next. "We couldn't reach the agent service. Please try again in a moment." — not "Error: 500 Internal Server Error".
- Copy is sentence case. Never Title Case for body text or buttons. Exceptions: page titles, official names.
Inclusive defaults
- Singular "they" by default. No gendered pronouns in templates.
- No assumptions about the reader's role or business size.
Examples
| ❌ Don't | ✅ Do |
|---|---|
| "Submit Form" | "Save changes" |
| "Welcome User!" | "Welcome to your workspace" |
| "Click here to message your AI Agent" | "Message Bob to get started" |
| "An Error Occurred" | "We couldn't load your threads. Try refreshing." |
| "Successfully Sent!" | "Sent" (in a toast — the toast color carries the semantics) |
6. What NOT to do
These are anti-patterns we have explicitly rejected. If a design proposal contains any of these, push back.
❌ Red on black
The previous design used #7E1D1D claret on #0A0A0B near-black backgrounds. This reads as harsh, amateur, and visually fatiguing. Dark mode now uses warm #0F0F10 with the lighter #C94B4B accent — calibrated for contrast and comfort.
❌ Inline hex colors in components
Every color goes through a token. If you need a new color, define it in globals.css first and then reference the alias. A grep for #[0-9a-f]{6} in TSX should turn up zero matches in component code (only in globals.css).
❌ All-caps headlines or labels in body copy
Reserve uppercase for section eyebrows (10px, 700 weight, 0.08em tracking) and badges/pills. Page titles, button labels, and body text are sentence case.
❌ Serifs in product UI
The product is Inter end-to-end. No Georgia, Garamond, or other serifs in dashboards, chat, settings, etc. Marketing pages are a different surface and may use serif display fonts where appropriate.
❌ Multiple primary buttons in one view
One primary CTA per screen (or per section). If there are competing actions, the secondary action gets the secondary variant. The eye should know where to land.
❌ Modal-trapped destructive actions
Destructive actions (delete workspace, archive org, remove member) should:
- Use the danger variant explicitly.
- Require typed confirmation for irreversible actions ("type ACME to confirm").
- Never auto-focus the destructive button.
❌ Confidence scores in client UI
Clients never see numeric confidence (e.g. "82%"). They see handling — "AI" or "Human review" badge. Confidence is operator data; revealing it dilutes the Specialist persona model and creates a false expectation of opaqueness control.
❌ Exposing the team
Clients never see:
- The word "Expert" in attribution (only "Specialist name" or the specialist's persona).
- The internal note panel.
- The AI agent's name (e.g. Felix). They see "AI" if it's relevant.
- Multiple senders in a single thread (other than themselves and the Specialist).
If a client asks "who answered this?" the answer is always "Bob" (the Specialist).
❌ Yellow/amber for Debug Mode panels
Make development affordances (debug mode, demo login panels) subtle — --surface-3 background, single-pixel separators, low chroma. They shouldn't shout for attention or be confused with warnings.
❌ Random border-radius
Stick to the scale: 4px (badges), 8px (inputs, small buttons), 12px (cards), 18px (chat bubbles), 50% (avatars). No border-radius: 7px ad-hoc.
❌ Mixed icon styles
We use line-style icons (Lucide-like, 1.5–2px stroke, currentColor) throughout. No mixing line and filled. Exception: the H logo tile is filled by design.
❌ Toasts with raw Error objects
toast.error(err instanceof Error ? err : "Friendly fallback") — never let raw stack traces or "Internal Server Error" reach users. The error is logged separately.
7. Accessibility baselines
- All interactive elements have ≥44×44px touch targets (use
.tap-targetutility). - Text contrast ≥4.5:1 for body, ≥3:1 for large text. Verify both light and dark modes.
- Focus ring visible on every focusable element —
--accent2px outline + 2px offset (set globally on:focus-visible). - Form labels are programmatic —
<label>for inputs,aria-labelfor icon-only buttons. - Color is never the only signal — risk badges have text labels; status pills do too.
- Animations respect
prefers-reduced-motion(skip slide/pulse if requested).
8. Performance baselines
- Default body 13px is faster to scan and lets us pack more meaningful content above the fold.
- Skeletons over spinners — perceived speed.
- Polling (queue, chat) at 10–15s intervals; WebSocket pushes when available.
- No layout shift after data load — reserve space (placeholder cards, fixed-height rows).
9. When in doubt
- Reach for tokens, not values. If you're typing a hex, stop and check whether a token already exists or should be added.
- Prefer existing components over new ones. If you need a card, use the same pattern other cards use. Variation should be intentional.
- Match the role's mental model. A client wants conversation; an expert wants a queue; an AM wants a dashboard. Don't import the wrong metaphor.
- Cut, then cut again. The more you remove, the better B2B SaaS gets. If a label, divider, or icon doesn't earn its place, delete it.
Owner: Design + Platform team