Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
CSS in JS - Emotion Guidelines

CSS-in-JS

This repo uses a CSS-in-JS library called Emotion for its styling.

Why Emotion?

Emotion is a performant and flexible CSS-in-JS library. Building on many other CSS-in-JS libraries, it allows us to style apps quickly with string or object styles. It has predictable composition to avoid specificity issues in CSS. With source maps and labels, Emotion has a great developer experience and performance with heavy caching in production.

Also, Material UI v5 will most likely use Emotion instead of JSS:
material-ui - [RFC] v5 styling solution

Styling with Emotion

  • Use the css prop with template literals (don't use styled).
  • Define styling outside of the component.
    • Less clutter, easier to maintain and refactor.
    • Makes it easier if we'd like to change our styling library if needed.
  • Prefer creating small styling blocks instead of using CSS selectors.
    • Keeps the code modular and avoid specificity issues,
    • CSS selectors are still useable, but note that they affect the entire sub-tree.

Basic Usage

import { css } from '@emotion/react';

import { colors } from 'styles/theme';

const someComponentCss = css`
  background-color: ${colors.offWhite};

  /* Will affect all descendent "a" elements, use only when it's safe */
  a {
    color: ${colors.brandPrimary};
  }
`;

const titleCss = css`
  font-size: 3rem;
`;

function SomeComponent() {
  return (
    <div css={someComponentCss}>
      <h1 css={titleCss}>Some heading</h1>
      <p>
        Click <a href="#">here</a> to see something nice.
      </p>
    </div>
  );
}

export default SomeComponent;

Passing Props

import { css } from '@emotion/react';

const titleCss = fontSize => css`
  font-size: ${fontSize}rem;
`;

function SomeComponent({ fontSize }) {
  return (
    <div>
      <h1 css={titleCss(fontSize)}>Some heading</h1>
    </div>
  );
}

export default SomeComponent;

Conditional CSS

  • Conditional CSS blocks should be wrapped with css to support intellisense
import { css } from '@emotion/react';

const buttonCss = isActive => css`
  /* Conditional ternary value */
  background-color: ${isActive ? 'blue' : 'white'};

  /* Conditional value, if falsy, the property will not be set at all */
  z-index: ${isActive && 100};

  /* Conditional block of CSS */
  ${isActive &&
  css`
    transform: scale(2);
    color: white;
  `}
`;

function SomeComponent({ isActive }) {
  return (
    <div>
      <button css={buttonCss(isActive)}>Do Something</button>
    </div>
  );
}

export default SomeComponent;

Mobile-First Approach

  • Follow the suggestions in Avoiding CSS overrides in responsive components.
  • The default CSS that we write (outside any media query) targets all viewports.
  • Any property that would be overwritten on tablet viewport should be inside mobileOnly.
  • The media query helpers are defined in styles/utils.ts.
import { mobileOnly, tabletUp, desktopUp } from 'styles/utils';

const compNameCss = css`
  display: flex;

  ${mobileOnly} {
    flex-direction: column;
  }

  ${tabletUp} {
    flex-direction: row;
    padding: 20px;
  }

  ${desktopUp} {
    padding: 40px;
  }
`;

Styling Material-UI components

  • Don't use MUI styling (JSS) at all.
  • Override styles using the relevant selectors for each component.
    • Each MUI component documents the available class names in the doc (e.g. MUI Alert API)
    • Some components are just wrapper over other components (e.g. MUI TextField)
// This is a wrapper component with the same name as the MUI component.
import { Alert as MuiAlert } from '@material-ui/lab';
import { css } from '@emotion/react';

import { colors } from 'styles/theme';
import { setOpacity } from 'styles/utils';

const muiAlertCss = css`
  &.MuiAlert-root {
    padding: 10px;
    font-size: 1.8rem;
    justify-content: center;
    border-radius: 7px;

    .MuiAlert-message {
      padding: 0;
      text-align: center;
    }

    /* Error */
    &.MuiAlert-standardError {
      color: ${colors.dangerRed};
      background-color: ${setOpacity(colors.dangerRed, 0.1)};
    }

    /* Success */
    &.MuiAlert-standardSuccess {
      color: ${colors.successGreenText};
      background-color: ${setOpacity(colors.successGreenBg, 0.1)};
    }
  }
`;

function Alert({ children, ...restProps }) {
  return (
    <MuiAlert css={muiAlertCss} {...restProps}>
      {children}
    </MuiAlert>
  );
}

Detached Class Names

In case we need to add a class name in CSS but not necessarily pass it directly to a component via css, we can use Emotion's Class Names.

The css from ClassNames just adds the given styles into a <style> tag in the HTML and returns the class name string. This is useful, for example, with MUI classes, which adds class names on top of inner elements. Usually we won't need it, but some components like Autocomplete have inner elements that are rendered in a React Portal outside the component's tree, which means that are our styling convention above won't work for those elements.

// This is a wrapper component with the same name as the MUI component.
import { Autocomplete as MuiAutocomplete } from '@material-ui/lab';
import { css, ClassNames } from '@emotion/react';

const muiAutocompleteCss = css`
  .MuiTouchRipple-root {
    display: none;
  }
  .MuiButtonBase-root:hover {
    background-color: transparent;
  }
`;

/**
 * We use `classes` since the Autocomplete Popper is opened in a React Portal,
 * thus can't be reached with our regular CSS.
 */
const classesCss = css => ({
  paper: css`
    &.MuiPaper-root {
      font-size: 1.8rem;
      font-weight: 300;
      margin: 2px 0;
    }

    .MuiAutocomplete-listbox {
      padding: 10px 0;
    }

    .MuiAutocomplete-option {
      padding: 7px 18px;
    }
  `,
});

function Autocomplete(props) {
  return (
    <ClassNames>
      {({ css }) => (
        <MuiAutocomplete
          css={muiAutocompleteCss}
          classes={classesCss(css)}
          {...props}
        />
      )}
    </ClassNames>
  );
}
@alexilyaev

This comment has been minimized.

Copy link
Owner Author

@alexilyaev alexilyaev commented Dec 10, 2020

Why not Emotion Styled?

It's a good question...
Well, the quick answer would be "I tried Styled and didn't like it" 😀.

But I could think of some reasons:

  • For me, using Styled was breaking a mental model I was used to. So I found myself jumping up and down the code to see which element something is.
  • More concepts to think about instead of a single primitive.
  • With css I build styling blocks and apply them where I want and can reuse the same block for different elements.
    With Styled I need to think of the element and composition when I'm thinking about styling.
  • More variable naming challenges, how do I call a CheckoutProgress that I want to style, CheckoutProgressStyled?
    It's technically similar with the css prop, but I find it easier to name, e.g. checkoutProgressCss.
  • I found the css prop more flexible, either passing a reference to a css block or calling a function and passing what I need.
    e.g. I can pass something to the styling block that is not meant to be part of the props.
    Especially with TypeScript passing extra props just for the styling with Styled adds more work (I think).

I suppose there are downsides to the css prop:

  • Requires some setup (Babel plugin that transforms the prop to className).
  • When building reusable components and accepting className from the parent, it's important to forward className after the internal css prop usage, otherwise the css one will override the parent's className.

Why not styled-components?

Emotion can be used just like styled-components, see:
https://emotion.sh/docs/styled

MUI v5 will most likely use Emotion, and they compared a bunch of solutions:
mui-org/material-ui#22342

I see no benefit of using styled-components other than it's more popular.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment