Andrew Bloyce.

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.

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.
  • 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.

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.
Fig 1 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.
Fig 2 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.

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

.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. 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.
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 3 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 4 The form components in a real onboarding flow, with stepped navigation and date inputs.
A New Contract Template modal: a text input, a searchable team and location multiselect, radio options, and primary actions.
Fig 5 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 6 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 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.