andrew.bloyce / docs
v3.4.0

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.

Year
2021 – 2026
Role
Senior UX Engineer · system lead
Stack
Rails · Hotwire · Turbo · Tailwind
Status
Shipped · in production

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.

Scope

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.

A hand-drawn Excalidraw wireframe of a New Leave Request screen: a month calendar, a who-is-taking-leave select, leave-type radio buttons, start and finish dates, and a day-by-day hours breakdown.
Early Excalidraw wireframe, thinking through a leave-request flow.
A FigJam board comparing the current template-creation modal with a proposed full-page layout, annotated with notes on simplifying the flow and reusing an existing page pattern.
FigJam exploration: simplifying a modal into a full-page flow, with the reasoning on the board.

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:

app/views/rosters/_shift.html.haml haml
-# 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.

.claude/skills/design-preview/SKILL.md markdown
---
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.
Result

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.
Build vs buy

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.

A spread of Tanda design system components: day-of-week toggles, a searchable multiselect, shadow scale, segmented controls, gradients, a signature pad, and button variants.
Fig 1 A sampling of the library: toggles, searchable selects, a signature pad, buttons, and more.
A multi-step Hire & Onboard flow built from the design system: stepped tabs, side navigation, and text and date input fields.
Fig 2 The form components in a real onboarding flow, with stepped navigation and date inputs.
A Find a report with AI feature: a prompt input, a Find Reports button, and result cards with percentage-match pills.
Fig 3 Find a Report with AI: the same inputs, cards, and pills powering an AI feature.
A New Contract Template modal: a text input, a searchable team and location multiselect, radio options, and primary actions.
Fig 4 A modal flow with searchable team scoping, radio options, and primary actions.
The Tanda mobile app showing a weekly schedule with a highlighted shift card.
Fig 5 The same system on mobile: schedule and shift cards.

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.