The original address: www.joshwcomeau.com/css/styled-… Get rid of the smart front end team

For several years, 💅 Styled – Components has been my favorite tool for managing CSS in the React app.

Styled components are excellent, and it changes my view of CSS architecture in many ways and helps me keep the code repository clean and modules clean — much like React.

Styled components and React have another thing in common: at first, many people have difficulty accepting their concept 😅. The idea that “every style is a component” can be hard to swallow, as in “your view is now written in a mixture of XML/JS”.

Perhaps for this reason, I have found that many developers never take the Styled Components seriously. Developers introduce it into their projects, but do not update their own mental models of styles [1]. The shallow in, shallow out mentality keeps them out of tool best practices.

If you use Styled components, or a similar tool, such as Emotion, I hope this article will help you take full advantage of them. I’ve distilled years of experimentation and practice into some practical tips and techniques. If you apply these lessons correctly, trust me, you will be much happier writing CSS ✨.

The target readers

This article is intended for both beginners and experienced React developers who are already using Styled components or another CSS-in-JS solution, such as Emotion.

This article is not intended as an introduction to styled- Components, nor is it intended to compare or contrast it with other tools.

CSS variable

Let’s start with an interesting example.

Suppose we have a background component with transparency and color properties:

function Backdrop({ opacity, color, Children}) {return (<Wrapper> {children} </Wrapper>)} const Wrapper = styled. Div '/* omit */';Copy the code

How do you apply these properties to the Wrapper?

One way is to use interpolation functions:

function Backdrop({ opacity, color, children }) {
  return (
    <Wrapper opacity={opacity} color={color}>
      {children}
    </Wrapper>
  )
}

const Wrapper = styled.div`
  opacity: ${p => p.opacity};
  background-color: ${p => p.color};
`;
Copy the code

Such code works, but there are a few minor problems. As the code runs, styled- Components will generate a new class name whenever the value of the property changes and reinject it into the of the document. In doing so, there are sometimes performance issues (e.g., executing JS animations).

There is another way to solve this problem — use CSS variables:

function Backdrop({ opacity, color, children }) {
  return (
    <Wrapper
      style={{
        '--color': color,
        '--opacity': opacity,
      }}
    >
      {children}
    </Wrapper>
  )
}

const Wrapper = styled.div`
  opacity: var(--opacity);
  background-color: var(--color);
`;
Copy the code

CSS variables are a constant source of new gameplay. If you’re not sure what’s going on here, the CSS variables in my React development will help you understand it (and you’ll also learn some other tricks!). .

We can also specify default values using CSS variables:

function Backdrop({ opacity, color, children }) { return ( <Wrapper style={{ '--color': color, '--opacity': Opacity,}} > {children} </Wrapper>)} const Wrapper = styled. Div 'opacity: var(--opacity, 0.75); background-color: var(--color, var(--color-gray-900)); `;Copy the code

If we do not specify transparency or color when using

, we will use 75% transparency and dark gray in the theme color as the default.

This way of specifying property values feels good. It didn’t change the rules of the game, but it gave me some fun.

And that’s just the beginning. Let’s look at something more meaningful.

A single source of styles

If there was only one trick you could learn from this article, it would be this one. This is a real treasure.

In this article, I have a TextLink component. It looks like this:

const TextLink = styled.a`
  color: var(--color-primary);
  font-weight: var(--font-weight-medium);
`;
Copy the code

This is the component used for linking within the body of the article. Here’s an example from the original blog post:

On my blog, I have an Aside component for extra information:

In the Aside, “an included link” is represented by a TextLink, which is a very similar component to the TextLink in the text. However, I wanted to apply something different – I don’t like blue text on a blue background.

This is the concept known as “contextual styling” : The same component changes its appearance based on its context. When you use TextLink in an Aside, you add/replace some styles.

How would you solve this problem for this scenario? I often see code like this:

// Aside.js const Aside = ({ children }) => { return ( <Wrapper> {children} </Wrapper> ); } const Wrapper = styled. Aside '/* base style */ a {color: var(--color-text); font-weight: var(--font-weight-bold); } `; export default Aside;Copy the code

In my opinion, this is a very bad way to write it. This makes style backtracking very difficult in our application — how do we know which styles TextLink was set to? A project-wide search of TextLink is not feasible, you have to grep for a, good luck. If we didn’t know that Aside applied these styles, we would never be able to predict how TextLink would behave.

So, what’s the right approach? You may have considered using TextLink instead of a to specify these styles:

// Aside.js import TextLink from '.. /TextLink' const int = ({children}) => {/* omitted */} const Wrapper = styled. var(--color-text); font-weight: var(--font-weight-bold); } `; export default Aside;Copy the code

Styled – Components allow us to “embed” one component into another like this. When the component is rendered, it generates the corresponding selector, a class name that matches the TextLink component.

This is definitely better, but I’m not satisfied. We haven’t solved the biggest problem, we’ve just made it easier to solve.

Let’s go back to encapsulation.

What I like about React is that it encapsulates logic (states, side effects) and UI (JSX) into reusable boxes. A lot of people focus on “reusability”, but in my opinion, what’s even cooler is that it’s a “box”.

The React component sets strict boundaries around it. When you write JSX in a component, you can trust that HTML will only be modified in that component; You don’t have to worry about components elsewhere in the application “invading” and tampering with the HTML.

Take a look at the TextLink solution. Aside is invading and interfering with TextLink styles! If any component can override the style of any other component, then we really have no encapsulation at all.

Imagine if you were completely confident that all styles for a given element are defined in the Styled component itself?

As it turns out, we can do this [2]. The method is as follows:

// use import Aside from '@components/Aside'; import TextLink from '@components/TextLink'; Const Section = () => {return (<TextLink>an example of an Aside component.</TextLink> </Aside>)}Copy the code
// Aside. Js const Aside = ({children}) => {return (<Wrapper> {children} </Wrapper>) Wrapper = styled. Aside '/* style */'; export default Aside;Copy the code
// TextLink.js import { Wrapper as AsideWrapper } from '.. /Aside' const TextLink = styled.a` color: var(--color-primary); font-weight: var(--font-weight-medium); ${AsideWrapper} & { color: var(--color-text); font-weight: var(--font-weight-bold); } `;Copy the code

If you’re not familiar with the & character, it’s a placeholder for the name of the resulting class. When styles-Components creates a.textLink-ABC123 class for the component, it also replaces any & characters with the selector. Here is the resulting CSS:

.TextLink-abc123 {
  color: var(--color-primary);
  font-weight: var(--font-weight-medium);
}

.Aside-Wrapper-def789 .TextLink-abc123 {
  color: var(--color-text);
  font-weight: var(--font-weight-bold);
}
Copy the code

With this little trick, we reversed the control. We say “Here’s my base TextLink style, here’s my TextLink style wrapped in AsideWrapper” and the declarations for both styles are in the same place.

The mighty TextLink will once again be in control of its own destiny. Our style has a single source.

It really is so much better. Next time you’re in this situation, try it.

Choose the right tools for your scenario

This form of “inversion of control” makes things so nice and manageable when we have two generic components like Aside and TextLink, but is it appropriate for all scenarios?

Suppose we have a component, halloweensale.js. This is a marketing page that uses our standard components, but adds a scary font and an orange theme.

Should we update all standard components to support this variant? Of course not. 😅

First, importing HalloweenSale into TextLink increases the volume of our JavaScript package: whenever users visit any page on our site, they have to download all the marks and code for the HalloweenSale page, even in mid-march!

It also makes our textLink. js file cluttered with one-off variables, which are irrelevant 99% of the time and crowd out more relevant contextual styles.

There is an alternative — a composite API:

// HalloweenPage.js import TextLink from '.. /TextLink'; const HalloweenTextLink = styled(TextLink)` font-family: 'Spooky Font', cursive; `;Copy the code

The difference between these two cases is a little subtle, but very important!

In a Halloween scenario, I would take a generic standard component and combine it into a new component — one with a more specialized purpose so that it could be used in a specific way. HalloweenTextLink is reused in a different way than TextLink.

Can we do the same thing in the Aside/TextLink scene and create an AsideTextLink wrapper? Sure, but I don’t think we should:

  1. withHalloweenTextLinkNo, I use it everywhereAsideThe component! When I useTextLinkIt is useful to know these styles because they are a core part of the application.
  2. I want the context style to be applied automatically. I don’t want developers to be using itAside“, also remember to useAsideTextLink:
<Aside> Do you really think developers will remember to use the component variant </AsideTextLink href="">? </Aside>Copy the code

I know myself so well that I remember to do this at most 75% of the time. The other 25% of the time can result in a rather inconsistent UI!

Now, there’s a tradeoff: for HalloweenTextLink, we actually lose some visibility. In theory, I could silently break HalloweenTextLink when I changed the TextLink, and not even know it exists!

This is difficult, and there is no perfect solution, but by making a clear distinction between “core variants” and “one-off variants”, we can ensure that the most important ones are prioritised and managed well (does defining 30 one-off variants in TextLink.js really help?). .

Inherited CSS properties

Sometimes unexpected things happen: component boundaries are never completely sealed, because some CSS properties are inheritable.

But as far as I’m concerned, it’s not a big deal.

First, to explain exactly what I’m talking about, consider the following:

function App() {
  return (
    <div style={{ color: 'red' }}>
      <Kid />
    </div>
  )
}

function Kid() {
  return <p>Hello world</p>
}
Copy the code

The Kid component has no style set, but it ends up showing red text. This is because color is an inherited property, and all descendant elements will default to red text.

Technically, it’s a leak, but it’s a fairly harmless leak for the following reasons:

  1. Any style I set will override the inherited style, which will never win the priority wars. I’m sure all styles explicitly set will work. The component style is its own choice!
  2. Only a few CSS properties are inheritable, and they are mostly typesetting related. The layout properties are as followspadding 或 borderIt’s not going to be inherited. In general, typographic styles should be inherited; Reapply the current font color to each paragraph if necessary<strong> 或 <em>“Will be very annoying.
  3. In DevTools, the source of the component styles in effect is very clear, and it’s easy to figure out where the styles are set.

The same is true for global styles. In most of my projects, I have introduced a CSS reset and some normal normalize styles. They all work on tags (e.g., P, H1) to minimize priority. Life is too short to include a Paragraph component when you can use the P tag directly.

CSS in isolation

All right, I have one more good thing to share.

Imagine if we wanted the Aside component to have some space around it so it wouldn’t get stuck between its sibling paragraphs and headings.

Here’s one way:

// Aside.js
const Aside = ({ children }) => {
  return (
    <Wrapper>
      {children}
    </Wrapper>
  );
}

const Wrapper = styled.aside`
  margin-top: 32px;
  margin-bottom: 48px;
`;

export default Aside;
Copy the code

That would solve our problem, but I think it’s too heavy. It’s easy to get caught up in this approach — for example, what do we do when we need to use this component in a different situation with different spacing requirements?

Using margins in this way is antithetical to the idea of designing reusable, generic components.

Heydon Pickering famously said:

“Margins are like gluing something on before you decide what to stick it on, or if you should stick it on.”

Another problem is that margins are strange and can collapse in unexpected, counterintuitive ways, which can break the envelope. For example, if we put

I recently wrote an article about Margin collapse rules. If you’re surprised by margin’s performance, this article is very useful for you!

More and more developers are choosing not to use margins, and I haven’t given up the habit entirely, but I think avoiding “margin leaks” like this is a good compromise and starting point.

Why don’t we use margin for layout? There are a few options!

  • If it’s in a CSS grid, you can use itgrid-gapTo separate each element
  • If it’s in a Flex container, brand newgapAttributes can be very effective (although you may want to waitAdded support for SafariFor later use)
  • You canuseSpacercomponent”, a controversial but surprisingly useful choice
  • You can use specialized layout components, such as the Stack in the Braid system

The ultimate goal is to avoid backing yourself into a corner. I believe that, as long as we are conscious and understand the tradeoffs, it is fine to occasionally use margins to solve practical problems.

Finally, let’s talk about cascading contexts.

Take a closer look at the following code:

// Flourish.js const Flourish = styled.div` position: relative; z-index: 2; /* Ignore some styles */ '; export default Flourish;Copy the code

See the problem? As before, we pre-assign a Z-index property to the component, and we want 2 to be the correct level in all future cases!

The problem can be even worse, as shown in the following code:

// Flourish.js
const Flourish = ({ children }) => {
  return (
    <Wrapper>
      <DecorativeBit />
      <DecorativeBackground />
    </Wrapper>
  );
}

const Wrapper = styled.div`
  position: relative;
`;

const DecorativeBit = styled.div`
  position: absolute;
  z-index: 3;
`;

const DecorativeBackground = styled.div`
  position: absolute;
  z-index: 1;
`;
Copy the code

The top-level style component Wrapper does not set z-index… Of course, that’s all right, right?

I hope so. In fact, this leads to a very confusing problem.

If our Flourish component has a sibling with a Z-index value in the middle, the component will be “interlaced” with the Flourish background:

<div> <! -- The Malcom component gets stuck in the middle! --> <Malcolm style={{ position: 'relative', zIndex: 2}} /> <Flourish /> </div>Copy the code

We can solve this problem by explicitly creating a cascading context using the Isolation property:

const Flourish = ({ children }) => { return ( <Wrapper> <DecorativeBit /> <DecorativeBackground /> </Wrapper> ); } const Wrapper = styled.div` position: relative; isolation: isolate; `; // The rest is unchangedCopy the code

This ensures that any sibling element will be higher or lower than this element. As an added bonus, the new cascading context does not have z-Index, so we can rely entirely on DOM order or pass a specific value if necessary.

This stuff is complicated and beyond the scope of this article. Cascading context will be discussed in depth in my upcoming CSS class, CSS for JavaScript developers.

Tips and Tricks

Yo! I’ve covered all the advanced “goodies” I wanted to share, but before I go, I have a few more details that I think are worth sharing. Let’s see.

The as attribute

React developers are known for not understanding the semantics of HTML and use

for all cases.

A fair criticism of styled- Components is that it adds a layer of indirection between JSX and the generated HTML tags. Only by realizing this fact can I make the following clear!

Each styled- Component you create accepts a property that changes which HTML element is used. This is handy for headings, because the exact level of headings depends on the scene:

H1-h6 function fadeout ({level, children}) {const tag = 'h${level}'; return ( <Wrapper as={tag}> {children} </Wrapper> ); } // The following h2 is not important because it is always overwritten by const Wrapper = styled. H2 '/* Styled content */';Copy the code

It also makes it easy to render components as buttons or links, depending on the environment:

function LinkButton({ href, children, ... delegated }) { const tag = typeof href === 'string' ? 'a' : 'button'; return ( <Wrapper as={tag} href={href} {... delegated}> {children} </Wrapper> ); }Copy the code

Semanization of HTML is very important, and all developers using Styled – Components should know the AS attribute, which is crucial.

Increase priority

In most CSS methodologies, you will occasionally encounter a situation where you write a declaration that has no effect because another style overwrites it. This is called a priority problem, because unwanted styles have higher priority.

In most cases, if you follow the methods outlined in this article, I guarantee you won’t run into priority issues, except when dealing with third-party CSS. The blog has about 1000 style components and I’ve never had a problem with prioritization.

I’m hesitant to share this tip because it’s an escape hatch for a situation that should be avoided… But I also want to be realistic. We’ve always worked with code repositories that weren’t ideal, and it never hurts to add an extra tool to your toolbox.

Like this:

const Wrapper = styled.div` p { color: blue; } ` const Paragraph = styled.p` color: red; && { color: green; } `; // Use: <Wrapper> <Paragraph>I'm green! </Paragraph> </Wrapper>Copy the code

In this case, we have three separate color declarations for the same paragraph.

At the basic level, our paragraphs are red text given using standard style component syntax. Unfortunately, the Wrapper uses a descendant selector and overwrites the red text with blue text.

To solve this problem, we can use double ampersand to convert it to green text. As we saw earlier, the & character is a placeholder for the generated class name. Playing it twice repeats the class: not.paragraph, but.paragraph. Paragraph.

By doubling the emphasis on the class name, its priority is increased. .paragraph. Paragraph has a higher priority than.wrapper p.

This trick can be done without using nuclear weapons! In the case of important, it is important to increase the priority. But it’s also a Pandora’s box: once you start down the path of this technique, you’re on a doomed path.

Babel plug-in

In a production environment, Styled – Components will generate a unique hash value for each style component you create, such as.hnn0ug or.gajjhs. These concise names are helpful because they don’t take up much space in the HTML rendered on the server, but they are completely opaque to the developer.

Fortunately, a Babel plug-in exists! During development, it uses semantic class names to help us trace the source of the element/style:

If you use create-react-app, you can benefit from this plugin without having to change all imports to use: import styled from ‘styled-components/macro’;

Quick find and replace in your projects will greatly improve your development experience!

For other types of projects, you can follow the official documentation.

Descendant selector

Earlier we discussed how using descendant selectors makes it difficult for your application to track styles. But what if we could use the Babel plug-in to track styles?

Unfortunately, no. Not at all.

Again, here’s a pattern I’d like you to avoid:

// Aside.js const Aside = ({ children }) => { return ( <Wrapper> {children} </Wrapper> ); } const Wrapper = styled. Aside '/* base style */ a {color: #F00; } `; export default Aside;Copy the code

If we look at it in DevTools, we’ll see that the Babel plug-in doesn’t really handle this silly thing:

Even if you can, I would advise against using this pattern. We shouldn’t be able to understand what styles are in effect only after the code is rendered in real time; This is best done by reading the source file, ideally just a piece of code.

Does this mean that I will create a new styled. Whatever whenever I need to apply a style? Well, I can lower the bar a bit, just make sure that when I use descendant selectors, I always target component-specific elements:

// Whatever. Js const Whatever = () => {return (<em> this is a small <em> example </em>. </Wrapper>); } const Wrapper = styled.div` & > em { color: #F00; } `;Copy the code

This isn’t too bad, as long as you don’t “hack” into changing the style of an element under another component. To be honest, this is still not ideal, but I know that creating new style components all the time is tedious, and it’s a reasonable, pragmatic compromise.

The model of thinking

In this article, we have discussed some of the styled component’s specific apis, but the idea I want to convey is more important than any particular tool or library.

When we extend component thinking to CSS, we gain all sorts of new superpowers:

  • The ability to explicitly know if CSS declarations can be safely removed (without affecting other modules in the application!)
  • Break away from the priority problem and stop trying to find ways to increase your priority
  • A neat mental model that fits your brain and helps you understand exactly what your page looks like without having to do a bunch of manual tests

Styled -components are not styled itself, so there are many different ways of using them… But I have to admit, I get a little sad when I see developers treating it like a fancy class name generator or “Sass 2.0.” If you buy into the idea that styling components is components, then you can certainly get more out of this tool.

Of course, these are just my opinions, but I’m glad to know they’re in line with recommended practices. I sent a draft of this article to Max Stoiber, creator of Styled – Components, and here is his response:

In my opinion, many of these tools have faded from view. Only after several years of experimentation did the idea of “how to manage CSS in React” become clear. I hope this article has saved you some time and effort.


.

[1]. Mental Models. It can probably be understood as: when solving problems, people’s internal thinking and operation.

[2]. Example code: This writing method is easier to generate circular references. It is necessary to standardize code structure and introduce cyclic dependent detection mechanism when it is used in projects.