Rebuilding Zip's component library with Material UI has helped promote consistency and best practices across our codebase. Material UI offers a thoughtful developer experience and is built to support thorough customization. The availability of quality open-source component libraries should make frontend teams think twice before building library components from scratch.

Zip's component library before Material UI

In mid-2021, Zip’s team included 6 engineers. At this point, our component library was made up of forked react-bootstrap components and in-house components, alongside a few singular-focus third-party packages — e.g. react-overlay, react-select, react-dates, react-beautiful-dnd. At the time, our patchwork of a component library met our humble needs — we had a small team, very flexible design requirements (aka no designers 😅), and only a few customers.

By the latter half of 2021, our team, customers, and codebase were all growing rapidly, which meant stepping up expectations for UX quality and consistency. At this point, a few pain points with our hodgepodge component library became increasingly clear. Our various methods for building components each presented their own challenges:

Forked react-bootstrap components (do not attempt)

This is likely obvious to frontend devs reading this, but forking react-bootstrap components into our repo was not ideal:

  • The source code was not very dev-friendly, since the library was meant to be used as a package rather than forked.
  • The components we had forked were from an older, untyped version of react-bootstrap. Adding TypeScript types to these components was difficult, and the lack of types made usage error-prone, especially when engineers were accustomed to relying on type-safety elsewhere in our repo.
  • Styles were defined in separate Bootstrap Sass files, whereas the rest our app used styles defined in component files. Having an additional source of CSS made it more difficult to debug styling issues and modify existing styles.
  • As we made changes and our forked react-bootstrap version became out-of-date, Bootstrap's documentation and community became nearly useless.

Open-source UI packages

Many of the open-source packages we used provided substantial value and were well maintained (e.g. react-hot-toast, @visx). However, each additional package we added carried some cost:

  • Risk of the package no longer being maintained, e.g. Airbnb's react-dates
  • Divergent patterns between components from different packages
  • Maintenance to keep each package up-to-date, and additional overhead in making system-wide design changes across a variety of imported components

In-house library components

By the end of 2021, we tended towards building new library components in-house. As engineers added complexity to our library by building new components and extending existing ones, the library became difficult to use and maintain. We struggled with:

  • Inconsistency & anti-patterns: With different engineers creating components and new functionality as needed, our component library lacked consistency. Some variation was relatively harmless—component names, prop names, file formatting, styling methods—but in sum, had a negative impact on developer efficiency. Other anti-patterns and inconsistent practices resulted in quality issues, since developers couldn't assume that a component or its props would behave similarly to their counterparts elsewhere in the library.
    • Example - state management in form inputs: some of our input components followed standard controlled/uncontrolled state management conventions. But in others, passing a changed value to the defaultValue/initialValue prop would update the component's internal state via a useEffect. 🥲
  • Prop bloat: When adding functionality or custom styles to a component, engineers often added one-off props to enable customization or new functionality. This resulted in some of our core components having 30+ props, sometimes with misleading names. 🪤
  • Similar or duplicated components: Instead of reusing and extending our existing components, engineers sometimes created new components with similar styling or functionality. This usually happened when existing components were hard to find or lacked documentation. Or in other cases, devs weren't confident in safely extending these components because they were hard to reason about, lacked tests, or weren't type-safe.
  • UX consistency and polish: Many of our in-house components didn’t reuse base styles/child components and were thus styled inconsistently. Implementing a simple change to our design system could be a time-consuming and risky task, since that change may need to be made in different ways across multiple components.
    • Example - updating text input height: When we updated our design system to decrease the height of our text inputs, the change required updating 10+ components. Since the structure and styling of each input component varied, each one required different changes: potentially updating padding, min-height, or height values, or updates to absolute-positioning of elements within the input.

Evaluating our options: to build in-house or to yarn add?

As issues with our component library became increasingly painful, we knew we needed a more principled approach. Much of the pain caused by our existing library stemmed from the varied methods of implementation, so we knew we wanted to move towards a unified practice for building our library components. One of the core questions we had to think through: should we commit to building our components in-house? Or should we add (yet another) third-party library to the mix?

Option 1: Rewriting our components in house

Pros Cons
Complete control to customize components as we saw fit Time-intensive, considering the high bar for quality and consistency we hoped to meet
No need to ramp up on external APIs Difficult to enforce consistency, as the need for new functionality often comes up within feature work, outside of a dedicated component development roadmap

Building a great component library from scratch is hard. The architects of a component library must consider:

  1. Ease of use: Clear naming, code comments, documentation, examples
  2. Extensibility: Support new features and customization
  3. Robust design: Responsiveness, accessibility best practices, localization
  4. Robust frontend implementation: error handling, unit tests, sufficient browser support
  5. Consistency: design, state management/composition patterns, prop naming, TS types, etc.

Maintaining a good internal library can be especially challenging for orgs without a dedicated frontend platform team, where functionality/components are typically built as needed by various product teams.

Option 2: Rebuilding much of our library using a more comprehensive third-party library

Pros Cons
Faster development of new components, since we'd be using the open-source component as a starting point Potential to run into blocking limitations or bugs of open-source projects
Offers a relatively consistent dev experience across components Maintenance costs

We were already using quite a few third-party UI packages, and many of us felt that their limitations and maintenance cost outweighed the benefit of starting with a functional base. However, as we explored some more comprehensive UI libraries, it seemed we could reduce the pain caused by consuming many third-party packages, while avoiding the costly endeavor of building a high quality library from scratch.

We explored a few options for comprehensive UI libraries, but it quickly became clear that MUI made the most sense for us. (more in the review section later)

How we use Material UI

It's now been 18 months since we first decided to start using Material UI, and we now use ~20 of its components. Our MUI wrapper components have been used in over 1000 instances across our repo.

We typically import each type of MUI component only once, into a component that wraps the MUI component with limited additional functionality + styles. We prefixed our core components with the letter ‘Z’ (for Zip), but otherwise reuse MUI’s component name. For example, ZAutocomplete is our wrapper around MUI's Autocomplete.

Though we're using a limited subset of Material's components, we still use new MUI components regularly, as we extend and improve our component library. We usually start using a new MUI component when we need to rebuild one of our in-house library components, either to implement major design changes or to provide new functionality.

Our Review of Material UI: 4.7 out of 5 stars ⭐️

Generally, MUI has helped us achieve our quality and consistency goals, and accelerated development of new components and functionality. It has been especially beneficial in onboarding new engineers, and for full-stack developers who may have been less attuned to frontend best practices and the various quirks of our legacy components.

Why we love it:

  • Breadth: MUI’s broad coverage of common UI components and functionality is hard to beat. Though we didn’t have plans to rewrite more than a few of our components using a third-party library (we still only use a small percentage of the library!), it was good to see that MUI included many components not yet built into our library. Before considering the addition of a new third-party UI/util package, we can look to MUI first to see if using a new component might meet our needs.
  • Depth: MUI components are built to support deep customization, and they often support advanced functionality out-of-the-box. This has enabled faster development of complex component interactions.
  • Extensibility done right: MUI's APIs balance a simple developer experience with full extensibility. It's easy to build frontend with a basic/standard version of an MUI component, but teams can also extensively customize styles and functionality, without feeling like they're "hacking" around MUI’s implementation.
  • Modern but mature: MUI offers a great balance of stability and modernity. The library is very actively maintained and is regularly improved, but upgrades are well-documented and have been relatively painless.
  • Documentation: MUI’s documentation is well written and thorough. Additionally, MUI's large user base has been invaluable. Users have tried nearly everything imaginable, providing a trove of StackOverflow posts and GitHub issues for reference. Whenever I've felt stuck while attempting some customization on top of MUI, I've found its incredibly rare to not find a related community discussion.

Other thoughts:

  • Some signs of age: MUI comes with some baggage from being a longstanding project. Given its popularity in the 5 years since the v1 release, MUI (understandably) must be cautious and intentional in making breaking changes. It seems that this has resulted in some minor inconsistency or less-than-ideal patterns. If MUI Core were completely rebuilt in 2023, without any need to support migration from prior versions, I imagine there would be some improvement in the library's consistency and ease-of-use.
  • Overriding styles: For some of the components we built using MUI, starting from an unstyled base would have been easier than modifying MUI's already-styled components. In some cases, we ended up overriding most of the default styling. Occasionally, it also felt cumbersome to override MUI’s base styles vs. starting with an unstyled/less-styled component.
    • Note: we started using MUI Core components before MUI released @mui/base, which provides unstyled components. 🥳
  • Styling: Theming and styling may have a relatively steep learning curve, depending on what your team is used to. Since MUI's components can be styled a few different ways, a unified approach requires teams to establish their own standards. That said, I think MUI's methods of theming/styling promote a clear hierarchy of overrides.
    • TextField example: we substantially customize the base TextField styles in our MUI theme. Our Autocomplete wrapper component renders our TextField wrapper, and overrides a few of our theme's TextField styles. Consumers of our Autocomplete wrapper in feature code can then use the sx and className props to override styles in individual <TextField /> instances.
  • Bundle size: If you are counting kilobytes, robust third-party components will inevitably be heavier than in-house components that only include what you need. MUI supports tree-shaking to only include the components you use in your production bundles (we use this method to enforce optimized imports).
  • Cost: MUI Core is free, but MUI X contains a few components that require a paid license, e.g. DataGridPro and their full range of date pickers. The pricing seems reasonable, but many teams prefer to use exclusively free packages. Zip hasn't purchased MUI X pro, but we're still considering doing so.

Compared to some other established UI libraries: Some UI libraries feel dated, either by their design or by their API. Others aren't maintained well, lacking the open-source engagement or ownership to fix bugs or keep the package up-to-date.

Compared to newer alternatives: Some new libraries seem like exciting projects, but they typically lack either breadth, community support, or stability.

Note: We didn't consider licensed libraries like KendoReact.

Did MaterialUI solve our problems with consistency across component implementations?

No, but it’s helped! Unless you're using MUI components out-of-the-box and without customization, keeping your component library consistent and easy-to-use is still a major effort. We still struggle with:

  • Enforcing UX + code best practices: MUI has helped, since MUI now contains most of the functionality for certain library components, and sets a good consistent precedent for certain patterns like prop naming and state management. Having less core component code in our repo means we have less code to be concerned about. However, our component library adds significant styling and functionality on top of MUI (in addition to plenty of non-MUI lib components), which still needs to be held to a high standard. We’re still figuring out how to best unblock engineers who need new functionality, while maintaining a high quality bar and promoting reuse in our library.
  • Prioritizing reuse and extension: Preventing duplicate components and functionality still requires oversight, documentation/education, and an engineering/design culture that values consistency and reusability. By working to make sure eng/design are aware of our existing components and their variations, we can more intentionally reuse and extend our components.
  • Investing in our component library and DevX: Especially in a fast-paced environment, prioritizing investments at the platform level is still a challenge, even when some projects may pay for themselves in just a few months. Using MUI has made some types of investment easier, but that doesn't mean our component library doesn't need sizable investment and maintenance.
  • Design consistency: Partly because MUI's components are so easily customizable, it can be hard to prioritize consistency. Designs that are slight variations of each other can be implemented easily by engineers, without raising the question of whether the design variation is intentional or necessary. We've started a couple of practices to help encourage consistency and alignment.
    • Engineers + designers meet weekly to discuss "Components". This serves as a forum for questions around best practices and design consistency
    • We also use an allowlist for MUI props in our wrapper components, rather than extending the entirety of the MUI component prop set. This forces devs to first look at which MUI props are already used in the app. These props are more likely to be 1. props that the developer is already looking for, and 2. props that enable behavior/styling that is consistent with other usage of the component.

What would we have done differently?

Earlier migration ➡️ easier migration

Ideally, we would have built out new components in MUI as early as possible! For months, we were still using our legacy components in our rapidly growing codebase, even when we knew some of them would be fully rewritten soon. If we had built out and started using MUI-powered versions of these components earlier, we would saved substantial time:

  • Migrations to the new components would have been easier, since fewer components would need to migrated
  • Newer engineers would have never had to ramp up the quirks of our legacy components.

Being open to MUI’s default design and interaction patterns

As we were building new MUI-powered versions of our existing components, we usually aimed to replicate the exact design/behavior of the legacy component. Looking back, we may have benefitted from being more open to using MUI’s default design/interaction patterns.

Example: Form inputs

Zip's form inputs are styled such that the label is placed completely inside of the input box, and the box has a full border around it. This was a longstanding design, introduced before we had hired our first in-house designer. When rebuilding our input components with MUI, we saw that its TextField component provided three different layout options out of the box. However, none of them quite matched our implementation.

Though we were always able to customize MUI to match our existing designs, this could occasionally feel like forcing an anti-pattern on top of MUI. During migration to MUI, we may have benefitted from taking the opportunity to re-evaluate our existing designs. MUI's design decisions are often backed by usability research that startups could only dream of 😍 (example).

When should a team leverage Material UI or another third-party library?

I'd personally go so far as to say that Material UI is an excellent choice for most most orgs with between 5 and 500 engineers.

However, some products/teams may have less to gain from using a third-party component library. Perhaps:

  1. They have plenty of frontend and design bandwidth available to build your own component system from scratch (e.g. large consumer tech companies)
  2. They have very specific design needs (e.g. large consumer tech companies)
  3. They are very concerned about bundle size (e.g. large consumer tech companies)
  4. They have a relatively static/simple application (maybe fire your frontend engineers and use Webflow?)
  5. You like the appearance and minimal functionality of early 2000's web applications

Building a component library from the ground up can be tempting, and is often seen as what companies should do when they have the resources. But given the rising complexity of frontend applications and the availability of high-quality open-source component libraries, building a component library from scratch feels increasingly hard to justify.

Further reading:

Atomic design:

Some Material UI history:

Share this post