tanghoong design
  1. Design system
  2. Components

04 — Components

Components

The working parts. Every one is plain HTML plus a class — no framework, no JavaScript unless the behaviour genuinely requires it.

Buttons

Five intents. Exactly one --filled per view — it is the primary action, and two primaries means neither is. Every variant clears 44 px; that is a touch-target requirement, not a style preference.

Intent
<button class="btn btn--filled" type="button">Filled</button>
<button class="btn btn--tinted" type="button">Tinted</button>
<button class="btn btn--quiet"  type="button">Quiet</button>
<button class="btn btn--outline" type="button">Outline</button>
<button class="btn btn--danger" type="button">Danger</button>

Always set type on a button inside a form — the default is submit, and an unlabelled one will submit the form.

Size and shape

An icon-only button must carry aria-label; the SVG is aria-hidden. .iconbtn is the bordered header/toolbar variant.

Inline link with a nudging arrow
<a class="link-more" href="/work">See all work
  <svg width="16" height="16" …><path d="M5 12h14m-6-7 7 7-7 7"/></svg>
</a>

Chips and tags

A chip is a passive label — a topic, a stack item, a filter value. It carries no state. If it is clickable, make it an <a> or <button> and it picks up the hover treatment.

Chips
Production Integration Applied AI Outline Accent Clickable
<div class="chips">
  <span class="chip">Production</span>
  <span class="chip chip--outline">Outline</span>
  <span class="chip chip--accent">Accent</span>
</div>

On a sunken section chips would disappear, so the CSS lifts them to --bg-elevated automatically inside .s-sunken, .card and .panel.

Badges

A badge carries state: status, severity, a count. Unlike a chip, its colour is meaningful — which is exactly why it must always be paired with a word. Colour alone fails for a colour-blind reader and for anyone on a monochrome display.

Badges
Neutral Live Pending Failed Beta 12
<span class="badge badge--ok">
  <span class="badge__dot" aria-hidden="true"></span>Live
</span>

Cards

An elevated surface for a self-contained item. When the whole card is a link, make the card itself the <a> — one hit target, one hover state, one tab stop. A card that is not a link should not lift on hover.

Card grid
<div class="cards cards--3">
  <a class="card" href="/work/bid-pipeline">
    <span class="card__title">Realtime bid pipeline</span>
    <span class="card__desc">Cut p95 from 240 ms to 38 ms…</span>
    <span class="card__status">Delivered · 2024</span>
    <span class="card__foot">
      <span class="chips"><span class="chip">AdTech</span></span>
      <svg class="card__arrow" …></svg>
    </span>
  </a>
</div>

.card__foot uses margin-block-start:auto, so a row of cards with uneven copy still lines its footers up.

Panels

The card's quieter sibling: a well that groups related content. No hover, no lift, no destination. Use it for settings groups, summaries and sidebars.

Panel
Deployment Healthy

Two regions, blue/green, with automatic rollback on a p95 regression above 20%.

Elevated variant

Use .panel--elevated when the panel sits on a sunken section and needs to lift instead of recede.

Notes and alerts

An inline message. The leading rule carries the intent colour and the wash tints the background — but the words still say what happened.

Intents
NeutralContext that is worth knowing but changes nothing.
InformationThe markdown twin of this page lives at /index.md.
SuccessDeployed to production. Two regions are serving traffic.
CautionThis key expires in 14 days. Rotate it before the next release.

Put role="alert" on a note that appears in response to something the user just did. Do not put it on a note that was on the page at load — it will be announced for no reason.

Stats

Figures set in tabular numerals so digits line up in a column, with an uppercase label beneath. The figure is the headline; the label explains it.

Stat row

12.4k

Peak req/s

Sustained, single region

38 ms

p95 latency

Down from 240 ms

40+

Pipelines

In production

15 yrs

Delivery

APAC

Row lists

Records separated by hairlines. Denser than cards and the right choice once you have more than about six items — a long grid of cards becomes hard to scan.

Definition lists

Key/value metadata for a case study, a spec sheet or a profile. Uppercase keys, regular values, in a sunken well.

Facts
Role
Forward deployed engineer, sole implementer
Duration
11 weeks, discovery to production
Stack
Go, Postgres, Cloudflare Workers
Status
Delivered and merged

Tables

Always wrap a table in .table-wrap. On a phone the wrapper scrolls sideways; the page body never should. Numeric columns get .num for right alignment and tabular figures.

Table
Latency by region, p95, last 30 days
RegionStatusp50p95Requests
SingaporeLive12 ms38 ms4,204,110
Kuala LumpurLive14 ms41 ms2,880,004
SydneyDegraded28 ms190 ms612,900
FrankfurtStandby0
<div class="table-wrap">
  <table class="table">
    <caption>Latency by region</caption>
    <thead><tr><th>Region</th><th class="num">p95</th></tr></thead>
    <tbody><tr><td>Singapore</td><td class="num">38 ms</td></tr></tbody>
  </table>
</div>

Add .table--compact for dense data views. Keep the <caption> — it is the table's accessible name.

Lists

Unordered, ordered, numbered steps

Plain lists

  • Markers use --text-3 so they sit behind the text
  • Siblings are spaced with --s-2
  • Nested lists inherit the same rhythm
    • Second level
    • Still readable
  1. Discovery
  2. Thin vertical slice
  3. Production hardening

Numbered steps

  1. Frame the problem

    Write the failure mode down before writing code.

  2. Ship a thin slice

    One real path, end to end, into production.

  3. Instrument, then widen

    Measure before adding the second path.

Tabs

Real tabs need arrow-key navigation and aria-controls wiring. If you only need to switch views, links to separate pages are simpler and more robust.

Breadcrumb
  1. Home
  2. Work
  3. Realtime bid pipeline
Avatar and meter
CT CT CT
Migration72%
Divider and empty state

or

No results

Nothing matched that filter. Try widening the date range.