← Back to work

Design system · Designed & built

Flint: one design system for web and iOS, checked on every build

The design system under AccessSpark and Inner Spark Media. One token source feeds CSS, MUI, shadcn/ui, and SwiftUI, and every build proves they match and that text clears 7:1.

Role
Design, token architecture, build pipeline, and docs
Timeline
June 2026 to now · 16 releases
Tools
Figma, Node, Astro, React, SwiftUI
Focus
Design systems, accessibility, multi-platform
flint.innersparkmedia.com
The Flint docs siteThe Flint documentation home page. A side nav lists Get started, Foundations, Components, Workbench, and Guides. The main area traces one decision, the primary button fill, from the blue color ladder to a semantic token, to a component token, to the code each platform ships: CSS, MUI, shadcn, and Swift.FlintDesign SystemWorkbenchDarkGet startedWeb and CSSMUIshadcn/uiiOS and SwiftUIFoundationsColorContrastTypographyComponentsButtonCheckboxInputWorkbenchGuidesFlintGet startedBrowse foundationsOne decision, every platformPrimitivecolor.blue#1550a0step 700, light#93c5fdstep 300, darkSemanticaction.primarylightdarkComponentbutton.primary.bglightdarkPlatformsCSS--button-primary-bgMUIpalette.primary.mainshadcn--primarySwiftComponent.buttonPrimaryBg

The context

I run several products on my own: AccessSpark (a React app on MUI), its marketing site (Astro), the Inner Spark Media site (EJS), and two iOS apps. Each one needed the same blue, the same type, the same focus ring.

Every product had its own copy of the colors. Copies drift. A blue got nudged in one place and not the others. The iOS apps carried hand-typed hex values. A stray release tag made the token repo look like a 2.x release when it was on 1.8. Nothing broke loudly. It just slowly stopped matching.

For an accessibility company, drift is worse. A color that passed contrast in one product could fail in another, and nobody would know until an audit found it.

Before and after: copied colors versus one sourceBefore: four products each keep their own copy of the primary blue, and the copies have drifted to three different hex values. After: one token, action.primary, is built into four outputs, a CSS variable, the MUI palette, the shadcn primary variable, and a Swift color, all landing on the same value.Before: copied by handAccessSpark appMUI theme file#1550a0Marketing siteCSS variables#1a5fa8Consulting siteIts own stylesheet#2767cciOS appsHex typed into Swift#1a5fa8One "primary blue." Three values. No check.After: one sourcetokens.jsonaction.primary= blue.700CSS--color-action-primaryMUIpalette.primary.mainshadcn/ui--primarySwiftUIColors.actionPrimaryOne value. A build that fails if any platform disagrees.
The problem and the fix. Illustration: the before is simplified, the after is the real build output.

What I owned

All of it. This is my system for my own products, so I was the designer, the token architect, the build engineer, and the docs writer. The Figma library and the code both come from the same decisions, and when they disagree the repo wins.

Token architecture

Primitive ladders, semantic tokens for light and dark, and a component tier. Naming that reads the same in CSS, JavaScript, and Swift.

Build and checks

The build that turns one file into every platform's format, plus the contrast, parity, and page checks that gate each release.

Docs and workbench

flint.innersparkmedia.com: getting started per platform, foundations, component guidance, and a live workbench.

Figma library

Variables and components that mirror the tokens, so design and code start from the same values.

The constraints

One person maintains it, around a full-time job. That rules out anything that needs a team to maintain. If a rule matters, a script has to enforce it.

Four platforms with different ideas of what a theme is. CSS wants variables. MUI wants a theme object. shadcn/ui wants Tailwind variables with its own names. SwiftUI wants a Swift package. The products could not be rewritten to fit the system. The system had to fit them.

The decisions

Tokens in code are the source, Figma follows. The common setup runs the other way: designers own variables in Figma and export them. I rejected that. My products ship from code, a script can check code, and a script cannot check a Figma file. The Figma library mirrors tokens.json, and the docs say plainly that the repo wins.

Three tiers, and apps never touch the bottom one. Primitives are the color ladders. Semantic tokens name a job, like text.muted or action.primary, and point at a ladder step for light and another for dark. Component tokens point at semantic ones. I rejected a flat list of semantic hex values per theme. That is how the drift started: two themes, typed twice, edited once.

Three token tiersPrimitives: 86 colors on 8 ladders that nobody uses directly. Semantic: 62 tokens per mode that apps use, each pointing at one ladder step in light and another in dark, for example action.primary points at blue 700 in light and blue 300 in dark. Component: 34 tokens that components use, each pointing at a semantic token, for example button.primary.bg points at action.primary, so it follows light and dark on its own.Primitives86 colors on 8 ladders. Nobody uses these.300700blue, sky, gray, slate, green,amber, red, violetSemantic62 per mode. Apps use these.action.primaryLight{blue.700}Dark{blue.300}Light and dark split here, once.Every pairing is checked at 7:1for text and 3:1 for edges.Component34 tokens. Components use these.button.primary.bgpoints at{semantic.action.primary}Save tripSave tripNo mode of its own. It follows.
Three tiers. Light and dark split once, at the semantic tier. Real token names and counts from the v1.13 build.

Text at 7:1. WCAG AA asks for 4.5:1. I set Flint's bar at 7:1 (AAA) for every text pairing and 3:1 for control edges, focus rings, and chart marks. The rejected option was AA with a buffer "where it matters." That turns into a judgment call on every screen. One hard number is something a script can enforce, and an accessibility company should not ship the minimum.

Bridge to shadcn/ui, keep MUI. AccessSpark runs on MUI. I looked hard at moving it to shadcn/ui and rejected a rewrite. Instead Flint ships a shadcn bridge, one CSS file that maps every shadcn variable onto Flint, so new work can use shadcn and AccessSpark keeps working. Same tokens, both libraries.

Build my own workbench, drop Storybook. I first built the component workbench in Storybook. Then I audited the live docs with AccessSpark. Flint's components passed. Storybook's own interface did not: missing landmarks, empty button names, removed focus outlines, and I could not fix any of it without forking Storybook. A design system that sells accessibility cannot publish a test bench that fails it. I rebuilt the workbench inside the docs site in about a day.

The workbench turns differences into findingsA grid of input states down the side, Default, Hover, Focus, and Error, and three implementations across, MUI, shadcn, and plain CSS. The shadcn and plain CSS inputs draw a clear edge at 4.8 to 1. The MUI inputs draw a faint edge at about 1.6 to 1, under the 3 to 1 minimum, and are flagged as a finding filed as ASDS-44.StateMUIshadcnPlain CSSDefaultHoverFocusErrorCampsite nameCampsite nameCampsite nameFindingMUI's default edge isabout 1.6:1. Flint needs 3:1.shadcn and CSS draw 4.8:1.Filed as ASDS-44
Three implementations of the same spec, side by side. Where they differ, that is a finding. Simplified from the live workbench.

What shipped

One package, @innerspark/flint, built from one tokens.json. A build turns it into CSS variables, typed JavaScript, an MUI theme, a shadcn bridge, a Swift package with SwiftUI and UIKit colors, and a DTCG file for other tools. Then it checks itself. A release cannot ship if any gate fails.

The build gatetokens.json goes through the build, then three gates in order. Contrast: 70 pairings per mode, text at 7 to 1, edges at 3 to 1. Parity: 714 checks that CSS, MUI, shadcn, and Swift land on the same values in light and dark. Pages: zero axe violations on all 35 docs and workbench pages in light and dark, both densities, plus focus checks. Passing all three leads to a release. Any failure stops the release.tokens.json+ buildContrast70 pairings per modeText at 7:1Edges, focus, charts at 3:1Parity714 checksCSS, MUI, shadcn, SwiftSame value, light and darkPages35 pages, 0 axe violationsLight, dark, both densitiesFocus lands and holdsReleaseAny failure stops the release
What every build checks, with the real numbers from v1.13.
Docs that cannot driftEvery value, ratio, and code sample on the docs site is read from the build. Nothing is typed by hand.
A live workbenchEvery component as MUI, shadcn/ui, and plain CSS, every state, light and dark.
iOS on the same tokensA Swift package with SwiftUI colors, UIKit colors, type, space, and motion.

The outcome

I pointed AccessSpark at the live docs. The first crawl found real problems in how I had built the site and in Storybook. I fixed mine, replaced Storybook, and crawled again.

Auditing Flint with AccessSparkA loop with four steps. One, AccessSpark audits the live Flint docs. Two, it finds 83 failures and 548 issues, overall score 83. Three, the fixes: page structure, security headers, and SEO in the docs, Storybook replaced with a built-in workbench, and five false positives flagged back to AccessSpark. Four, the re-audit scores 94 with zero failures, and the loop returns to step one on the next release.1. AuditAccessSpark crawls thelive Flint docs.2. Findings83score, 83 failures,548 issues3. FixesHeadings, headers, SEOStorybook replaced5 false positives flaggedback to AccessSpark4. Re-audit94score, 0 failures,131 issues leftRuns again on every release
Real numbers from two AccessSpark crawls of flint.innersparkmedia.com on October 13, 2026. The 131 issues left are warnings and info. None are failures.
4
Platforms on one token source: CSS, MUI, shadcn/ui, SwiftUI
714
Parity checks on every build, from the build log
83 to 94
AccessSpark score for the docs, with failures from 83 to 0

The audit also went the other way. Five of AccessSpark's findings were wrong: a rule that only read inline CSS, advice to turn a toggle button into a link, and a rollup that blamed the whole site for Storybook's problems. Those are AccessSpark bugs to fix.

What I'd carry forward

Every rule I care about fails the build when it breaks: 7:1 text, matching platforms, accessible pages. The stuff I left to memory is what drifted.

Audit your own tools too. The workbench existed to catch problems in components, and it turned out to be a problem itself. I only saw it because I ran the same audit on the test bench that I run on the product.

There is more to do, and I would rather say so. The MUI theme does not apply every component token yet (ASDS-44). This portfolio still carries a hand-copied set of Flint colors, which is exactly the drift Flint exists to stop, and moving it onto the package is next.

Next case studyD&B Hoovers