15

I'd like to have a component, styled with Emotion, that takes props that ultimately control the styling. For example, consider a GridCol component that has various props that change the padding and width (the width can be changed across different viewport widths).

I'd like to use an API like this:

<GridCol gutter size="2>

// or alternatively, like this:

<GridCol gutter size={{
  m: 2,
  l: 4
}}>

There are three things happening here:

  • the gutter is a boolean prop that adds some horizontal padding to the column
  • the size prop can be a string or an object. If it is a string, we just add a few lines of CSS and we're good, however, if it is an object, we need to insert some media-queries based on a breakpoints object that is set elsewhere.

Emotion's docs are not clear how to handle styling of this nature, at least I have not seen it, so I was hoping that a common solution could be found.

For the gutter prop, it is trivial:

const GridCol = props => styled('div')`
  display: block;
  box-sizing: border-box;
  flex: 1 0 0;
  min-width: 0;
  padding: ${props.gutter ? `0 10px` : '0'};
`

For the size prop, it becomes more complicated, I'd like the resultant CSS to look something like this:

const GridCol = props => styled('div')`
  display: block;
  box-sizing: border-box;
  flex: 1 0 0;
  min-width: 0;
  padding: ${props.gutter ? `0 10px` : '0'};

  /* styles here if  `size` is a string */
  width: 10%;

  /* styles here if  `size` is an object */
  @media screen and (min-width: 500px) {
      width: 20%;
  }


  @media screen and (min-width: 800px) {
      width: 30%;
  }


  @media screen and (min-width: 1100px) {
      width: 40%;
  }
`

The width values will be determined by the prop's key, which corresponds to a value in a breakpoints object, this part is not trivial, but I don't know how to dynamically generate the css needed.

I'm sure there's more info that I could add, I have made some attempts but none of them are working at the moment. My feeling is that I should create a stateless functional component that generates the css for each condition, then joins the CSS at the end..

Zander
  • 2,471
  • 3
  • 31
  • 53

3 Answers3

31

This is a great question. First, avoid this pattern.

const GridCol = props => styled('div')`
  display: block;
  box-sizing: border-box;
  flex: 1 0 0;
  min-width: 0;
  padding: ${props.gutter ? `0 10px` : '0'};
`

In this example, a new styled component is created on every render which is terrible for performance.

Any expression, or interpolation, can be a function. This function will receive 2 arguments: props and context

const GridCol = styled('div')`
  display: block;
  box-sizing: border-box;
  flex: 1 0 0;
  min-width: 0;
  padding: ${props => props.gutter ? `0 10px` : '0'};
`

As for the size prop in your example, I would use the following pattern.

import { css } from 'emotion'

const sizePartial = (props) => typeof props.size === 'string' ?
  css`width: 10%;` :
  css`
   @media screen and (min-width: 500px) {
      width: 20%;
   }


   @media screen and (min-width: 800px) {
      width: 30%;
   }


   @media screen and (min-width: 1100px) {
      width: 40%;
   }
 `

You can then use the partial just like any other function that occurs in an expression.

const GridCol = styled('div')`
  display: block;
  box-sizing: border-box;
  flex: 1 0 0;
  min-width: 0;
  padding: ${props => props.gutter ? `0 10px` : '0'};
  ${sizePartial};
`

This is an extremely powerful pattern that can be used to compose reusable dynamic styles across your project.

If you are interested in other libraries that leverage this pattern check out https://github.com/emotion-js/facepaint and https://github.com/jxnblk/styled-system

tkh44
  • 370
  • 6
  • 11
  • This answer is a few years old now. Does it still hold true, specifically concerning performance? – skilar Jan 10 '20 at 18:12
  • 2
    With regards to creating a new styled component on every render, yes it still holds true. Just as you would not want to create any other component in the render function of another component, you do not want to create a styled component on every render. – tkh44 Jan 23 '20 at 23:55
12

If you are looking for a way to implement conditional styles with Emotion's css prop, you can do something like this:

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

const styles = ({ isSelected }) => css`
  border: solid 1px black;
  border-radius: 10px;
  padding: 16px;
  cursor: pointer;
  ${isSelected === true &&
  `
    background-color: #413F42;
    color: white;
  `}
`;

const ConditionalComponent = () => {
  const [isSelected, setIsSelected] = useState(false);
  return (
    <div css={styles({ isSelected })} onClick={() => setIsSelected(!isSelected)}>
      Click here to change styles.
    </div>
  );
};

More details on this here: https://seanconnolly.dev/emotion-conditionals

Sean
  • 1,159
  • 1
  • 15
  • 21
  • Have you found a solution to make syntax highlighting work inside the condition? I am using VS Code, and unfortunately I haven't figured out a way to circumvent this problem. I installed the extension vscode-styled-components which works great except for the css definition inside the condition. – Michael Aug 29 '21 at 22:38
  • 1
    @Michael I make it work using css object again, like this by example ``` &.circle { border-radius: 100%; &.selected { background-color: #ff4e4e; ${32 % 2 > 3 && css` background-color: green; `} } }``` – Juan Salvador Portugal Sep 07 '22 at 19:43
  • Interesting! I would have not thought, that this is possible. I will give it a try. – Michael Sep 08 '22 at 20:40
4

Emotion has a helper called cx that provides functionality similar to the popular classnames library. You can use this to write conditionals more easily:

import { cx, css } from '@emotion/css'

const cls1 = css`
  font-size: 20px;
  background: green;
`

const foo = true
const bar = false

const SomeComponentWithProp = ({ foo }) => (
  <div
    className={cx(
      { [cls1]: foo === 'bar' },
    )}
  />
);

(Adapted from the linked docs)

Jack Guy
  • 8,346
  • 8
  • 55
  • 86