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
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
- 200 OK · ok-700 on ok-50
- 400 · danger-700 on danger-50
- deprecated · warn-700
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.
| Dark theme pairing | ratio |
|---|---|
| ink-700 body on surface | 11.3 |
| ink-900 headings on surface | 15.7 |
| ink-500 secondary on surface / on ink-50 | 7.0 / 6.5 |
| brand-600 text on surface | 6.0 |
| surface text on brand-600 (primary button) | 6.0 |
| brand-800 logo on surface | 9.2 |
| ok-700 / danger-700 / warn-700 on their tints | 8.2 / 7.1 / 8.0 |
| code-dim on code-bg | 5.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
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.
text-h1 · 32/1.15 · -.02emtext-h2 · 22/1.3text-lg · 17/1.55text-base · 15/1.65text-sm · 13/1.55text-meta · 12/1.5text-micro · 11 · .07emComponents
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.
Button — one shape, four intents
Navigation row — section, family, current, unbuilt
Menu row — plain and selected
Design note — pin and card
Contents rail — group, entry, current
Badge — state on a parameter
Parameter row
Editable value pill — locked and removable
Status strip
Response row — collapsed, expanded, hover
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.
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-ctlrounded-cardBuilt in, not added after
- Visible focus on every interactive element, defined once in
@layer baseso nothing can opt out. - The response tree is buttons and
aria-expanded, so it is keyboard-operable and announced — a JSON viewer built from divs is a wall to a screen reader. - Status uses
role="status", so a result that arrives 600ms later is announced rather than silently appearing. - Text with colour in every state; contrast checked on the tint each pairing actually sits on.
prefers-reduced-motionrespected globally.
Concept by Umanga Shrestha · back to the interactive docs