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.