Targeting nested elements with Emotion

CSS in JS is all the rage, but how do you target nested components?

2020-02-05

Metadata
Targeting nested elements with Emotion
CSS in JS is all the rage, but how do you target nested components?
2020-02-05
./emotion.jpg
path
javascriptreactsoftwarecssemotionweb

TL;DR

Using a pattern of CSS overrides in Emotion can enable you to target nested components within a React application.

  const componentStyles = css`...`
  const overrideStyles = css`...`

  ...

  <div css={componentStyles}>
    <MyComponent cssOverrides={overrideStyles} />
  </div>

Problem

It is a common pattern to use CSS to target elements nested within another element, in order to only change the appearance of the element within a certain context. Using a CSS in JS solution such as Emotion makes this difficult because you end up with generated class names that are difficult to target when writing CSS.

This problem and solution were dependent on using the css prop provided by emotion. This is my preferred use of emotion but this problem would exist in any tool that uses generated class names.

As an example, let's assume we have two components,

A button component

  const buttonStyles = css`...`

  function Button() {
    return <button css={buttonStyles}>Button</button>
  }

Then a card component that renders a button

  const cardStyles = css`...`

  function Card() {
    return (
      <div css={cardStyles}>
        <Button />
      </div>
    )
  }

Our button may normally have a red outline, but when in a modal, we need it to have a green outline. We could potentially write a rule targeting the button element. But what if it was a <div>, or a <p>? We don't know what the class name will be applied to the button element, so we can't target the class name. Or can we?

Functional, just not great

With the css named template string, it actually returns an object describing the resulting stylesheet that was created. It is possible to access most of the generated class name through the object returned from a call to css.

  const cardStyles = css`
    & .css-${buttonStyles.name} {
      border: 1px solid green;
    }
  `

This way we have access to the generated class name and can apply conditional styling whenever a Button appears nested within a Card.

But this solution, while works, is not ideal. We are depending on an API that is not well documented and is bound to change. At some point, the name property could include the css- prefix. Or the property name could change to something else. It just is not very reliable. Which is why I prefer an override instead.

cssOverrides

Rather than depending on an internal Emotion API, I have gone the route of making my components accept a cssOverrides prop that I then apply inside of my component. Any time my Button is rendered, a cssOverrides can be provided, and it will be used in conjunction with my default styles.

const buttonStyles = css`...`

function Button({ cssOverrides }) {
  return <button css={[buttonStyles, cssOverrides]}>Button</button>
}
const cardStyles = css`...`
const buttonOverrideStyles = css`...`

function Card() {
  return (
    <div css={cardStyles}>
      <Button cssOverrides={buttonOverrideStyles} />
    </div>
  )
}

With this solution, if emotion were to change their API, our styles should be largely unaffected. We don't have to worry about generated class names resulting in a more robust error resistant solution.