/ design system ← Back to the concept

Foundations

The design system

Every value the concept renders comes from tailwind.config.js. There is no raw hex and no one-off pixel value in the markup, so the whole surface re-themes from one file — the same file this would use in the Rails app.

Values are sampled from searchapi.io rather than invented. The point of the concept is that it could ship into the existing product on a Tuesday, not that it looks like somebody else's product.

Colour

4 ramps · 2 themes

Named by role, not by hue. ink-500 is secondary text; calling it gray-500 would leave the next person guessing which grey to use where.

Ink — text, borders, surfaces

Brand — one accent, used sparingly

Semantic — status only

Each pairing is checked against its own tint rather than against white, because that is where it actually appears. Status is never colour alone — every state in the concept carries a word as well.

Two themes, one set of names

Every token above is a CSS variable with a light value and a dark value; nothing in the markup says which. The swatches on this page are the current theme — switch it with the button in the header, or under Prototype for the three-way choice. Dark keeps the brand hue and inverts the neutral ramp, so ink-900 is always "the most text-like" and ink-50 always "the most page-like". Two things flip with it: the primary button becomes dark-on-pale-indigo (it says text-surface, not text-white), and the code card sits a step lighter than the page with a hairline edge, because in a dark UI the raised thing is the lighter one. The default is the OS setting, remembered once changed.

Contrast ratios in the dark theme
Dark theme pairingratio
ink-700 body on surface11.3
ink-900 headings on surface15.7
ink-500 secondary on surface / on ink-507.0 / 6.5
brand-600 text on surface6.0
surface text on brand-600 (primary button)6.0
brand-800 logo on surface9.2
ok-700 / danger-700 / warn-700 on their tints8.2 / 7.1 / 8.0
code-dim on code-bg5.4

Every text node on the rendered page was swept against its effective background in both themes; the lowest text pairing is 4.5 or better in each. The sweep is what caught text-white on the pale dark-theme brand — 2.6:1 — before it shipped.

Type

7 steps

A scale, not a bag of sizes. Anything that does not fit one of these steps is a design question, not a CSS question. The current docs set section headings at 18px/600, close enough to body text that a section boundary is hard to see while scrolling — h2 here is 22px with a rule under it.

Page titletext-h1 · 32/1.15 · -.02em
Sectiontext-h2 · 22/1.3
Lede paragraphtext-lg · 17/1.55
Body — parameter descriptionstext-base · 15/1.65
Supporting — help text, hintstext-sm · 13/1.55
Meta — counts, statustext-meta · 12/1.5
Eyebrow labeltext-micro · 11 · .07em

Components

would be ViewComponents

In the Rails app each of these is a ViewComponent holding its own utilities. Static HTML has no component layer, so the patterns that repeat are named once in src/css/application.css — thirty-one classes — rather than pasted at every call site. Everything below is the real class, not a copy of it: change the class and this page changes with the product.

Ten of the thirty-one are not shown here because they carry behaviour rather than appearance — how a focus ring composes on a control made of parts, a scrollbar that only appears on hover, the colour inversion a selected search row needs, the console's docked position. They are documented in the stylesheet, beside the rule.

Button — one shape, four intents

34px, 8px radius, 13px, one padding. Only colour carries intent: primary is the one action the page exists for, default is everything you can also do, accent is a link that is really a button, quiet is barely a control. Loading keeps the label — a button that becomes a bare spinner loses the thing it was about to do.

Navigation row — section, family, current, unbuilt

Google Search Google Maps Searchsoon

An engine this concept has not built is dimmed and says so on hover, rather than being a link that goes nowhere. On a touch device, where there is no hover to ask with, the label is always shown.

Menu row — plain and selected

Design note — pin and card

4
One language, not 132 Today each of the 132 code blocks carries its own language tabs, and the choice is forgotten by the next block.

One paragraph, beside the thing it argues about, with a caret pointing back at its pin. Below 640px it becomes a sheet at the foot of the screen, because there is no room beside anything.

Contents rail — group, entry, current

One continuous rail drawn by the list, not by each link: every entry used to draw its own 2px border and the margin above a group left a hole in it, so the line read as a broken dash and the marker appeared to jump rather than move. A group's spacing is padding, not margin, so the marker starts where the rail does.

Badge — state on a parameter

Required Optional Deprecated Enterprise only

Parameter row

location Optional

Editable value pill — locked and removable

q=chatgpt &gl=us×

Required parameters are tinted and carry no remove control — the affordance is not offered rather than offered and refused.

Status strip

200 OK|0.02s|stored|7 top-level keys
400 Bad Request|0.41s|Missing required parameter

Failure is a designed state, not an afterthought. Docs that only ever show success teach half the API.

Response row — collapsed, expanded, hover

"search_metadata":{ 9 keys }
"organic_results":[ 4 items ]
"title":"Introducing ChatGPT"

Icons

One 16×16 grid, one 1.5 stroke, round caps and joins, currentColor, one render size. Before the set existed they were drawn where they were needed: five stroke widths, three grids and six sizes across twenty-one inline SVGs — which nobody reads as a bug, only as the drawing being slightly off everywhere. A shape that scales while its stroke does not gets optically lighter, so a second size would need a second weight; one size instead. play is the exception: a solid triangle, because its weight is area and an outlined play glyph on a filled button reads as a different control.

The sheet below is rendered from src/js/icons.js — the same module the app draws from — so it cannot drift from what ships.

Space and radius

Tailwind's 4px scale, unchanged — inventing a spacing scale is a way of making a codebase harder to join. Two radius tokens only: rounded-ctl (8px, their chip radius, shared by every control so buttons and inputs always agree) and rounded-card (12px).

rounded-ctl
rounded-card

Built in, not added after

Concept by Umanga Shrestha · back to the interactive docs