Case study · Design systems
Tanda Design System
A from-scratch design system for a workforce management platform. Built on a modern Rails stack with Turbo, Hotwire, Tailwind, and Stimulus, then extending into AI-native tooling.
01 The brief
Tanda is a workforce management platform used by tens of thousands of businesses. When I joined, the product had grown faster than its component library. Competing patterns and duplicated components were spreading across the codebase, every team was rebuilding the same buttons and modals from scratch, and the CSS had become a sprawl of bespoke SCSS, full of specificity wars and dead styles with no shared scale.
The brief was to standardise the UI, but the real work went deeper: a component library, a documentation system, and a way of working so any engineer could reach for the right pattern without asking a designer first. My background as a developer shaped how I built it. Years on the other side of the handoff had taught me what a good developer experience feels like.
68 components and 97 documentation pages, built across three coexisting front-end stacks.
02 The challenge
The hard part wasn't really what to build. It was the conditions I had to build it under.
- Three coexisting stacks. When I started, the codebase was split between a legacy stack (raw HAML and jQuery) and a React section. Hotwire, Turbo, and Stimulus arrived later, adding a third. Every component I built had to look and behave identically across all three.
- Nothing to build on. There were no design tokens, no shared CSS scale, and nowhere to put documentation. I started from the ground up.
- No allocated time. The design system was never my main job, so every component and doc page got fit around regular product delivery.
- A team to bring along. A design system imposed from the top rarely sticks. Engineers adopt a library when they've helped shape it, so I worked on it with them instead of handing it down finished.
03 What I did
My process is low-ceremony. I start rough, sketching a component and its states in FigJam or Excalidraw, then move to an in-code prototype. The design decisions get made in the browser, against real content and real breakpoints on the actual stack, instead of in a mock that hides how things really behave. Designing and building in one place is the advantage I lean on most.
I treated the system as a product, with Tanda's engineers as its users. The component
library is a set of HAML partials, reached through a single
ds helper. The API is tight and keyword-based, and reads more
like a vocabulary than a config file:
-# Every component reached through one `ds` helper — a vocabulary, not a config file
= ds.card type: :info, title: "Morning shift" do
%p.leading-tight Roster published and notified.
= ds.pill text: "Published", type: :primary, icon: "check" Just as important as the components was the documentation. I built a living docs site inside the app, around 97 pages, one per component, where every example runs for real instead of sitting as a static spec. The card page renders actual cards, the form pages submit and validate, and the Turbo and Stimulus pages show live behaviour. There are even working push-notification and email test harnesses. Each page also says when to reach for a component, and when to leave it alone.
Because Tanda's front end spanned three stacks at once, every component had to look and behave the same across all of them. Underneath, I led the migration from bespoke SCSS to utility-first Tailwind, doing it one page at a time. That gave the product a shared scale for spacing, colour, and type that the components enforced by default, and specificity wars stopped being a category of bug.
04 Key decisions
Three calls where the design and the engineering were the same decision.
Documentation that runs real code
I wouldn't ship a component without its page in the docs site, and those pages run the component for real rather than describing it. The card page renders live cards, the forms submit and validate, the Turbo and Stimulus pages show real behaviour. That turned the docs into the acceptance criteria: if a component couldn't be shown working on its own page, the API wasn't finished. Adoption stuck because the documented path was also the easiest one.
Bespoke library, no frameworks
I built the library from scratch, with no JavaScript framework and no off-the-shelf
component kit. On a Rails and Hotwire stack the components are HAML partials reached through
a single ds vocabulary. Going bespoke cost more upfront, but
every component then fit Tanda exactly, down to our own tokens and accessibility standards,
with no heavy client framework weighing the app down.
Usability over novelty
I keep form components simple. One example: instead of a toggle, I reach for a single checkbox or a pair of radio buttons. Toggles look modern, but research keeps finding that people misread their on/off state, and I care more about usability than novelty. A lot of Tanda's form design follows Adam Silver's HTML-first approach: accessible, robust, and boring in the best way possible.
Encode the system so AI could use it
Once the system and its docs existed, they became the foundation for AI tooling. I wrote a
Claude Code command, now a shared skill the team uses, called
design-preview. You give it a prompt and it builds a screen from
our real components, reads each component's partial to get the parameters right,
drops the result into the design system's previews, and hands back a shareable link. It
replaced a paid Magic Patterns subscription because it had something a generic tool can't:
our own components, tokens, and constraints. None of that works without the system
underneath. The taste was already written down as components, ready for the model to borrow.
---
name: design-preview
description: Create interactive HAML design previews in the design system
---
# Generate the screen using only real ds.* components.
# Before using one, read its partial in
# app/views/shared/design_components/ to get the parameters right.
# Write it into the design system's previews/ folder, then reply
# with a shareable dev link to the running preview. design-preview replaced a paid Magic Patterns subscription,
generating previews straight from our own component library.
05 Impact
The system now runs right across Tanda, on both desktop and mobile. It's the shared vocabulary the product is built from.
- It became the vehicle for a platform migration, carrying the front end off a legacy stack (jQuery and raw HAML) onto modern Rails: Hotwire, Turbo, and Stimulus.
- The quality of UI that engineers ship without a designer in the loop is much higher than it was, which lets the team move faster.
- The component library is reached over 7,500 times across the codebase.
We were about to pay for DocuSign. Instead I built a first-party signature component on the same primitives as everything else. A rough estimate puts the saving at about $30k a year.
06 What it looked like
The system in production: real screens from across the Tanda product, on desktop and mobile.
07 What I learned
Ship early, then listen
Rarely was a component perfect on the first go. Each one grew and evolved as developers and customers used it. There's real value in getting a component out early and listening to the feedback instead of trying to perfect it in isolation.
Communication is most of the job
A big part of the design job is communication, and it's something I could have done better. Sometimes I shipped work without speaking to key internal stakeholders first, and it eroded trust. In a future role I'd work harder to keep everyone in the loop, regardless of their role.
Hold your constraints loosely
We stayed frameworkless for a long time, and for most of it that was the right call. But towards the end there was an internal push for ViewComponents, and I should have listened to it sooner. I held too tightly to avoiding frameworks, and was slower than I should have been to loosen that commitment once the trade-offs had shifted.