With hundreds of engineers, developers and designers working on Yelp, ensuring visual consistency across Yelp is a challenging task. We’ve been migrating our web components from Yelp Cheetah to React to increase designer and developer productivity while ensuring visual consistency across our web app. Along the way, we built Lemon Reset - a package containing consistent, cross-browser React DOM tags, powered by CSS Modules.

Since our design system components are the building blocks of our frontend, we had to port them to React as the first step before our developers could port their features. We made a lot of design decisions about these new components, including how to tackle the problem of CSS in React.

In a traditional web stack, where CSS and markup files are independent, we applied styles using selectors that target DOM elements and classes. Those selectors have infinite inheritance thanks to the cascading part of cascading style sheets - arguably the best and worst part of CSS. That means that we had to manually scope our class names using a BEM naming convention to avoid naming collisions and specificity wars. Relying on human conventions such as manual namespace scoping doesn’t scale as an organization and codebase grows.

As we added more and more new features at a faster rate, it also became more difficult for developers to track of all the stylesheets they needed to import for their pages. When we write the markup for a button, we assume that button.css will also be loaded whenever we use a Button component. Since our components couldn’t declare dependencies on CSS, it was up to developers to ensure that the styles they needed for their components were included in the CSS package for the template.

Template file:

<link rel="stylesheet" type="text/css" href="web-pkg.scss">

<button class=”btn”>Wow!</button>

web-pkg.scss

import 'yelp_styleguide/assets/scss/lib/buttons';
import ‘cool_feature’;

yelp_styleguide/assets/scss/lib/_buttons.scss

@import ‘colors’;

.btn {
    background: $yelpy-red;
}

_cool_feature.scss

.my-feature {
...
}

To tackle some of these issues in our ever-growing frontend codebase, we created our design system with reusable components. Rather than writing every component from scratch, developers can build UIs by simply gluing together prefabricated components, allowing us to more quickly iterate and build a visually consistent product.

But the consistency we wanted was unenforceable due to the cascading nature of CSS. Our consistent class names allowed developers to deviate from our design system by creating new, more specific CSS rules, making it more difficult for us to maintain a consistent visual language.

.btn {
    color: white;
}
.btn--primary {
    background: red;
}
<button class=”btn btn--primary”>Wow!</button>

A red button

That means that our standard components might not always be so standard in usage, as things like our color guidelines could easily be overridden by one line of rogue CSS.

.btn.btn--primary {
    background: blue;
}

A blue button

With hundreds of developers across many feature teams, our frontend infrastructure team can’t be on every code review that touches an SCSS file. After all the effort put into standardizing our components, one line of CSS could still sneak its way into the codebase and turn our red buttons into blue buttons!

React to the rescue!

In a React world, we’re able to package together JSX with the styling it requires. Our React components explicitly declare dependencies on the CSS they need, which means that we can statically extract all the styles needed on a page so our developers no longer have to manually ensure that the CSS their components need is included!

Button.js

import styles from'./Button.scss';

<Button className={styles.button}/>

Button.scss

.button {
    background: red;
}

Additionally, instead of relying on human naming conventions, we can use CSS modules to provide automatic scoping for class names. That means we no longer have to worry about naming and styling conflicts!

This all sounds too good to be true, so what’s the catch?

If everything in CSS Modules becomes namespaced, how do we share basic styles that we want to include across all our components’ pages? In a markup and CSS world, we provided Meyer Reset as a global style sheet with global styles that targeted DOM elements, such as h1 for the Yelpy red headings we know and love; however, any CSS selector targeting a DOM element instead of a class can’t be scoped, and scoping is the whole point of CSS modules!

h1 {
    font-size: $h1-font-size;
    color: $yelpy-red;
}

The Yelpy red headings we all know and love

A few options we considered initially:

We could just do what we used to and include a package with styles that we would want globally, such as Meyer Reset, typography styles, and our grid layout scss. We could declare a dependency for all of our components on the global style package, let Webpack resolve which styles we need, and dump those styles as a global on the page, but having global side effects seems gross when we can explicitly express dependencies on our CSS.

Global styles would also pollute the entire page and make it impossible to use a Yelp button in isolation without affecting other styles on the page. Yelp Wifi and Yelp Reservations are separate products from the consumer Yelp app, so it’s important for them to be able to borrow components as they see fit rather than be forced to adopt our entire design system.

So what’s a core web team to do?

The Solution

We needed a solution that would satisfy two key requirements:

  1. Components should be able to be used in isolation from one another

  2. Components need to be generic enough for developers to use across the site, but consistent enough that they adhere to our design system

The solution we decided on was to create components for everything, and to style our components in two layers:

  1. Basic components for HTML tags that would handle baseline styles for browser resets.

  2. Components for our design system that abstract away the underlying DOM elements and expose a predetermined set of styles for developers to use.

Layer 1: Lemon Reset

The first layer of components should contain the basic styling that every component needs: a reset stylesheet for browser consistency. We didn’t find any existing open source library that provided basic React components with Eric Meyer’s popular CSS reset stylesheet Meyer Reset styles built in for use with CSS Modules, so we built our own.

This first layer of components became Lemon Reset, now open sourced for your convenience!

For every element in Meyer Reset, we generated a React Component:

export const LemonReset = ({ tag: Tag, children, className, tagRef, ...otherProps }: Props) => (
    <Tag className={classNames(styles\[\`lemon--${Tag}\`\], className)} ref={tagRef} {...otherProps}>
        {children}
    </Tag>
);

export const H1 = ({ children, className, ...otherProps }: TagProps) => (
    <LemonReset tag="h1" className={className} {...otherProps}>
        {children}
    </LemonReset>
);

The “LemonReset.H1” component is styled with the reset CSS for h1 from Meyer Reset, and that’s it. The second layer of abstraction components can then use these reset components internally by passing in all necessary props down to the underlying HTML element.

Layer 2: Design System Components

Components need to be generic enough for developers to use across the site, but consistent enough that they adhere to our design system.

Inspired by React Primitives, we created these Abstraction Components encapsulating Lemon Reset for our developers to use, such as:

  • <Text>

  • <Heading>

  • ` `

  • <Button>

  • And 65 other components!

An example of one of our design system components

An example of one of our design system components

This allows us to constrain the patterns that appear within our UI. Developers are required to choose values from our design system which we provide for props like font size, color, margin, padding, and background-color. These standards help us create visual consistency across Yelp and surface existing solutions for design needs.

Learn more about our design system at yelp.com/styleguide

Learn more about our design system at yelp.com/styleguide

By using CSS Modules and providing abstraction components with built-in styles from our design system for our developers to use, we can increase designer and developer productivity and ensure visual consistency across our web app.

Become a Software Engineer at Yelp

Want to help us make even better tools for our full stack engineers?

View Job

Back to blog