styled-components, one more time
Tags: react, webdev, javascript
Published: 2019-06-07 21:18:43

---
title: styled-components, one more time
published: true
description: Add more semantics to your React components
tags: [react, webdev, js]
cover_image: https://thepracticaldev.s3.amazonaws.com/i/992rru8tp6yatakjmhn8.png
---

**UPD**: [About div vs button](https://dev.to/stereobooster/debate-that-never-happened-5b98).

Originally [styled-components](https://github.com/styled-components/styled-components) as a library appeared in 2016. The idea was proposed by [Glen Maddern](https://github.com/geelen). It is [CSS-in-JS](https://github.com/stereobooster/css-in-js-101) solution, but in my opinion, the most powerful part is not CSS-in-JS, but the ability to create small components fast.

Consider following snippet:

```js
<h1 style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}>
  Hello World, this is my first styled component!
</h1>
```

vs

```js
const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

<Title>Hello World, this is my first styled component!</Title>;
```

As you can see there is more semantics in this code. You can easily tell that it is a title, it can be hard to tell otherwise (in case of `h1` it is possible to guess, but if it would be `div`?..).

We can for sure do the same without styled-components:

```js
const Title = ({ children }) => (
  <h1
    style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}
  >
    {children}
  </h1>
);
```

But let's face it: nobody does it. Where is with styled-components it happens naturally. This is simplified version, it doesn't pass props, it doesn't handle `ref` property.

styled-components pair nicely with a11y. For example, instead of this:

```js
<>
  <div
    role="button"
    aria-expanded={expanded}
    aria-controls={sectionId}
    id={labelId}
    className={styles.Label}
    onClick={() => onToggle && onToggle(index)}
  >
    {title}
    <span aria-hidden={true}>{expanded ? "▲" : "▼"}</span>
  </div>
  <div
    role="region"
    aria-labelledby={labelId}
    id={sectionId}
    hidden={!expanded}
    className={styles.Panel}
  >
    {expanded && (isFunction(children) ? children() : children)}
  </div>
</>
```

we can write

```js
const Label = styled.div("Label");
Label.defaultProps = { role: "button" };

const Panel = styled.div("Panel");
Panel.defaultProps = { role: "region" };

<>
  <Label
    aria-expanded={expanded}
    aria-controls={sectionId}
    id={labelId}
    onClick={() => onToggle && onToggle(index)}
  >
    {title}
    <span aria-hidden={true}>{expanded ? "▲" : "▼"}</span>
  </Label>
  <Panel aria-labelledby={labelId} id={sectionId} hidden={!expanded}>
    {expanded && (isFunction(children) ? children() : children)}
  </Panel>
</>;
```

Nicer isn't it?

## Alternatives

This idea is so popular that it got copied by other libraries.

### CSS-in-JS

- [Emotion](https://emotion.sh/docs/styled)
- [JSS](https://github.com/cssinjs/styled-jss)

### "Zero-runtime" CSS-in-JS

- [Linaria](https://github.com/callstack/linaria#syntax)

### style property

- [unstyled-components](https://github.com/stereobooster/unstyled-components)

### CSS modules

- [css-modules-components](https://github.com/stereobooster/css-modules-components)
- [styled-style](https://github.com/akameco/styled-style)
- [css-to-js-loader](https://github.com/theKashey/css-to-js-loader)

## DIY

Let's write (very) simplified implementation of styled-components, to demystify things a bit. So we started with this:

```js
const Title = ({ children }) => (
  <h1
    style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}
  >
    {children}
  </h1>
);
```

1) we need to pass props:

```js
const Title = ({ children, ...props }) => (
  <h1
    style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}
    {...props}
  >
    {children}
  </h1>
);
```

2) We need to handle `ref`

```js
const Title = React.forwardRef(({ children, ...props }, ref) => (
  <h1
    style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}
    ref={ref}
    {...props}
  >
    {children}
  </h1>
));
```

3) Let's make tag configurable with `as` property (some libraries call it `is` or `use` as well):

```js
const Title = React.forwardRef(({ children, as = "h1", ...props }, ref) =>
  React.createElement(
    as,
    {
      style: { fontSize: "1.5em", textAlign: "center", color: "palevioletred" },
      ...props,
      ref
    },
    children
  )
);
```

4) Let's add a way to override styles

```js
const Title = React.forwardRef(({ children, as = "h1", ...props }, ref) =>
  React.createElement(
    as,
    {
      ...props,
      style: {
        fontSize: "1.5em",
        textAlign: "center",
        color: "palevioletred",
        ...props.style
      },
      ref
    },
    children
  )
);
```

5) Let's generate component

```js
const styled = defaultAs =>
  React.forwardRef(({ children, as = defaultAs, ...props }, ref) =>
    React.createElement(
      as,
      {
        ...props,
        style: {
          fontSize: "1.5em",
          textAlign: "center",
          color: "palevioletred",
          ...props.style
        },
        ref
      },
      children
    )
  );

const Title = styled("h1");
```

6) Let's pass default styles from outside

```js
const styled = defaultAs => defaultStyles =>
  React.forwardRef(({ children, as = defaultAs, ...props }, ref) =>
    React.createElement(
      as,
      {
        ...props,
        style: {
          ...defaultStyles,
          ...props.style
        },
        ref
      },
      children
    )
  );

const Title = styled("h1")({
  fontSize: "1.5em",
  textAlign: "center",
  color: "palevioletred"
});
```

7) Add display name to the component

```js
const styled = defaultAs => defaultStyles => {
  const component = React.forwardRef(
    ({ children, as = defaultAs, ...props }, ref) =>
      React.createElement(
        as,
        {
          ...props,
          style: {
            ...defaultStyles,
            ...props.style
          },
          ref
        },
        children
      )
  );
  component.displayName = `${defaultAs}💅`;
  return component;
};
```

8) Make styles customizable depending on properties

```js
const isFunction = x => !!(x && x.constructor && x.call && x.apply);

const styled = defaultAs => defaultStyles => {
  const component = React.forwardRef(
    ({ children, as = defaultAs, ...props }, ref) =>
      React.createElement(
        as,
        {
          ...props,
          style: {
            ...(isFunction(defaultStyles)
              ? defaultStyles(props)
              : defaultStyles),
            ...props.style
          },
          ref
        },
        children
      )
  );
  component.displayName = `${defaultAs}💅`;
  return component;
};

const Title = styled("h1")(() => ({
  fontSize: "1.5em",
  textAlign: "center",
  color: "palevioletred"
}));
```

9) Filter non-html properties, let's filter out all properties in `propTypes`:

```js
const filterObject = (rest, shouldForwardProp) =>
  Object.keys(rest)
    .filter(shouldForwardProp)
    .reduce((obj, key) => {
      obj[key] = rest[key];
      return obj;
    }, {});

const styled = defaultAs => defaultStyles => {
  const component = React.forwardRef(
    ({ children, as = defaultAs, ...props }, ref) =>
      React.createElement(
        as,
        {
          ...(component.propTypes
            ? filterObject(props, key =>
                Object.keys(component.propTypes).includes(key)
              )
            : props),
          style: {
            ...(isFunction(defaultStyles)
              ? defaultStyles(props)
              : defaultStyles),
            ...props.style
          },
          ref
        },
        children
      )
  );
  component.displayName = `${defaultAs}💅`;
  return component;
};
```

10) Bonus. Let's use `Proxy` instead of the first function:

```js
const styled = Proxy(
  {},
  {
    get: (_, defaultAs, __) => defaultStyles => {
      const component = React.forwardRef(
        ({ children, as = defaultAs, ...props }, ref) =>
          React.createElement(
            as,
            {
              ...(component.propTypes
                ? filterObject(props, key =>
                    Object.keys(component.propTypes).includes(key)
                  )
                : props),
              style: {
                ...(isFunction(defaultStyles)
                  ? defaultStyles(props)
                  : defaultStyles),
                ...props.style
              },
              ref
            },
            children
          )
      );
      component.displayName = `${defaultAs}💅`;
      return component;
    }
  }
);

const Title = styled.h1({
  fontSize: "1.5em",
  textAlign: "center",
  color: "palevioletred"
});
```

Now you know what is inside!