
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.
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:
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.
Rather than building everything from scratch, we made two foundational choices:
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.
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:
Our Button component wraps MUI's unstyled button with our Tailwind styling:
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:
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.
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.
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.
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:
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.
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:
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.
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}}
Merge is hiring for several critical engineering roles across offices!