Paying Down the Palette
5 min read
In August I joined Mastra as a product engineer. Mastra is an open source TypeScript framework for building AI agents, and alongside it we build a few apps: Studio, Platform, and Factory. A lot of my time since then has gone into rebuilding the design system those apps share, with a small frontend team. This is mostly about how that's changed the way I think about design.
Decisions made once
I think most people picture a design system as a folder of components. I've started thinking about it more as a long list of decisions that someone made once, so nobody has to make them again on every screen. Things like which red means error, or how tall a button is.
Getting those decisions right is a craft thing, and I think it matters more now that so much UI gets built with agents.
A bug that showed up twice
In my first couple weeks at Mastra, the breadcrumbs in one of our apps were cutting off the bottoms of letters like g, p, y, and j. A fix went in that morning. Eleven days later a new ticket came in for Platform: "Breadcrumb labels clip their descenders." It was the exact same bug in a different app.
- Projects
- Typography
- Page layouts
The problem was never really in the breadcrumb. It was in the shared layer underneath, and each app had its own slightly different copy of that layer. Fixing it there means every screen picks it up. This was more a learning moment for me of the breadth of a true design system.
Why debt piles up
When a team is moving fast, every screen solves its own problem in the moment. That's usually the right call at a startup. The cost just shows up later as a lot of small decisions that almost match.
Nobody did anything wrong here. This is just what happens when shipping comes first for a while. Each choice made sense on its own, and you only notice the drift once you line things up next to each other (the funniest one: our small tabs rendered half a pixel taller than our medium ones).
sm,
md,
sm
md
When we started digging in, I wrote that "the hardest part will be knocking down the 'debt' we currently have."
Order matters
My take at the time was "the type sizing matters first, then we do colors and continue to build." So we went type first (weight, size, and what each style is for), then color, and then ported over the pieces that were already good.
We also rolled it out slowly on purpose: one piece at a time, with the old colors left in place during the move so nothing broke.
Ask for a role, not a color
Color is the easiest place to see the layer idea working. Instead of a component picking a specific gray, it asks for what it needs: background, card, border. The raw colors live underneath, and the roles map onto them. Switch to light mode, or tweak the ramp, and everything that asked for "card" comes along with it.
Raw grays
Breadcrumb labels clip their descenders
Platform, eleven days later
#131313, #e7e7e7, #242424
Semantic roles
Breadcrumb labels clip their descenders
Platform, eleven days later
card, card-foreground, border
Building out the ramps was a challenge. I posted at the time: "new appreciation for people who work regularly with color. building out ramps this week has been challenge but a great learning experience." (Color people, I see you now.)
Restraint
We started with ten roles, and the rule for adding more is that a component has to show a state the existing roles can't describe. That rule keeps the role list from slowly turning into its own pile of debt.
Make the rule concrete
A lot of design direction starts as a feeling, like "keep it subtle." Focus rings are a good example. They need to be visible for people navigating with a keyboard without distracting everyone else. Ours are now set to the softest strength that still meets accessibility contrast guidelines, in light and dark mode. I think this is where a system earns its keep: "keep it subtle" gets turned into an actual number that every component can follow.
before
after
Fix first, then enforce
Once a system exists, it's tempting to turn on strict checks right away. We decided to fix first and enforce after, because a check that fails on every old inconsistency mostly just blocks people from shipping. For type, the enforcement is already in place: if you reach for a text size outside the scale, the tooling points you back to the shared styles.
Building at agentic speed
Agents are a big reason this matters right now. Coding agents tend to copy whatever patterns are closest to what they're working on. If the codebase has three ways to build a form, an agent will happily make a fourth. When I tell an agent to use our shared UI package, it pulls in everything, including whatever hasn't been cleaned up yet.
So I think a clean system with rules on top of it is one of the best things you can give an agent. Our goal from here is to build a system, and enforce rules for it, so that shipping at agentic speed becomes a complement to UX instead of a sacrifice.
New work gets easier on top of it too. Illustrated empty and error states landed on October 5, and a new Cmd+K menu merged on Platform this week.
Functional interfaces
In one of our September team updates I described the work as "burning down old issues while implementing the right practices for future composability." I think that's still a fair summary.
More than anything, this work gave me a new appreciation for the beauty of functional interfaces. When the descenders aren't clipped, the reds mean the same thing on every page, and the small tab is actually small, nobody notices. I think that's kind of the point.
Lots more to come from me.