DEV Community

Cover image for styled-components, one more time
stereobooster
stereobooster

Posted on • Edited on

styled-components, one more time

UPD: About div vs button.

Originally styled-components as a library appeared in 2016. The idea was proposed by Glen Maddern. It is CSS-in-JS 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:

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

vs

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:

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:

<>
  <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

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

"Zero-runtime" CSS-in-JS

style property

CSS modules

DIY

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

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

1) we need to pass props:

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

2) We need to handle ref

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):

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

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

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

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

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

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:

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:

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!

Top comments (22)

Collapse
 
jensgro profile image
Jens Grochtdreis

After reaching your example with the DIV creating a button I stopped and the whole article rendered useless.
Why don't you use a button? How could I tell that the rest of the article is correct when you propose such a terrible code?
In cases like this I wish for a page with peer review, not the possibility for everyone publishing and telling the newbies to write bad code.

Collapse
 
stereobooster profile image
stereobooster

Here is The button for you.

Collapse
 
jensgro profile image
Jens Grochtdreis

If you DO know that your code is wrong, why not write it good in the first place? This site is about learning and teaching. Teach good code not wrong code.

Thread Thread
 
stereobooster profile image
stereobooster

What is the difference between <div role="button" tabindex="0"> and <button>?

Thread Thread
 
jensgro profile image
Jens Grochtdreis

There is a huge difference. First of all, a DIV is a meaningless construct. Even those two attributes will not mimic all the built-in functions, a button has. A button comes with multiple functions and abilities, that you must rebuild with JavaScript, if you don't use a button-element.

There is a great article on smashingmagazine: smashingmagazine.com/2019/02/butto...

One of the first rules of frontend-development is to use the thing that is supposed to be used. So if you need a button and don't use one, you are not lazy, because you would have to care for many things, but you don't have enough knowledge. Because using a div or span instead of a button is nothing more than bad craftsmanship. But with a little more caring for the details this will be fine. Just don't believe, that only JS is important and HTML and CSS aren't necessary.

If you use semantic HTML in the way it was meant you are free of many unneccessary thoughst regarding JS-workarounds. And writing <button type="button"> is easier and faster than <div class="button" role="button" tabindex="0"> and all the supporting JS.

But the most important take-away for you should be to read the smashingmagazine-article. Please do!

Thread Thread
 
stereobooster profile image
stereobooster

So no difference you say. You wrote that long message and didn't make one (at least one) simple example that would explain the difference.

Strong opinion weakly held.

Thread Thread
 
jensgro profile image
Jens Grochtdreis

You didn't read my response? I told you that there IS a difference. A huge difference. And as this difference is huge the article reflects it better than I could in a comment.

Your code is a meaningless div which is shaped into something that could be a button. And only with many JS will be. As a button has already everything built in and works as intended.

To be clear: someone who uses your code tells me, he doesn't care for frontend. That's the reason I stopped reading. But you could be helped. HTML and CSS aren't by far as complicated as JS or PHP. The basics are simple to grasp if you understand, that it has NOTHING to do with programming.

Thread Thread
 
stereobooster profile image
stereobooster • Edited

You have so strong feelings about it, so harsh response, so I thought you have something explicit on your mind. I thought you are an expert in the subject that is why you got triggered. So I asked the question: "hey what the difference?". And you sent me a link to smashing magazine. Thanks, but I know how to search the internet myself, as well I'm familiar with smashing magazine I was explicitly interested in your opinion, why so strong feelings.

First of all, a DIV is a meaningless construct

Isn't <div role="button" add meaning?

Even those two attributes will not mimic all the built-in functions, a button has

Which one won't be mimicked?

If you use semantic HTML

Semantic is meaning. Isn't <div role="button" have the same meaning as a button?

And writing <button type="button"> is easier and faster than <div class="button" role="button" tabindex="0">

true

Thread Thread
 
jensgro profile image
Jens Grochtdreis

Oh yes, I AM an expert in this field. But the comment to an article is not the place to write an article, itself. And seeing this code I thought you could need a hint to a good article.

It's good that you know smashingmagazine but as they publish loads of article, this one must have slipped your eyes. Otherwise your example wouldn't be as criticised.

And as I pointed you to the article I don't see a use in telling you all the built-in functions, a button has. Read the artice, it is really good.

I do have a strong opinion on this because I know HTML and CSS very good. And I am teaching and writing articles for more than 13 years. So I do know how to write an article. And I am alsways amazed about articles with huge faults in them.

As I am not good in JavaScript and all those frameworks I rely on articles as yours. But reading those articles and learning from others is a matter of trust. So if I do know that a part of an article is wrong and is bad code, how am I to trust the rest? Maybe the rest is okay by itself. The article wouldn't on the other hand be a guiding light for my own learning experience because I wouldn't be able to follow your code (as I know HTML better).

And for your questions about the role: Read something short about WAI-ARIA. The first rule of WAI-ARIA is: "Don't use WAI-ARIA!". Meaning: if there is somiething in HTML which can be used, use it. So, don't mimic a button, use a button. There are five simple rules of ARIA. And none reads: Make anything out of a DIV with a sprinkle auf attributes.

ARIA-attributes are a help where help is needed. And in case of a button or a haeding, noe help is needed. <div role="heading">THis is dumb code!</div> isn't good either.

Is it clearer now? And please read the article.

Thread Thread
 
stereobooster profile image
stereobooster • Edited

Can you be more specific? Like, if you would use <div role="button">, instead of a button in this browser (or screen reader), exactly this functionality will not work as expected.

As of now, it sounds like a matter of taste.

The first rule of WAI-ARIA is: "Don't use WAI-ARIA!"

Are there real consequences of using one over another? Better browser support, maybe?

You just repeat common phrases (in all 3 comments without explaining): "huge difference" (what?), "built-in functions" (what are those?), " is bad code" (based on which metrics?)

Can you be more specific?

Thread Thread
 
jensgro profile image
Jens Grochtdreis

As I told you multiple times before: read the smashingmagazine article. It shows the mannifold aspects of a button that aren't recreated by a simple attribute.

Besides: if you know it should be a button and you use an attribute, why not use the element in the first place?

That's not a matter of taste. It's a matter of using something as intended. There maybe something similar in JS, but maybe not. In HTML there are elements with a certain meaning. If they were not you could just write a page with div. Funny thing is: you could. But it would be bad code. And tahat is not a matter of taste. Because if there is something that should be used as intended and you don't use it, but otherwise use another element which you try to change as much as it is possible to mimic the originally intended element. How would you call that? Interesting idea? Crativity?

I call it bad code. And bad code should only be a vehicle in an article to tell you, what NOT to write.

And please, if you would only read the article and read something about WAI-ARIA you would understand my "common phrases". I don't intend top write a second article in the comments. I only tell you, where in my opinion you are wrong and where you can find guidance to be better. We all should thrive to be better. If you don't get this sentiment, all other comments from my side would be useless.

I should write an article about writing an article.

Thread Thread
 
stereobooster profile image
stereobooster

It is ok to not know everything.
It is ok to say I got triggered never mind.
It is ok to say this is the best practice (mantra, rule of thumb), which I follow and never questioned myself.
It isn't ok to call code bad without explaining why. I can quote Kurt Vonnegut on this: "Dr. Hoenikker used to say that any scientist who couldn't explain to an eight-year-old what he was doing was a charlatan".

I don't intend top write a second article in the comments

I didn't ask you to write an article I asked to pinpoint at least one example to show the difference between button and div role="button".

Thread Thread
 
jensgro profile image
Jens Grochtdreis

Read the linked article. The author explaines everything you need.

Thread Thread
 
stereobooster profile image
stereobooster

ok

Thread Thread
 
suckup_de profile image
Lars Moelleken

Button

  • keyboard friendly
  • disabling via fieldset
  • different states
  • accessible by default
Thread Thread
 
gunnarbittersmann profile image
Gunnar Bittersmann

What’s the difference between <button type="button"> and <div class="button" role="button" tabindex="0">, you ask? It’s the behavior.

In her talk “ARIA, accessibility APIs & coding like you give a damn!”, Léonie Watson demonstated what it would take to turn a <div> into a button. And it takes a lot! (VideoSlides)

TL;DR: Don’t do it! Use the <button> element.

Collapse
 
stn1978 profile image
Stefan Nitzsche

Everything Jens said. Bad craftsmanship. RTFM.

Thread Thread
 
stereobooster profile image
stereobooster • Edited

meme about salt chief saying craftsmanship

Thread Thread
 
stn1978 profile image
Stefan Nitzsche

Wow, that’s really substantial. I’m seriously impressed.

Thread Thread
 
stereobooster profile image
stereobooster • Edited

meme about salt chief saying substantial craftsmanship

Collapse
 
thekashey profile image
Anton Korzunov

I think your version would be 10000 times faster than SC. Honestly - SC has a huge runtime cost, which is not measurable on small Apps, or Components, but would strike you later.

Remember the performance path SC has went - they were terrible slow in v1, even slower in v2, become 10 times faster since then, and double that speed at v4. There is almost no room for improvement, and it still 10-100(!) times slower than "pure CSS".

If you want SC-like CSS-in-JS - linaria should be your friend.
If you value customer experience over DX - go BEM way.

Collapse
 
stereobooster profile image
stereobooster

It depends on use case

necolas.github.io/react-native-web...

Some comments may only be visible to logged-in visitors. Sign in to view all comments.