Figma kept lying to me. So I moved the source of truth into code.
Every Pinnlo screen I drew in Figma looked good on the canvas and fell apart in the browser. Spacing drifted, states were missing, hover behaviours were implicit. I stopped trusting Figma as the source of truth and built a design system in HTML instead — every component rendered, interactive, and ready to copy into the real product.
Components across buttons, cards, inputs, surfaces, and nav — each with states.
Categories: colour, type, space, motion, components, patterns.
Source of truth. The HTML is the spec, the prototype, and the export — all at once.
Buttons
3 variants · ripple hover.btn {
position: relative;
min-width: 97px;
min-height: 44px;
padding: 0 16px;
border-radius: 6px;
font-family: 'Space Grotesk', sans-serif;
font-size: 12px;
font-weight: 500;
overflow: hidden;
}
.btn .ripple-grid {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(5, 1fr);
pointer-events: none;
}
@keyframes cellRipple {
0% { background: rgba(251,146,60,0.5); }
40% { background: rgba(244,114,182,0.4); }
70% { background: rgba(147,197,253,0.3); }
100%{ background: transparent; }
}Three variants with a 10×5 ripple grid that radiates Orange → Pink → Blue from the cursor on hover.
Strategy Card
190×254 · ripple gridStrategy Name
An app for strategy…
.strategy-card {
width: 190px;
height: 254px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
border: 1px solid rgba(0,0,0,0.08);
overflow: hidden;
position: relative;
}
.content-inner {
position: absolute;
inset: 8px;
background: rgba(255,255,255,0.85);
border-radius: 8px;
}
.grid-bg {
background-image:
linear-gradient(rgba(0,0,0,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,0,0,0.03) 1px, transparent 1px);
background-size: 16px 16px;
}The primary card — grid background behind a frosted inset, with the signature ripple hover animating through the cells.
3D Box Loader
8 boxes · isometric · pure CSS.loader-3d {
width: 200px;
height: 320px;
transform-style: preserve-3d;
}
.loader-box div {
width: 48px;
height: 48px;
transform: rotateY(-47deg) rotateX(-15deg)
rotateZ(15deg) scale(0);
}
.box0 div, .box3 div, .box6 div { background: var(--orange); }
.box1 div, .box4 div, .box7 div { background: var(--pink); }
.box2 div, .box5 div { background: var(--blue); }Eight boxes fly in from offscreen, stack in isometric perspective, then drop through a lit ground plane. 3s cycle, zero JavaScript.
From Figma to code, step by step.
- 01Audited the UI surface
Screenshotted every Pinnlo screen. Counted buttons, cards, inputs. Most were subtly different — that was the problem to solve.
- 02Explored in Figma first
Set colour, type, and spacing tokens as variables. Built component variants. Stress-tested states, density, dark mode.
- 03Translated each component into HTML
Took the Figma spec and rebuilt it in raw HTML + CSS. No framework. Tokens became CSS custom properties.
- 04Interactive states in the browser, not Figma
Hover, focus, active, disabled, loading — written once in CSS and reused. Figma can't show these honestly; code can.
- 05Ported components into React
Each HTML prototype became a React component in Pinnlo. Same tokens, same names, same behaviour.
- 06Kept the HTML library as the reference
When something drifts in production, I open the HTML library and copy it again. It's the canonical version.
HTML is a better Figma than Figma.
Figma is a drawing tool pretending to be a spec. Browsers are a spec pretending to be nothing. When the design system lives in HTML, there's no translation layer and no drift: the prototype is the spec, the spec is the demo, and the demo is what ships. Tokens defined once in CSS custom properties propagate through every surface — light mode, dark mode, Pinnlo brand, client brand — without any rebuild.
Building a design system taught me to design less.
Most of the job wasn't designing components — it was deleting them. Every near-duplicate button, card, or input was a decision I'd failed to make earlier. Pruning the system forced me to commit to one answer per problem.
Tokens are the most important thing in a design system, and they're invisible. Naming them well — --surface-muted, not--grey-200 — is what lets the system outlive the brand it was built for.
The best proof a system works is how fast you forget it's there. By month three, I was shipping Pinnlo features without thinking about components at all — which is exactly the point.