How we built an opinionated design system that enables our engineers to ship faster
.png)
A few years ago, our frontend codebase looked like many others: multiple patterns for the same problems, inconsistent styling approaches, and engineers burning cycles on decisions that had already been made a dozen times before.
Some components used custom CSS stylesheets, others used styled-components, and still others—bless them—mixed inline styles with utility classes in what can only be described as creative chaos. We'd rebuild essentially the same button or card component in subtly different ways across different features. We had freedom, sure—but that freedom was killing our velocity.
Today, our frontend is powered by an opinionated design system that combines Tailwind CSS with Material UI's unstyled components, built in a separate repository.
The result? We can ship features faster, our codebase is more maintainable, and—perhaps most surprisingly—AI agents can now write production-quality frontend code for us.
The hidden cost of freedom
React is famously un-opinionated. That's often celebrated as a strength, but it comes with a cost: Every team builds their own conventions, and without clear guidelines, complexity accumulates incrementally.
We saw this firsthand. Our team would:
- Debate whether to use <code class="blog_inline-code">className</code> composition or styled-components (again)
- Write 500-line React components that mixed business logic, API calls, and rendering into one glorious mess
- Create duplicate components because finding the existing one was harder than just rebuilding it
- Burn PR review cycles debating code organization instead of actually discussing business logic
The problem wasn't bad engineering—it was decision fatigue. Every "small" choice about how to structure a component or apply styling got multiplied across our entire team. As our codebase grew, so did the cognitive overhead.
Building on strong foundations
Rather than building everything from scratch, we made two foundational choices:
1. Tailwind CSS for styling
We chose Tailwind because a utility-first, opinionated system removes entire categories of decision-making. Instead of debating CSS architecture or inventing class names, we compose UIs directly in JSX with a constrained, shared vocabulary.
Our <code class="blog_inline-code">tailwind.config.js</code> defines our design tokens—colors, typography, spacing—so engineers reach for a single source of truth:
This isn't just about consistency—it's about eliminating entire categories of decisions. Need a gray background? You don't invent a hex code, debate whether it should be #EAEAE9 or #E8E8E8, or create yet another design token. You just pick from gray-0 through gray-90 and move on with your life.
2. Material UI Base for component primitives
Material UI's unstyled components (<code class="blog_inline-code">@mui/base</code>) gave us accessible, production-tested component primitives without visual opinions.
We get:
- Battle-tested accessibility patterns
- Complex interaction logic (focus management, keyboard navigation, accessibility)
- Framework-agnostic primitives we can style however we want
Our Button component wraps MUI's unstyled button with our Tailwind styling:
Explicit rules that keep us moving fast
The technical stack was only half the solution. We needed code organization principles.
We developed several rules for React development at Merge, all focused on one goal: keeping components and logic small, focused, and predictable.
Here are a few snippets of the rules we developed and try to abide by:
One React component per file
Following the Single Responsibility Principle, each file contains exactly one React component. The file is named after the component, and if there are child components, they live in a nested <code class="blog_inline-code">components</code> directory.
Why? Multiple components in the same file create ambiguity.
You open <code class="blog_inline-code">Dashboard.tsx</code> and find three components—which one is the Dashboard? Even if they're tiny components, consistency matters more than the convenience of keeping things "close together." When every file maps to exactly one component, navigating the codebase stops being an archaeological expedition.
Complex logic stays out of JSX
JSX should contain only two things: rendering logic and single-line anonymous event handlers. Everything else—multi-step event handlers, conditional logic, derived state—lives outside the JSX as named functions or computed values.
JSX should describe what renders, not how it works. When you extract logic and give it a clear name, the component becomes self-documenting.
Balance complexity when adding features
When you add complexity to a component, find another way to reduce it. Adding new functionality? Extract a subcomponent, create a custom hook, or refactor how the component is organized.
An absolutist would say every line of code is technical debt—and we know from Star Wars that only Sith Lords deal in absolutes. But here's what's not up for debate: when you add features to existing components without refactoring, complexity compounds. Without active efforts to reduce that complexity, you eventually wake up one day staring at a 2500-line component that does everything, handles every edge case, and is maintained by exactly zero people.
The alternative is incremental improvement: add a feature, but also extract a hook or break out a subcomponent. This keeps our codebase healthy and prevents those nightmare refactors where someone has to set aside two weeks to untangle a single component.
Atoms, molecules, and composability
Our design system follows atomic design principles—a methodology popularized by Brad Frost that treats interfaces as hierarchical systems rather than collections of pages. The power of this approach is composability: build robust primitives once, then combine them into increasingly complex components without starting from scratch each time.
Here's how it works in our system:
- Foundations are the base layer: our Tailwind color tokens, typography scale, spacing units, and border radii. These are pure design values—not components yet
- Atoms are the smallest functional components, built from foundations + MUI Base primitives. A <code class="blog_inline-code">Button</code> combines MUI's unstyled button (for accessibility and interaction logic) with our Tailwind tokens (for styling). A <code class="blog_inline-code">Text</code> component enforces our typography scale. These atoms can't be broken down further without losing functionality
- Molecules: Composed components like <code class="blog_inline-code">InputCombo</code>, <code class="blog_inline-code">Alert</code>, <code class="blog_inline-code">Accordion</code>
This hierarchy matters because it creates a force multiplier: when we update a foundation (say, our primary blue), every atom using that color updates automatically. When we improve an atom (like adding better focus states to <code class="blog_inline-code">Button</code>), every molecule using that button improves. Changes cascade through the system predictably.
Here's a real molecule in action—an <code class="blog_inline-code">Alert</code> that combines multiple atoms into a cohesive, reusable pattern:
Every component is fully typed with TypeScript, battle-tested with snapshots, and documented with live Storybook examples. When engineers need a UI element, they import it from the shared repository instead of rebuilding it from scratch. The button has already been invented—and refined, tested, and made accessible.
The unexpected benefit: AI-native development
Here's where everything compounds.
In most codebases, AI agents hallucinate component APIs, mix styling approaches, and generate code that looks reasonable until you try to use it. But the problem isn't the AI—it's that human engineers face the same issues. Inconsistent codebases are brutal for everyone, whether you're carbon-based or made of transformers.
Our opinionated design system changed this. With clear foundations, atomic hierarchy, and explicit rules, AI agents became competent:
- The patterns are learnable. One way to build components. One styling approach. The atomic hierarchy (foundations → atoms → molecules) is a mental model that AI can replicate
- The component library is discoverable. All components live in predictable locations with Storybook docs. The AI doesn't need to guess or invent its own button
- The rules are enforceable. AI can easily follow predefined rulesets; such as the one component per file, commented sections, and logic extracted from JSX mechanically
We've gone from "AI as autocomplete" to "AI as a pair programmer that knows our conventions."
This isn't aspirational—it's how we work. AI agents generate components that pass code review. The same opinionated system that helps humans ship faster makes our AI tools genuinely productive.
The Future is opinionated
As AI becomes more integrated into software development, the advantages of opinionated systems will only grow. Clear patterns, consistent conventions, and well-documented components aren't just good engineering practice—they’re what make AI agents effective collaborators.
This philosophy extends beyond our internal tooling. We're building Merge Agent Handler to give AI agents secure access to thousands of third-party tools.
Just like our design system provides clear patterns for building UIs, Merge Agent Handler provides clear pathways for agents to interact with external systems. The principle holds: opinionated systems with well-defined boundaries make everyone—human and AI—more capable.
Want to build faster? Stop debating how to build. Start enforcing opinions. Your future self will thank you.
{{this-blog-only-cta}}