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.
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.
- Adoption. There would be no point in having a beautiful design system if engineers didn't use it. I needed to find a way to foster collaboration and embed the design system into the core workflows of everyone writing code.
03 What I did
I start rough, sketching a component and its states in FigJam or Excalidraw, then move to an in-code prototype. Usually I skip high-fidelity designs unless the situation demands it. This process keeps things moving quickly and allows for fast feedback loops.
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.
= 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 mockups of push-notifications and email sending. Each page also includes usage documentation: when to reach for a component, and when an alternate pattern is preferred.
In order to combat the messy SCSS stylesheets Tanda adopted Tailwind CSS. I contributed greatly to the migration from bespoke SCSS to utility-first Tailwind. I methodically moved pages on the old stack to the new stack, improving the UX where possible and using our new design system.
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 helper. Going bespoke was more effort upfront, but it meant
that every component fit Tanda exactly as we needed it,
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. Adoption is global and usage ubiquitous.
- 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 when there was an internal push for ViewComponents 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.