When a Design System Starts Fighting the Product
A software stack can age faster than the people who built it expect. A codebase that felt perfectly sensible two years ago can start to creak once the product changes shape, the team grows, and new pages keep landing on top of old assumptions. That’s especially true in software engineering, where small shortcuts tend to pile up quietly. One button gets styled a little differently. Then a form field. Then a whole page that “borrows” a class from somewhere else because nobody has time to untangle the mess right now. Before long, the product is still working, but the visual rules have become more suggestion than system.
That’s the point where a design system migration stops being a nice tidy refactor and starts becoming a practical cleanup job. The payoff rarely shows up on day one. Nobody wakes up after a rewrite and says, “Well, that was worth it, I can feel the ROI in my coffee.” The immediate experience is usually the opposite. Files move, styles break, docs get rewritten, and a few bugs crawl out of the walls. The value comes later, when the team can build faster without tripping over three different ways to make the same page look almost right.
This was Sniips’ first completed migration of that kind, which makes the whole exercise a little more interesting. Plenty of teams talk about standardizing UI. Fewer finish the job. Here, the work had reached the point where the old setup was clearly slowing things down, and the team decided it was time to replace the patchwork instead of adding yet another patch to it.
When the product keeps changing, the old styling stops being a neutral backdrop and starts acting like an extra piece of code you have to argue with every day.
The starting point was messy in a very ordinary, very believable way. Some pages leaned on Bootstrap. Others used hand-rolled CSS that had grown around old page layouts and one-off fixes. A few parts had AI-generated styling layered on top, which sounds convenient until you realize the result is often a pile of code that looks fine in isolation and slightly awkward everywhere else. On top of that, some pages followed the design rules only loosely. Not wildly. Just loosely enough that nobody could point to a single source of truth and say, “Here, this is what we do.”
That kind of setup doesn’t explode all at once. It drifts. A header is a bit off here, a card spacing rule is duplicated there, a template gets a special case because “this page is different.” Then the exceptions become the system. At that stage, the old design rules aren’t guiding the product anymore. They’re trailing behind it, trying to catch up.
So the rewrite wasn’t about polishing something already stable. It was about clearing out a layered mess of tech debt before it turned into the default way of building things. The team had reached the point where the old styling setup was no longer helping new work move cleanly through the app. It was making every new screen a negotiation. The next step was to replace that negotiation with something the whole codebase could actually agree on.

What the Rewrite Actually Unlocked
Once the first migration pain had been cleared, the real question was simple: what did the team get for the trouble? The answer wasn’t “a prettier codebase” in the abstract. It was a setup that let the product speak the same visual language whether a page was built in React, HTMX, or plain Jinja templates. That sounds tidy on paper. In practice, it meant fewer one-off decisions, fewer little mismatches between screens, and fewer moments where a developer had to wonder, “Wait, how did we do this on the last page?”
That consistency mattered because the old component library had started to show its limits. JoyUI was fine when everything lived inside React, but the moment you stepped outside that world, things got awkward. Some parts of the product used server-rendered templates, some leaned on HTMX for partial updates, and some still lived in React. A design system that only behaved nicely in one of those worlds was never going to feel comfortable for long. The team needed something that could move across those boundaries without a lot of ceremony.
A design system only earns its keep when it works in the parts of the app that don’t match its original assumptions.
The shift also opened the door to a fuller shared component library. That’s the part people sometimes gloss over. A library isn’t just a pile of buttons and inputs. If it’s done well, it gives teams a starting point for cards, menus, dialogs, tables, empty states, and all the other bits that make a product feel finished instead of stitched together. Without that shared base, every new feature starts at zero. With it, teams can spend their energy on the actual product instead of re-creating the same UI scaffolding again and again.
There was another quiet payoff too: the rewrite let the team clean up old inconsistencies instead of dragging them forward. Older interfaces had accumulated small differences over time. One page used a slightly different spacing rule. Another handled validation in a more awkward way. A third had styling that drifted just far enough from the rest of the app to feel off, even if nobody could point to one single bug. By rebuilding against a shared system, the team could apply lessons they had learned along the way much more uniformly. A choice that had once been buried in a random component file could become a standard pattern, documented and reused.
That mattered for engineering as much as for design. When the same rules apply in more places, code reviews get easier. New pages are less likely to invent their own tiny dialect. Bugs caused by inconsistent UI behavior tend to show up less often, or at least in more predictable places. It’s not glamorous work. It is, however, the kind that keeps a product from turning into a drawer full of mismatched parts.
The team also picked the new stack partly because it fit better with LLM-assisted development. That detail is easy to wave away, but it turns out to be practical. Models are much more useful when the surrounding code is regular, reusable, and explicit. Give them a messy mix of styles and a half-dozen ways to build the same thing, and they tend to wander. Give them a clean component library with a clear pattern, and they can produce more useful first drafts with less correction. The stack wasn’t chosen for novelty. It was chosen because it made both humans and agents less likely to trip over the same old problems.
If you look at the public Sniips site and its pricing page, the product promise is straightforward enough. The rewrite was about making the code underneath that promise less brittle, more uniform, and easier to extend without starting from scratch every time.
The Setup: Small Guardrails Before the Big Leap
Before the first page was rewritten, the team did the unglamorous work that keeps a migration from turning into a fire drill. The live Sniips app had to keep working while new patterns were introduced, old ones were still present, and pages from different eras sat next to each other without looking like they’d met in a dark alley. That called for a setup that was narrow, explicit, and a little opinionated. The company’s about page describes the product as a way to make custom text snippets available across devices, and that cross-device promise shaped the front-end work too. The interface needed to feel the same whether it was rendered in React, HTMX, or plain Jinja.
Tailwind CSS was the first piece of that groundwork. Not the stock version, though. The team tuned it to the brand palette so the design tokens matched the actual product instead of some generic shade of “modern SaaS blue.” Spacing, color, radius, and typography were all set up to produce the same visual language across the app, which meant contributors didn’t have to invent their own tiny style systems every time they wanted a button, card, or form field. That’s the boring part, in the best possible way. When the defaults are sane, people stop freelancing with CSS just to make one page behave.
The safest migrations look boring from the outside. That usually means the rules are finally clear enough for people and agents to follow them.

On top of that Tailwind foundation, the team chose a lean component library built from ShadCN and BaseUI. That mix gave them a small set of reusable building blocks without dragging in a heavy, awkward framework. ShadCN supplied the familiar component patterns people already knew how to work with, while BaseUI filled in the lower-level pieces that needed predictable behavior and accessibility. Together, they formed a clean starting point for the new design system. The point wasn’t to collect components for the sake of it. The point was to have one set of buttons, inputs, dialogs, and layout pieces that engineers could reuse without first asking, “Which version of this thing is the least ugly?”
The tricky part, of course, was the transition period. A migration like this can’t assume the old world disappears overnight. So the team built a compatibility layer that let legacy components and the new ShadCN and BaseUI pieces appear on the same page without wrecking spacing, behavior, or event handling. That mattered because the rollout wasn’t a theatrical swap of one entire interface for another. It was a gradual replacement, page by page and component by component. In practice, that meant older screens could keep rendering while newer ones adopted the shared system, and nobody had to pretend the codebase had suddenly become neat. It hadn’t. It just became manageable.
Documentation carried a lot of that weight. The team wrote a DESIGN.md file that acted as the migration guide for both engineers and agents. It spelled out which component to use for common UI patterns, how to handle old markup during the transition, and when to stop improvising. More usefully, it gave concrete examples instead of vague taste-based advice. “Use the shared dialog” is a lot easier to follow than “make it feel consistent,” especially when a model is helping draft the code and needs exact instructions rather than vibes. The document became the reference point for decisions that might otherwise have been repeated in Slack, re-litigated in code review, and then forgotten by the next person with a deadline.
Linters rounded out the setup. They pushed contributors toward the shared design system and away from one-off styling that would only create cleanup work later. If someone reached for a stray utility class or tried to build a local variant that already existed in the library, the tooling gave a nudge. Not a lecture. Just enough friction to make the shared path easier than the shortcut. That kind of enforcement sounds dull, yet it’s what keeps a design system from turning into a polite suggestion. With the palette locked in, the component library ready, the compatibility layer in place, and DESIGN.md telling everyone how to proceed, the team had enough structure to start the migration without crossing their fingers every five minutes. From there, the real rollout could begin.
Rolling the Migration Through the Codebase
The first pass was deliberately large. The team took the Jinja and HTMX styling and rewrote it in one broad pull request, then spent the next couple of days doing little more than fixing the broken edges that turned up after it landed. That sounds a bit brutal on paper, and, to be fair, it probably felt that way in the moment too. A migration at this size rarely arrives with a neat little bow on top. The real work starts once the code is in place and people begin clicking through pages that have lived with the old rules for months or years.
The safest migration is the one that can absorb a bad assumption without forcing everyone back to square one.
That first wave of fixes did more than patch bugs. Each correction fed something back into the system. If a component behaved oddly, the component library got adjusted. If an instruction had been too vague for an AI coding agent to follow reliably, the docs were tightened. If a recurring mistake kept slipping through review, the lint rules were hardened so the same error would get caught earlier next time. In other words, the migration was treated as a feedback loop, not a one-shot rewrite. That matters, because design work tends to drift unless you keep teaching the codebase what “good” looks like.
After the initial push, the team spent about a month refining the whole setup. New components were added as pages exposed gaps in the shared library. Prompts and written guidance were cleaned up when the agents misunderstood a pattern or produced a half-right result. The system also got better at recovering from mistakes. If an AI-generated change came close but missed a detail, the team updated the instructions so the next page wouldn’t make the same detour. This is where a migration becomes less about code and more about repetition. You teach the machine, then you teach it again when it learns the wrong lesson.
The page-by-page work happened in vertical slices. That’s the part that kept the rollout from turning into a giant blockade. Each pull request handled one page, or one small cluster of related pages, and it had to preserve behavior while swapping the visual layer over to ShadCN. The request wasn’t just “make it prettier.” It had to keep forms working, keep states intact, keep the same routes and interactions, and then prove the result with before-and-after screenshots. Those screenshots were more than a nice record for the PR description. They gave reviewers a quick way to check whether the new version still behaved like the old one, minus the visual clutter that had built up over time.
Working this way also made it easier to keep moving while the rest of the app stayed live. One slice could land, get checked, and then the next slice could start without dragging half the company into a half-finished interface swap. That’s the sort of discipline that keeps codebase cleanup from becoming codebase chaos. It also gave the team room to work across stacks instead of forcing everything through one giant bottleneck. Jinja, HTMX, and React pages could all be handled in their own turns, with the shared component library catching the common patterns in between.
The numbers tell the story without much decoration. In about a week, the migration cut the set of files tied to @mui/joy from roughly six hundred down to under two hundred. Along the way, the team shipped dozens of pull requests across multiple stacks. That pace only worked because the process was broken into small enough pieces that each one could land cleanly. A huge rewrite can freeze a team. A series of narrow, reviewable changes keeps the lights on while the old system is peeled away one page at a time.
Don’t Leave the Old System Behind
The end goal was not a half-finished makeover where the old design system hangs around like a box of cords you swear you’ll sort out later. The team wanted it gone. No stray JoyUI components. No old styling patterns lurking in odd corners. No quiet fallback to the habits everyone used before the migration.
That sounds a bit obsessive until you remember how modern code gets written. A lot of engineers now use AI tools to draft UI, refactor components, or patch pages faster than they could do by hand. Those tools do not arrive with taste. They copy what they see. If the codebase still contains a blend of old and new approaches, the model will happily reproduce that mess with great confidence and only mild formatting issues.
A migration only becomes a new default when the old default stops being available.
That is where cleanup stops being housekeeping and starts doing real work. If the repository still carries the old system, every new page can drift back toward it. One engineer copies a component that “sort of works.” Another agent fills in a form with styling that looks familiar but belongs to the previous stack. Before long, the team has a fresh design system with a few fossils embedded in the walls. Nobody planned for that. It just happens because codebases have a memory, and tools built on pattern matching inherit it.
A cleaner repository changes the day-to-day experience in a very practical way. An engineer can open a file, reach for the shared component library, and trust that the rules are already settled. They don’t need to wonder whether this screen should use a leftover Bootstrap class, a hand-rolled CSS fix, or a shiny new component from the current stack. The answer is already there. That saves time, but it also cuts down on second-guessing, which tends to waste even more time than the actual coding.
There’s a softer benefit too, though I’d call it a side effect rather than the main prize. Once the old code is removed, people stop arguing with the codebase itself. They spend less energy decoding exceptions and more energy building the thing they actually want to ship. New pages look like they belong together because the repository makes that the easiest path, not the heroic one.
That, in the end, is what a successful migration looks like. The rewrite matters. The rollout matters. The cleanup matters just as much. If the old system still has a home, it will keep renting space in your future work. If you remove it, enforce the new rules, and make the shared patterns easy to use, the new system finally becomes the one everybody reaches for without thinking twice.




