A design system is not a component library. It is the set of decisions that stop being decisions — so that the fortieth screen costs a tenth of what the first one did. This guide is the system we build on every Varritech engagement: tokens, extraction, type, components, motion, and the verification step that catches what a screenshot never will.
Inside this guide you'll find the token extraction method we run against a live site or a Figma export, the type rule that makes a layout read as one brand, the component depth test, the animation gotcha that makes a good figure look broken, and the DOM measurements we assert before any UI change ships.
Phase 1: Tokens — The Layer Every Other Decision Inherits
Phase 1 · Tokens
Everything above the bottom layer is a consequence
builds bottom-up · loops ~16s
05Screenshundreds of them
04Patternspage shells, forms, tables
03Componentsbutton, card, field
02Rolesground, surface, ink, line
01Tokensthe values, and their rules
The test that matters
Can a stranger build an on-brand screen from the token file alone, asking zero questions?
If no, you have a palette. A palette does not survive the fortieth screen.
Tokens are decisions that stopped being decisions. Layers 2 through 5 are cheap only when layer 1 is written down, named by role, and carries its usage rules.
Every design system has a bottom layer, and everything above it is a consequence. Get the bottom layer written down and named, and a new screen is an afternoon. Leave it in someone's head and every screen is a negotiation, every handoff is a guess, and six months later you have four blues and nobody can say which one is correct.
Tokens are that bottom layer. Not a style guide, not a mood board, not a Figma page called "Colors." A token set is a small file of named values that both the design and the code read from, and that a stranger can be handed as the answer to "what is this brand."
The test for whether you have one: can someone who has never seen the product build an on-brand screen from the token file alone, without asking anyone a question? If the answer is no, you have a palette, not a system.
What Actually Belongs in a Token Set
Ground and surface, not "background." The page ground, the card surface, the elevated surface. Three values, named by role. Teams that name colors by appearance — gray-100, off-white — end up with a dark mode that requires renaming everything.
One accent, and the rule for where it is allowed. The rule matters more than the hex. An accent with no usage rule becomes decoration, and decoration is how a brand stops meaning anything.
Ink, at two or three weights. Headline ink, body ink, faint ink. Not "black" — headline ink is rarely actually black, and the difference is most of what makes typography look considered.
A line color, expressed as an alpha over the ground.rgba(37,4,81,0.12) survives a ground change; #E5E5E5 does not.
Status colors, quarantined. Green, red, amber, teal — and an explicit statement that they are for status only, never decoration. This one line prevents the most common drift we see.
Type: family, weight, size, tracking, and line height per role. A family name alone is not a token. H1 48px / 900 / lh 110% / tracking -1% is.
Radius, spacing scale, and shadow. Three numbers each, at most. A twelve-step spacing scale is a scale nobody follows.
⚠️
The most expensive token mistake is not choosing a bad value — it is not writing down the usage rule. A gradient with no rule ends up as a page background. A status red with no rule ends up as a heading. We now write the constraint next to the value in the same file, because a value without its rule is not a token, it is a suggestion.
Case Study: ConvergeFlow — When the Numbers Are the Brand
Case study · ConvergeFlow
One rule does more brand work than the palette
same data, two type rules · loops ~12s
Numbers in the body face
Booked calls1,284
Reply rate7.4%
Sequences live36
Reads as generic SaaS
Numbers in JetBrains Mono 500
Booked calls1,284
Reply rate7.4%
Sequences live36
Reads as instrumentation
The rule Every numeral in the product is mono. Metrics, prices, codes, counts, timestamps.
The palette was not the distinctive part. Orange on near-black is common; a product where every number is set in a mono face is not. Rules travel further than values.
ConvergeFlow is a managed-outbound product for blue-collar SMBs. We authored its brand system, and the token set that came out of it is a good demonstration of how much work a single usage rule can do.
The type stack is three families with three jobs. Archivo for headlines and CTAs — H1 at 48px, weight 900, line height 110%, tracking -1%; buttons at 14px bold uppercase with +2% tracking. Chivo at 400 and 500 for body and UI, body at 16px with 150% line height. And then the rule that does the actual work:
Every number in the product is set in JetBrains Mono, 500. Metrics, table figures, prices, codes, counts, timestamps. Large metric at 36px, tracking -2%.
That one rule is what makes a ConvergeFlow screen recognisable at a glance. The palette is orange #E85002 with olive #707653 and sand #D9C3AB on a near-black #050505 — a good palette, but not a distinctive one; plenty of products use orange on dark. What no other product in the category does is put every single numeral in a mono face. A dashboard where the numbers are mono and the labels are not reads as instrumentation. The same dashboard with numbers in the body face reads as a generic SaaS table.
📐
The Engine Gradient, and why the rule is the token. ConvergeFlow's signature gradient runs sand to orange to deep red to black: #D9C3AB → #F16001 → #C10801 → #000000. The brand book restricts it to "primary interactions and major focal points" — in practice, the launch button and a cover surface. Written into the token file as a constraint, it stays a focal point. Written down as just four hex stops, it becomes a page background within a month, and then the launch button has nothing left to be.
Case Study: Varritech — Two Token Sets, One Company
Case study · Varritech
Two token sets, both correct, one scope line
alternates scope · loops ~14s
Brand book
#250451 ground · #99FF32 accent
Governs printed & branded documents
Site rebrand
#07040D ground · #80FF00 accent
Governs web, video, ads, anything matching the site
If ambiguous Ask which. Do not pick the prettier one, and never rebuild either from memory.
"Match the brand" is not an unambiguous instruction. Two deliverables were built off remembered hex codes and both were off-brand. The scope line costs one sentence and prevented every repeat.
Our own brand is the cleanest example of why a token set needs a declared scope.
Varritech has two legitimate, simultaneously correct token sets. The printed brand book specifies a Persian Indigo ground #250451 with Chartreuse #99FF32 — that is the system for branded documents, proposals, and PDFs. The website rebrand specifies a near-black ground #07040D with lime #80FF00, Halyard Display at weight 850 for headlines and Chakra Petch for body and micro-labels. Same company, same logo, two different grounds and two different greens.
Neither is wrong. What is wrong is treating "match the brand" as an unambiguous instruction. We have been burned by this twice in the same quarter, in two different media:
Asked to build an outro card "in the same style as the website rebrand," a first pass was built from the documented brand book — Persian Indigo ground, Chartreuse accent. Correct to a real document, off-brand for the request.
Asked to build a motion-graphics overlay kit "based off the Varritech site," a first pass shipped white cards, a yellow accent, a purple eyebrow, and Montserrat. None of it from the site. The correction was blunt and it was right: that is not the branding at all.
Both failures have the same root cause, and it is not taste. It is working from remembered tokens instead of reading the token file. Hex codes drift in memory in a way that feels certain. The fix we now enforce is procedural: before the first frame, first card, first component, open the actual source file and port the literal values.
🎯
The scope line is part of the token set. Ours now reads, in the file itself: brand book governs printed and branded documents; site tokens govern web and anything told to match the site; if the request is ambiguous, ask which. Adding that sentence cost nothing and has since prevented every repeat of the two misses above.
How to Run a Token Extraction
Find the single source of truth and name it in writing. A Figma file id, a .dc.html, a live CSS file, a brand PDF. One of them is authoritative. If two disagree, resolve it now, with the founder, in one message — not silently, and not by picking the prettier one.
Pull literal values, never remembered ones. Read the file. Copy the hex, the weight, the tracking. This is the step every failure above skipped.
Sample the rendered output, not the source, for proportion. Source files tell you which colors exist. Only the rendered page tells you which one is the ground and which one is a 1% accident.
Write the usage rule beside every value that has one. Gradients, status colors, and the accent always have one.
Declare the scope. Which media does this set govern, and which set governs the others.
Hand it to someone who has never seen the product and ask them to build one screen. Every question they have to ask is a missing token. Fix those, then ship the file.
Correctedthe ad kit had been using #EFE1E1 and #1E1E24, both wrong
A swatch list flattens the distinction the design depends on. Both colours are "in the brand"; one is the page and one is a button. Sample the render — the proportions are the design.
Most engagements do not start at a blank page. They start with a live site, a Figma file, a deck, a PDF, and a founder who says "match this." Extraction is therefore a core skill, and it has an order that matters.
The Extraction Order
Do these in sequence, because each step constrains the next:
Ground and proportion first. What color is most of the page? Everything else is read against it.
Ink second. Headline ink and body ink are usually different, and the difference is usually the thing people get wrong.
Accent and its usage third. Where does it appear, and how often. Count, don't guess.
Type roles fourth. From the live CSS if there is one, because rendered documents frequently strip font names.
Shapes and motifs last. Radii, borders, the one weird shape that repeats.
Case Study: Portable Tenant — Sampling a 261 MB PDF
Portable Tenant is a tenant-screening platform. We needed the real tokens from a Landing Page redesign that lived in Figma, and both easy routes were closed: the browser automation wedged on the Figma canvas, and no Figma access token existed on the machine — only FIGMA_ACCESS_TOKEN=your_token_here placeholders in an env example, which is its own small lesson about trusting configuration files.
So we exported the Figma file to PDF — 261 MB, 176 frames — rendered the 1440 by 9258 desktop frame, and pixel-sampled it. Sampling the rendered frame rather than eyeballing it produced a share-of-page table:
Token
Hex
Share of page
white
#FFFFFF
37.9%
peach ground
#FEE4D1
24.7%
neutral card
#F1F1F1
9.0%
peach deep
#FFCFAE
1.2%
Portable Energy
#FD7A1C
accent
ink (headlines)
#060308
display only
Digital Trust navy
#263889
secondary
That table immediately corrected two values in an ad kit we had built a week earlier from second-hand tokens. The light ground was #FEE4D1, not the #EFE1E1 we had been using. The headline ink was #060308, not the #1E1E24 we had been using — that value is real, but it is the body and UI ink, not the display ink. Both errors were small enough to be invisible in isolation and large enough to make a set of ads feel subtly not-quite-right next to the real site.
🔎
Share-of-page is the measurement that separates a ground from an accent. Two colors can both be "in the brand" while one covers a quarter of every page and the other appears on three buttons. A palette swatch list flattens that distinction completely, which is exactly how a designer ends up using an accent as a ground and wondering why it feels loud. Sample the render; the proportions are the design.
There is a second lesson in that engagement worth stating plainly: a rendered document cannot confirm type. The PDF embedded Type 3 outlined fonts with no names, so it could tell us every color and no font. The live Webflow CSS could — --fonts--headline: Figtree, --fonts--body: IBM Plex Sans. Colors from the render, fonts from the code. Neither source answers both.
How to Extract Tokens From a Live Site
Pull the computed styles, not the stylesheet. Cascades, overrides, and CSS variables mean the authored value and the applied value are frequently different.
Screenshot the full page and quantise the colors. A simple histogram over the rendered pixels gives you the share-of-page table above. This takes minutes and settles arguments.
List every distinct font-family, size, and weight actually in use. The result is almost always larger than the intended scale — that gap is your consolidation work.
Measure the radii and the border colors. Border colors are usually alphas; capture them as alphas.
Write the set into a single file, with the scope line, before you build anything.
Phase 3: Type — The Scale, and the Rule About Numbers
Phase 3 · Type
A role is five properties, not a font name
rows enter together, leave together · loops ~16s
Display48 / 900 / lh110 / -1%
Heading 232 / 700 / lh120 / -0.5%
Heading 324 / 700 / lh130 / 0
Body copy16 / 400 / lh150 / 0
Label12 / 700 / lh100 / +2%
36.0metric · mono 500 / -2%
Two rules Negative tracking on display, positive on small uppercase. Line height as a percentage, never a pixel value.
Six roles is enough for most products. If you need a seventh, you probably need a different component instead.
Type is where a design system is most often under-specified and most visibly wrong. A palette can be slightly off and read as a variant. Type that is slightly off reads as amateur.
Building the Scale
A scale is a small set of named roles, each fully specified. Fully specified means five properties, not one:
Role
Family
Size
Weight
Line height
Tracking
Display
Headline face
48px
900
110%
-1%
H2
Headline face
32px
700
120%
-0.5%
H3
Headline face
24px
700
130%
0
Body
Body face
16px
400
150%
0
Label
Body face
12px
700
100%
+2% uppercase
Metric
Mono face
36px
500
100%
-2%
Six roles is enough for most products. If you need a seventh, you probably need a different component instead.
Two things make the difference between a scale that holds and one that erodes. First, negative tracking on display sizes and positive tracking on small uppercase labels. Large type set at default tracking looks loose; small uppercase set at default tracking looks cramped. Second, line height expressed as a percentage of the size, not a fixed pixel value — a fixed value breaks the moment someone uses the role at a different size, which they will.
The Micro-Label Problem
Uppercase micro-labels with wide tracking — [ VARRITECH · NYC ], SYSTEMS LIVE, section eyebrows — are a signature of a lot of modern interfaces, ours included, at letter-spacing: .14em to .24em.
They also break the moment you leave the browser. Image libraries generally have no tracking control, so a micro-label composited into a video frame or a static ad has to be drawn character by character with manual spacing. We learned this the tedious way. If your brand leans on tracked labels, write the fallback method into the system, because someone will need to reproduce them outside CSS within the month.
✍️
A logotype is an asset, not a font. ConvergeFlow's logotype is Barend Slanted — and it is a fixed image file, never re-set in another face, never rebuilt from a webfont. Every brand has at least one thing in this category. Put it in the token file as an asset path, with the instruction not to re-typeset it, or someone eventually will.
Delete it and the same complexity reappears in every call site at once.
A button that cannot express "in flight" is a billing problem, not a design problem. A real buyer clicked a pay button with no pressed state twice and was charged twice.
The instinct when building a design system is to enumerate: button, input, card, modal, toast, forty more. Inventory feels like progress and produces libraries nobody uses, because the thing that makes a component worth using is not that it exists — it is that it absorbs complexity the caller would otherwise carry.
The Shallow Component Test
Borrowing the framing directly from software architecture: a deep component has a small interface and a lot of behaviour behind it. A shallow component has an interface nearly as complicated as its implementation, and using it costs about as much as not using it.
The test is deletion. Imagine removing the component and inlining it everywhere it is used. If complexity disappears, it was a pass-through and should not exist. If complexity reappears in five places at once, it was earning its keep.
Concretely, in a component library:
<Card> that takes fifteen props to cover every variant is shallow. The caller has to understand the whole variant space to use it. Split it by role — <StatCard>, <FeatureCard> — each with two or three props.
A <Button> that takes a variant and nothing else is deep. All the state work — hover, focus ring, disabled, pressed, loading — is absorbed. That absorption is the entire value.
A wrapper that only forwards props is the classic pass-through. Delete it.
The pressed and loading states deserve specific attention, because they are where a shallow button becomes a revenue bug. We shipped a checkout whose pay button had no pressed state and was not disabled while the payment intent was in flight. A real buyer clicked it twice, and was charged twice — two charges five minutes apart for the same tier, and two separate fulfilment records minted, so nothing downstream caught it either. His words afterwards: the button didn't change on press, so he clicked it again.
💸
A button component that cannot express "in flight" is not a design problem, it is a billing problem. The fix belongs in the component, not in each page that uses it: a submitting flag that both disables the control and swaps the label, plus a guard so the second invocation is a no-op. Every checkout, every destructive action, every form that costs money. If the design system's button cannot do it, every team will hand-roll it, and one of them will forget.
Signature Shapes Are Components Too
The thing that makes a product look designed rather than assembled is usually one repeated non-obvious shape. Portable Tenant's is a notched card: three large corner radii and one squared into a tab — border-radius: 14px 44px 44px 44px — with a circular number badge sitting on the squared corner.
It appears throughout the site, and it does more brand work than the palette does. It also carries a rule discovered by getting it wrong: on neutral-fill variants the tab must be hidden, or it reads as a rendering artifact rather than a deliberate shape. That is a component rule, and it belongs in the component, not in a designer's memory.
The second motif in that system is a node — a ring, a filled core, two satellite dots — which is the logo's letterform reused as oversized background geometry bleeding off the frame. One shape, two scales, two jobs.
Find the equivalent in your system. If you cannot name one, that is the gap, and it is a bigger gap than any missing component.
Phase 5: Motion — Where Design Systems Break
Motion is the part of a design system that is almost never specified and almost always improvised, which is why it is the part that most often looks broken.
The Stagger Trap
Phase 5 · Motion
animation-delay staggers the exit too
watch the right-hand stack empty · loops ~9s
Per-row keyframes
Staggered in. All leave at 97%.
animation-delay
Staggered in and out. Mid-loop it reads as failing to load.
Bake the stagger into the entry percentages, keep the exit identical. Same duration on every element, one keyframe per row. Correct in a one-shot animation, a bug in a loop.
This one cost us real time and it generalises to every looping animation, so it is worth stating precisely.
The natural way to stagger a list of elements entering is animation-delay: element one at 0ms, element two at 200ms, and so on. In a one-shot animation this is correct and fine.
In a looping animation it is a bug. animation-delay offsets the entire cycle, which means it staggers the exit exactly as much as it staggers the entrance. Mid-loop, the stack empties one row at a time, and an empty-then-refilling list does not read as intentional — it reads as a component failing to load.
The fix is to give every element the same duration and the same animation-name pattern, with a separate keyframe per row, baking the stagger into the entry percentages while keeping the exit percentage identical across all of them:
Rows enter at 2%, 6%, 10%. All of them leave at 97%. The stagger is visible on the way in and invisible on the way out, which is what the eye expects.
Motion Budget
Three rules that keep motion from becoming noise:
One thing moves at a time in the viewport. Two competing animations halve the impact of both.
Entrances are for content that just arrived. A section that animates in every time it scrolls into view is a section that fights the reader.
prefers-reduced-motion is not optional, and it is not a toggle to zero. Pin animated elements to their end state, so the layout is complete and static. An unhandled reduced-motion preference on an opacity-0 entrance animation means the content never appears at all — the accessibility failure and the functional failure are the same failure.
🎬
Scale motion to the canvas, don't port it. Our site uses drifting blurred color blobs at roughly 100px blur as background texture behind dense content. Ported at site scale into a mostly-empty vertical video frame, the same blobs stopped being texture and became the subject — they dominated the composition. The values were correct and the result was wrong. Background motion is calibrated against content density, so any motion token needs a note about the canvas it was tuned for.
Phase 6: Verification — Measure, Do Not Eyeball
Phase 6 · Verification
Four numbers a screenshot cannot give you
flips between the two cases · loops ~14s
viewport
footer
r.left === 0 && r.right === innerWidthinset by 24px
innerHeight − r.bottom31px gap below
scrollWidth > innerWidthno overflow
getComputedStyle(el).backgroundcream, not white
Both invisible at screenshot compression. Both obvious in one getBoundingClientRect() call.
And a narrow window is not a mobile viewport. A 430px capture showed content clipped at the right edge; measured properly, scrollWidth was exactly 430 with zero overflowing elements. A false positive costs a fix-hunt after nothing.
This is the phase that separates a design system that holds from one that quietly rots, and it is the phase most teams skip entirely. A screenshot is evidence that something rendered. It is not evidence that it rendered correctly.
The Geometry Assertions
For any layout change, assert geometry from the DOM rather than judging an image:
Four numbers, and they answer the four questions that compressed screenshots hide: is this element actually full-bleed, is the footer actually flush, is there a horizontal scrollbar, and is the background the color we think it is.
Case Study: The Footer That Looked Flush
On a Portable Tenant dashboard, a screenshot showed a footer at the bottom of the page. Reported as fixed. It was not: the footer was inset rather than full-width, with a cream gap beneath it. Both defects are approximately invisible at screenshot compression and both are obvious in one getBoundingClientRect() call.
The underlying cause is a pattern worth recognising, because it recurs constantly: a footer nested inside a centered max-w-7xl padded wrapper can never be full-width or bottom-flush. No amount of CSS on the footer itself fixes it. The fix is structural — a full-width flex column root at min-h-[calc(100dvh - headerH)], the header and content in an inner max-w-7xl wrapper that grows, and the footer as a direct full-width child pushed down by that growth. Then strip any page-level padding wrapper that insets everything.
Case Study: The Overflow Bug That Was Not There
The inverse failure is just as expensive. A headless screenshot taken at --window-size=430,1600 showed a heading, cards, and badges visibly clipped at the right edge. It read as an unambiguous horizontal-overflow bug on mobile.
There was no bug. Measured at a properly emulated 430px viewport, documentElement.scrollWidth was exactly 430 and zero elements had a bounding rect extending past it. --window-size sets the capture canvas but leaves desktop viewport semantics in place, so wide content gets cropped rather than reflowed. The screenshot was a false positive, and acting on it would have sent a fix-hunt after nothing.
Real mobile verification means setting device metrics before navigating — width, height, device scale factor, and the mobile flag — then measuring, then capturing the image if you want one.
⚠️
And check you are measuring the build you think you are. Restarting a local server, waiting a fixed number of seconds, and getting a 200 proves nothing: the old process may not have died, the new one may have failed to bind the port, and the 200 is the stale server serving the previous build. We chased a "broken layout" that was entirely this. Kill by port, confirm the port is free, poll until the new server answers, and only then measure. And on any deployed system, the preview build is the last word — a local dev server can wedge in ways the diff never explains.
How to QA a Layout Change
Capture a baseline before editing — the current production or preview URL.
Make the change, deploy, and capture the same views again.
Measure geometry at desktop and at a properly emulated mobile viewport. Full-bleed elements reach left: 0 and right: innerWidth. Sticky footers sit at gapBelow ≈ 0. scrollWidth never exceeds the viewport.
Diff the two captures and annotate what changed so the reviewer sees the delta, not the whole page.
Click-test interactive elements at mobile width. Responsive wrap and overlap bugs are invisible in a desktop capture — a fixed navbar whose buttons wrap can render those buttons unclickable while the desktop screenshot looks perfect.
Bonus: Accessibility — The Floor, Not the Ceiling
Bonus · Accessibility
Contrast is a token property, recorded once
ratios measured, not guessed · loops ~13s
ink on ground14.9:1passes
ink-soft on ground9.2:1passes
faint on ground4.9:1passes
accent on white1.4:1body text · fails
Write it in the file Body needs 4.5:1, large display needs 3:1. A proposed colour that fails is rejected before it ships, not after.
Most accessibility work is free if it happens at the token layer. Focus rings live in the component, status colour is always paired with a label, and reduced motion pins animations to their end state.
Accessibility work goes wrong when it is treated as an audit at the end. Treated as a set of tokens and component rules at the start, most of it is free.
Contrast is a token property, not a page property. Every ink-on-ground pair in the token file gets its ratio recorded next to it. Body ink on ground needs 4.5:1; large display type needs 3:1. Recording the ratio in the file means nobody has to re-derive it, and a proposed new color that fails is rejected before it ships rather than after.
Never encode meaning in color alone. Status colors quarantined to status is a brand rule and an accessibility rule at once — pair every status color with an icon or a label.
Focus rings live in the component, not the page. If the design system's button owns its focus ring, focus is correct everywhere by default. If it doesn't, someone will remove one with outline: none and nobody will notice for a year.
Hit targets at 44px minimum, including the invisible padding around small icons.
Every interactive element is reachable by keyboard and announces what it is. A <div onClick> is neither. This is one of the few rules worth enforcing with a lint rule rather than a review.
prefers-reduced-motion pins animations to their end state, as covered above.
Bonus: Dark Mode Without a Second Design System
Dark mode is cheap if the tokens were named by role and expensive if they were named by appearance. That is the whole story, and it is decided long before anyone asks for dark mode.
Named by role — ground, surface, ink, ink-soft, line — dark mode is one alternate block redefining those six values. Named by appearance — white, gray-100, near-black — you now have tokens whose names are lies in one of the two themes, and every consumer has to be revisited.
Three practical rules from doing this retrofit on real products:
Define the complete palette on the default selector, then redefine only what changes in the dark block. A value whose only definition lives inside a media query is a value that disappears when the query doesn't match.
Elevation inverts. In light mode, elevated surfaces get lighter and shadows get stronger. In dark mode, elevated surfaces get lighter and shadows get weaker to the point of useless. Elevation in dark is a surface token, not a shadow token.
The accent usually needs a different value, not the same one. A lime that sings on near-black vibrates painfully on white. Two accent tokens, one per theme, is normal and correct.
Bonus: Handoff — What Engineering Actually Needs
A handoff fails when it delivers pictures instead of decisions. What engineering needs, in order:
The token file itself, in a format the code can import. Not a screenshot of the palette.
Every state of every interactive component — default, hover, focus, active, disabled, loading, error, empty. The empty state is the one that is always missing and always shipped last, badly.
The breakpoints, and what actually changes at each. "It's responsive" is not a spec.
The long-content case for every container. What does this card do with a 90-character title. Design systems are broken by real data, not by lorem ipsum.
The motion spec, including reduced-motion behaviour.
The one rule per signature shape — like hiding the notch on neutral fills — that is otherwise carried only in a designer's head.
Bonus: The Design System Readiness Checklist
A single token file exists, is named as the source of truth, and declares its scope
Ground, surface, ink weights, line, and accent are named by role, not appearance
Every value with a usage constraint has that constraint written beside it
Status colors are declared status-only
Type roles are specified with family, size, weight, line height, and tracking
The number/mono rule (if the brand has one) is written down
Contrast ratios are recorded next to each ink-on-ground pair
Buttons own their focus ring, disabled state, and in-flight state
No component has more than four props without a reason you can state
Signature shapes are components, with their exception rules
Looping animations use per-row keyframes, never animation-delay
prefers-reduced-motion pins animated elements to their end state
Empty, loading, and error states exist for every data-backed component
Layout changes are verified by DOM geometry, not screenshots
Mobile is verified with emulated device metrics, not a narrow window
A stranger can build an on-brand screen from the token file alone
Bonus: Tools and Resources Library
Tokens and systems
Style Dictionary — token transformation to CSS, iOS, Android from one source
Tokens Studio for Figma — keeps Figma variables and a code token file in sync
Radix Colors — well-constructed scales if you need a starting palette with the alpha and dark variants already derived
Component libraries worth building on
Radix Primitives — unstyled, accessible behaviour. The correct base for a house system
shadcn/ui — Radix plus Tailwind, copied into your repo rather than installed. Good when you want to own the code
Ariakit — an alternative primitives set with strong keyboard behaviour
Design and prototyping
Figma — variables and modes map cleanly onto a role-named token set
Claude Design — for producing branded page and artboard layouts directly as HTML
Penpot — open-source alternative when a client cannot use Figma
Verification
Playwright — screenshots, device emulation, and DOM assertions in one tool
axe DevTools — the accessibility baseline scan
Chrome DevTools Protocol — device metrics override, for real mobile measurement
Type and color
Google Fonts and Fontshare — where most of the good free display faces are
APCA contrast calculator — more perceptually accurate than the WCAG 2 formula for large type
Coolors and Leonardo — for deriving a scale from a fixed brand accent
Bonus: Recommended Reading
Start here
Refactoring UI — Adam Wathan and Steve Schoger. The most directly applicable book on this list
Atomic Design — Brad Frost. The vocabulary most teams end up using
Thinking with Type — Ellen Lupton. Why the type rules above are the type rules
Deeper
A Philosophy of Software Design — John Ousterhout. The deep-versus-shallow framing in Phase 4 is his, applied to components
Design Systems — Alla Kholmatova. The organisational half, which is usually the harder half
Inclusive Components — Heydon Pickering. Accessibility as component design rather than audit
Worth following
Josh Comeau — CSS mechanics explained properly
Adam Argyle — modern CSS features before they are widely used
Smashing Magazine — still the best general source for practical front-end technique
What's Next?
Guide 3 covers the production architecture underneath all of this — the decisions that are expensive to reverse, the data-layer traps, the money paths, and how to measure before you refactor. Guide 4 covers security and compliance. Guide 5 covers testing culture and the specific ways a green test suite lies to you.
If you would rather have this installed than read about it, that is what we do.