Case Study · Design Systems & Accessibility

SurfaLux — design system & website build

Taking a beautiful but structureless Figma comp file and turning it into a build file: a tokenized, accessible, responsive design system with 190 variables, 32 component sets, and 27 page frames across three breakpoints — with the developer handoff spec written as it went. Brand direction by the studio 9045andco.

Live — surfalux.com
Client

SurfaLux™ — glass coating technology

My role

Icons & imagery · page layout · carousels · design system · accessibility · responsive build · handoff

Studio

9045andco — brand direction & initial comps

Deliverables

Tokens · components · 3 breakpoints · dev handoff

Overview

A comp file is not a build file.

SurfaLux makes a hydrophobic coating for architectural glass — the kind of product whose proof is in a photograph of water refusing to stick. 9045andco set the brand direction and initial comps ahead of final client content, deliberately, so the structure could be built while copy was still being written.

What arrived was six page frames of visual direction — and no reusable objects of any kind. Every page was absolutely-positioned rectangles and text. My scope ran from the visible craft — the icon set, image generation and editing, page layout and the carousels — down to everything underneath: the token architecture, the component library, the accessibility decisions, the responsive system, and the handoff document a developer could actually build from.

The brief asked for six things — header and footer, an accordion FAQ, a data-capture widget, button and type styles, content sections, and “an overarching design system, responsive and accessible from the outset.” None of them existed as objects when I started. All of them do now.

Iconography & Imagery

None of it arrived. It was drawn and made.

Before any of the system work, there was the work you can see. The comps carried brand direction, a few flattened reference icons and placeholder art — no icon library, and no photography that could survive being blown up to full-bleed width. The icon set and the imagery on this site were produced for this build, alongside the layout and the carousels.

The SurfaLux feature icon set — line icons in dark green circles, each labelled: Durable, Clean, Limescale, Professional, Eco-friendly, No maintenance, PFAS-free, Time saving, glass types, Water repellent, ROI, 54M+ sq ft, Trusted worldwide, Established 2014, EMPA approved.
Icons / Feature. One stroke weight, one circle, one optical size — so a row of them reads as a row, not as a collection of unrelated drawings. The set now runs to seventeen in the library, and it grew as the copy did: each feature the page claims has an icon that means it.

One ink value. Four modes.

Icons are not placed loose on the page. They sit inside an Icon Callout component, and the component takes its colour from a collection that holds exactly one variable — ink — with four modes. Set the mode on a section and every icon inside it recolours at once. Nobody has to remember which green a callout is supposed to be on a dark band, because nobody is choosing it.

White
on photography
Black
on light surfaces
Alpine Green
#2F4A3A
Light Green
#6B9F7F
The icon set rendered in white ink for use over photography.
The same components, White mode. The drawing does not change; the ink does.

Generated to match a photographer’s work.

The brand photography was shot by 9045andco’s photographer — the hero, the comparison, the machine. What that shoot did not cover was the applications themselves: the five surfaces the coating actually sells into. The SurfaLux applications carousel is mine end to end — shower doors, windows, glass railings, solar panels and car windshields, every one of the five slides generated with AI, then graded and composed to sit beside the photography without announcing itself. So is the professional-application band further down the coating page. The SurfaLine machine pages are photographed throughout: that hardware is real, and it was shot.

Matching is the whole job. Same low-key lighting, same cool glass against warm interior, same depth of field, same restraint about how much beading is on the surface. The test was never whether a generated image looked impressive on its own — it was whether it looked like it came off the same shoot. An image that reads as a different library is worse than no image at all.

A modern house with large glass walls at dusk, warm interior visible through the glass.
WindowsArchitectural glazing — the carousel slide that has to make a homeowner recognise their own house. Warm interior against cool glass, shot at the hour the glass does the most work.
A walk-in glass shower enclosure in a modern bathroom.
Shower doorsThe highest-volume application, and the one the FAQ spends the most words on. Built clean and uncluttered so the glass is the subject rather than the bathroom.
A glass balcony railing on a timber deck overlooking a garden, water beading on the coated half.
Glass railingsExterior glass, where the coating’s argument is maintenance rather than appearance. Built as a split frame — coated and uncoated in one shot — so the claim is visible without a caption.
A solar panel in daylight, tight beads of water on the coated half and sheeted water with debris on the uncoated half.
Solar panelsThe only daylight scene in the set, because the argument here is dirt rather than gloss. Coated and uncoated in one frame: tight beads on one side, sheeted water and debris on the other.
Beaded water on a car windshield.
Car windshieldsAutomotive — a second market on the same coating. Droplet behaviour matched to the supplied hero so the range reads as one product.
The Professional Application section — a dark stone bathroom with a frameless glass shower, headline and copy set on the dark left third of the image.
Professional applicationThe section as it ships. The scene was built to fall away into darkness on the left, so the headline and paragraph sit straight on the image — no scrim, no black box behind the type.

Every image ships at the size it is used at. The carousel card renders at 1232×599 on desktop and 688×578 stacked — separate files, not one photograph scaled by the browser. The alternative is a full-resolution render being downloaded to fill a card, on a site whose audience is often standing on a job site.

The Audit

Count it before you claim it.

Before proposing anything I audited all six frames and every loose object on the canvas, and reported the measured state rather than an impression of it. These are counts read from the live file, not estimates:

0

Variables / design tokens

0

Published color, text or effect styles

0

Components or component sets

0

Frames using auto-layout

1

Breakpoint — a single 1436px width

The audit was written to be useful rather than damning. Designing ahead of content is a legitimate choice and the visual language was consistent enough to tokenize cleanly — three typefaces, five colors, a small set of genuinely repeating patterns. A small, well-behaved system, not a sprawl. But it also surfaced fifteen structural findings that would each get more expensive the longer they sat:

FindingWhat it actually wasCost if left
Header duplicated 6×Five loose text nodes, a logo and a pill rectangle on every page — never grouped, let alone componentized. One nav label change meant six edits.High
Footer duplicated 6×, inconsistentlyGrouped on five pages, entirely loose on the sixth. Same picture, two structures — a developer builds it twice.High
The FAQ accordion wasn't oneA static picture of an accordion: all five questions in a single text node, spaced with blank lines. No item structure, no states, no question–answer pairing.Rebuild
The data-capture widget didn't existRequired by the brief. Not present in any state — and neither was the warranty form, despite a page named for it, nor a contact form.Blocking
Six images in three slotsDuplicate image nodes stacked 1px apart. Whichever sits on top wins; someone exports the wrong one.Medium
Eight different left edges83 · 88 · 92 · 104 · 123 · 139 · 140 · 141px, where there should be one container and one gutter. Three of them are indistinguishable drift.Medium
A transparent accidentrgba(217,217,217,0) on every outline button — Figma's default gray dropped to 0% alpha. It renders correctly and means nothing.Low
Seven of fifteen findings, ranked by what they'd cost to leave alone.

The highest-risk item wasn't structural — it was legal. Two of the three typefaces are licensed for desktop use only. A desktop licence lets you design with a font; serving it on a public site ships the file to every visitor and needs a separate webfont licence. Neither blocks design. Both block launch — and finding it late would mean re-setting every heading and label in the system.

My Scope

From comp to build file.

01

Audit & archive

02

Token foundations

03

Component library

04

Responsive build

05

Handoff spec

The original comps were duplicated into an archive page before anything was touched, so the studio always had the untouched version to compare against. Everything after that was additive.

Foundations

190 variables, in five layers.

Tokens only pay off if they're layered. Raw values live at the bottom and are hidden from every picker in the file, so a designer physically cannot reach for a hex — they have to reach for a meaning. If the meaning is missing, the answer is to add the token, not to work around it.

01

Primitives

75

Green ramp anchored on two real brand values. Neutrals, feedback ramps, spacing, radius.

hidden from pickers
02

Semantic

69

surface · text · border · action · feedback · overlay. Every pairing contrast-checked before it was created.

modes: Dark · Light
03

Typography

30

3 families, 4 styles, an 11-step size scale, 9 line-heights, 3 tracking steps.

modes: Desktop · Tablet · Mobile
04

Breakpoints

15

6 widths, container max, four gutters, column counts, max measure.

drives responsive behaviour
05

Icon Ink

1

One variable, ink, carrying the four approved icon colours. Set the mode on a section and every icon inside it follows.

modes: White · Black · Alpine Green · Light Green
Each layer references the one before it. Components bind to Semantic only — never to Primitives, never to a literal.

Two findings the ramp produced. The green ramp was built mathematically around Alpine Green as step 700. When the studio later added a second brand green independently, the derived 400 step landed within one value in two channels of it — a useful signal that the ramp's maths matched the brand's intent. It was snapped to the exact brand value.

The second was less flattering and more useful: the documented type ramp specified a display weight the site does not use. A scan of the live pages found 18 of 19 licensed-font headings set in 45 Light and exactly one in the documented 35 Thin — and no token existed for the weight actually in use, so nothing could have been bound to it. The specimen and the pages now agree. They didn't before, and nobody had checked.

Colour

Six values, and one of them was an accident.

The brand palette is small and strong. The work was giving each value a defined role — a colour doing three unrelated jobs isn't a token yet, it's a coincidence.

Alpine Green

#2F4A3A · green/700

Primary CTA fill and section surface. White on it measures 9.7:1 — AAA.

Light Green

#6B9F7F · green/400

Secondary CTA fill. Black label required — see the contrast table below.

Gray

#EEEEEE · neutral/100

Light surface. Anchors the neutral ramp and drives the Light mode.

Metallic Silver

#8A8D8F · PMS 877 C

A metallic spot ink screens can't reproduce. Flagged, unbound, awaiting a decision.

Four named brand values. The fifth was Figma's default gray at 0% alpha.
Accessibility · WCAG 2.1 AA

Contrast decides the label colour. Not preference.

The brief asked for accessible from the outset, so every semantic pairing was measured before it became a token rather than audited afterwards. The core palette turned out to be genuinely strong — three of the four primary pairings clear AAA.

One pairing did not, and it mattered: the new CTA green. White on it is 3.05:1 and fails AA at the 14px button label size. Black on the same fill is 6.89:1 and passes at any size. So black labels are required — not preferred. The hover and active states also step lighter rather than darker, because the next step down fails against a black label. That's the kind of rule that gets quietly "fixed" by someone darkening a hover six months later, so it's written down in the file, not just in my head.

PairingRatioResult
White on Black21:1AAA
White on Alpine Green9.7:1AAA
Black on Gray18.1:1AAA
Black on Light Green6.89:1AA — any size
White on Light Green3.05:1Fails AA at 14px
Control border on raised surface3.89:1 dark / 4.30:1 lightClears 1.4.11
Metallic Silver as body text on white3.34:1Fails AA
Measured pairings. Every semantic token was checked before it was created, not after.
◉

Focus states, everywhere

Nothing in the comps showed keyboard focus. Every interactive component now carries the same 2px focus ring — button, all three input types, checkbox — so focus is one visual language, not five. 2.4.7

⊞

Targets sized for hands

Accordion toggles were 15×15px. Carousel dots were bare 16px circles. Both now sit inside hit frames that clear the minimum, and checkbox targets are the full 44px row rather than the box. 2.5.8

⚠

Error states that exist

The comps had no error, success, focus or disabled colour at all — on a site with two forms. A full feedback token set now drives an error summary banner plus per-field messages, each prefixed with a glyph so colour isn't the only signal. 1.4.1 · 3.3.1 · 3.3.3

↕

Reflow, decided in advance

When a multi-column section stops fitting it stacks rather than squeezing — the threshold is any column falling below 320px. Written into the handoff so it isn't re-litigated per page. 1.4.10

▤

Labels that stay put

Field labels are persistent elements, never a placeholder standing in for one. Input values are set at 16px because iOS zooms the page on focus for anything smaller. 3.3.2

◐

The open one: hero legibility

Six full-bleed heroes put white text over photography with a 60%-opacity raster PNG as the only scrim — including one with bright snow directly behind the headline. Contrast there isn't measurable. Scrim tokens are built and ready; the treatment is the studio's call. 1.4.3

A worked example

One component. Ninety variants.

Button is the component every system gets wrong in the same way — by encoding content as variants and exploding the count into the thousands. Here the style, size and state axes are variants; label text, icon presence and icon choice are properties. Six styles × three sizes × five states is 90 variants, which is a perfectly reasonable number. Encoding the label as a variant would have made it unbounded.

DefaultHoverFocusDisabled
PrimaryCall to actionCall to actionCall to actionCall to action
SecondaryCall to actionCall to actionCall to actionCall to action
TertiaryCall to actionCall to actionCall to actionCall to action
QuaternaryCall to actionCall to actionCall to actionCall to action
OutlinedCall to actionCall to actionCall to actionCall to action
TextCall to actionCall to actionCall to actionCall to action
Six styles × three sizes × five states, redrawn from the live component set. Active is omitted here for width; the focus ring is identical on every style, and on every form control.

The CTA tokens are deliberately mode-independent — the same green on any surface. The previous arrangement flipped white and black by mode, which is exactly why a white button and a black button couldn't be chosen independently. Splitting them was a two-token change that removed a whole class of override.

Component Library

Thirty-two sets, built as objects.

Everything the brief asked for, plus everything the comps repeated — thirty-two component sets and five standalone components, 323 variants between them. Each one auto-layout, semantically bound, with its states designed up front rather than invented later:

▤

Header & Nav Panel

Replaced six hand-drawn copies with instances. The mobile pattern is a full-screen overlay rather than a slide-in drawer — there are only five destinations, and an overlay costs nothing to build and nothing to explain.

▦

Footer

Slot-based link columns, so the count can change without redrawing. Reworked mid-build: social squares out, the company column retired into a horizontal legal bar beside the copyright line.

⌄

Accordion

Rebuilt from nothing — the original was five questions in one text node. Real item structure, collapsed and expanded states, and a slot for the answer so it accepts any content.

⌸

Input family

Text, Textarea, Select and Checkbox — none of which existed. Seven states each on one axis, including a character counter that turns error-coloured over the limit and says how many characters to remove.

◎

Registration Form

The data-capture deliverable. Five fields, three states, and the success swap wired inside the component set — see below for why that distinction mattered.

⇄

Highlights carousel

Five slides, auto-advancing on a loop with smart-animate. It replaced a hand-built card row that had been sitting orphaned off-canvas — a real reusable pattern that never got placed into a page.

Input / Text — seven states on one axis, redrawn from the component set
you@email.com
We only use this to send your warranty benefits.Default
you@email.com
We only use this to send your warranty benefits.Hover — border steps up
you@email.com
We only use this to send your warranty benefits.Focus — 2px ring, same as Button
you@email.com
We only use this to send your warranty benefits.Filled
you@email.com
⚠ Enter a valid email address.Error — glyph, not colour alone
you@email.com
✓ Looks good.Success
you@email.com
We only use this to send your warranty benefits.Disabled
Redrawn in HTML from the live component set. The label is a persistent element, never a placeholder standing in for one, and the value sits at 16px because iOS zooms the page on focus for anything smaller.
The Data-Capture Deliverable

Wired once, not thirty times.

The brief called for a data-capture modal or widget. I argued for an on-page section instead, for two reasons: five fields fight a modal, and a modal can't be linked to from an email — which matters if the client wants to drive traffic straight to warranty registration.

The more instructive detail is where the interaction lives. Figma only lets a prototype navigate action target a frame on the same page, so the header's page-to-page links had to be wired as instance overrides — thirty of them, by hand, with every new page needing its own. The form's submit avoids that trap entirely: it's a state change inside the component set, so it works in every instance, on every page, at every breakpoint, wired exactly once.

Both states are pinned to identical dimensions, so the swap doesn't shift anything below it on the page. And the error state is built but deliberately unwired — documented rather than prototyped, so the treatment is unambiguous for the developer and nobody has to invent it later.

State = Form
Your full name
you@email.com
SL-0000-0000
MM / DD / YYYY
Dealer or fabricator name
* Required
Register
State = Success
✓ REGISTERED
Thank you — your warranty is registered.

We have sent a confirmation to the email address you provided. Keep it for your records — you will need it if you ever make a claim.

State = Error
Your full name
⚠ Enter your name.
you@email.com
⚠ Enter a valid email address.
SL-0000-0000
⚠ Enter the product code from your product label.
The three variants, redrawn from the component set. Form and Success are pinned to identical dimensions, so the swap moves nothing below it on the page.
Responsive

Nine pages. Three widths. Twenty-seven frames.

Responsive was built during the system, not retrofitted onto it — retrofitting is the single most expensive mistake available on a project like this. Every page exists at 1440, 768 and 375, with navigation wired at all three widths.

Type steps down through modes on the typography collection, which carry sizes only. Families, styles, line-heights and tracking are identical in all three, deliberately: the moment a family varies by mode, any context that can't load that font can no longer bind or edit the text.

TokenDesktopTabletMobile
font/size/4xl — hero headline85px48px34px
font/size/3xl — section heading55px38px28px
font/size/md — body20px20px20px
Container gutter104px40px24px
Body size holds across all three. Only the display steps carry mode values.

Named honestly: the tablet and mobile heading sizes are proportional estimates, derived from the content-width ratio and rounded — not measured. The licensed display face can't be loaded in the environment the build ran in, so line breaks at 768 and 375 have never been seen. Correcting one is a single value edit that every page inherits, so the fix is cheap. The risk is that nobody looks — which is why it's item four on the open list rather than a footnote.

1440
gutter 104 · 4xl 85px
768
gutter 40 · 4xl 48px
375
gutter 24 · 4xl 34px
Below the stack threshold every multi-column section collapses to one column rather than being squeezed. Nine pages exist at each of these three widths.
The Pages, Rebuilt

Same design. Different object underneath.

Every page was reassembled from components: each section auto-layout, every colour and gutter variable-bound, header, footer, button, accordion and carousel as instances. Zero overflow verified on all twenty-seven frames.

Pages whose copy is still unresolved open with an amber placeholder notice, and their layers are named — DUPLICATE, — PLACEHOLDER or — SLOT. That's a small discipline that does real work: it means nobody — client, studio or developer — mistakes a functional stand-in for an approved decision.

The SurfaLux homepage at desktop width, scrollable.
Homepage — layout & build by Anita Larkin from 9045andco's brand direction, assembled from components. Scroll inside the frame.
Handoff

Written as it was built, not after.

Handoff is the terminus of this project, and the build ran off it. But the first spec sheet was written the day the registration form was built, because the form's validation rules needed a written home immediately, not eventually.

That document sets the template for the rest: the error state as a visual reference, then five numbered blocks — fields and message copy, validation behaviour, submit behaviour, accessibility with the specific WCAG criteria cited, and the open questions the form still depends on. Naming what's unresolved is part of the spec, not an admission against it.

The handoff also ships a code-consumable token file alongside the Figma system — W3C format JSON plus generated CSS custom properties. It costs very little and removes the largest single source of handoff drift: a developer reading a hex off the inspect panel and typing one digit wrong.

◈

Component properties named like an API

label, variant, size, disabled — not "Text 1" or "Style A". Free alignment between the file and the code that consumes it.

✎

Annotations for the invisible

Focus order, form validation rules, what happens on submit — recorded in Dev Mode, where the developer will actually look for them.

⌗

Inspect returns token names

If the panel shows a raw hex, a binding is missing. That's the check, and it runs before handoff rather than after.

⚑

An open-questions list with owners

Eleven items, each with a named owner and whether it blocks. Font licences and the hero scrim decision sit at the top — both are design decisions with long tails.

What Diverged

The parts that didn't go to plan.

Three things behaved differently from how the build plan assumed, and each one is now a permanent property of the file rather than an outstanding task. Recording them is the difference between a system the next person can trust and one they quietly work around.

The planWhat actually happenedThe standing consequence
Wire navigation once, on the Header component Figma only permits a navigate action to target a frame on the same page. The component lives on one page, the site lives on three others. All navigation is instance-level. Every new page needs its header wired by hand. Permanent, not a workaround to revisit.
Bind every text node to typography variables, so a font swap is one edit The licensed faces can't be loaded in the build environment, and Figma refuses to bind or re-wrap text in a font it can't load. A binding sweep also wrote units that resolved wrong, damaging spacing system-wide. Reverted, with a snapshot taken first. Font families are not variable-bound. A font swap stays a manual pass — precisely the cost the plan existed to remove.
Build each breakpoint Cloning was the only method that carried the licensed display face through; retyping a heading silently substitutes it, and the substitution isn't obvious on screen. The three breakpoints are independent copies, not linked views. Nothing propagates — which is exactly why the content-entry method had to be decided before copy started landing.

The pattern worth naming: a fixed-height wrapper around a component instance hides breakage until the component changes size — which is precisely when you least want a silent failure. It cost 580px of dead space under one footer and 2,042px across a set of cloned pages before it was caught. Every wrapper added since hugs its content unless there's a stated reason not to.

Where It Stands

The system is done, and the site is built on it.

190

Variables across five collections

32

Component sets

27

Page frames, three breakpoints

0

Literal hex values in any component

Every one of those started at zero. The site is live at surfalux.com, built from them — and nothing left on the open list is blocked by a missing token or a missing component.

What the client gets out of it: copy arriving in pieces no longer costs a redraw, because auto-layout absorbs it. A changed decision — the green, the display face, the button treatment — propagates instead of being reapplied by hand across twenty-seven frames. And the next page starts at eighty percent rather than from a blank artboard.

Credits: Brand direction, initial comps and brand photography by 9045andco. Icons, image generation and editing, page layout, carousels, audit, token architecture, component library, accessibility specification, responsive build and developer handoff by Anita Larkin.

Next

Have a comp file that
needs to become a build file?

Start a project View the live site ← Back to all work