A stylish guide to styled components #1

The relationship between developers and CSS is kind of weird. By the time we can have a good time making a drawing or some impressive animation full of effects and colors, and we feel proud of ourselves, it can also be frustrating. These types of feelings can come in many ways: from a specific CSS property having an unexpected behavior due to a different browser implementation, having misunderstood a concept of how CSS does something, working on a project where you need to create hacks to keep the application as close to the design as possible or even when fixing a component, but realizing later on that the changes broke another part of the system, and you have no idea how that happened.

Organizing and writing your project’s CSS in a maintainable way is challenging, but it doesn’t always need to be. In this series of posts, we’ll talk about how we can set up styled-components in our projects and use this approach to style our web pages.

What’s the problem with CSS?

Even though CSS has evolved over the years and implemented new features, like CSS custom properties, where we can now set values to entities that will be reused throughout the application and make it possible to create dynamic layouts like changing your website’s theme, we still can find some issues with the CSS:

  • Global scope – When we start writing our style sheet, we don’t usually separate our styles by components because CSS does not have mechanisms to deal with this situation. Instead, CSS has a single global namespace. For example, when a class is created, its styles can be added to any element throughout the page. In the same way, a tag selector can target any element in the document. Styles are not scoped. That happens because of the way the cascade algorithm works.

    The cascade takes only the CSS declarations that come from different sources. During this time, the styles from these origins can overlap in scope, and the cascade algorithm figures out which declaration should be assigned to an element. That’s why our selectors clash when we define new styles, which ends up having an unpredictable impact in different places of the system.

    There’s a way to isolate the code of one component from another, and we can achieve this result through shadow DOM. But before we talk about it, let’s briefly see what the DOM is. When our page is loaded, the browser represents the markup document structure in a tree-like form called DOM (Document Object Model). DOM gives us an interface between our scripts and the elements that offer methods that can be used to change the document structure, styles, and content. For a better understanding, you can see an example of a DOM tree in the image below:

    Now that we know a little about the DOM tree, what does the shadow DOM do? The Shadow DOM is a technique that allows the browser to attach a hidden sub-DOM tree to any elements in our DOM. Once a component is created using this approach, it has an encapsulated context that ensures a boundary between the main document and the shadow node. Thus, all its structure and CSS will stay inaccessible to the rest of the page. Because of this encapsulation, we maintain control over our component’s CSS because the element won’t inherit all styles set in the main document.

    Some elements make use of this approach. If you render a a html input range tag, all we can see in the DOM is an a html input tag, but you realize that we see some inner structure like the slider track and thumb inside its shadow DOM.

    The problem with the Shadow DOM is that since it’s a new approach and new features are being released, not all browsers have available support for it yet, making us do workarounds to make this technology work properly.

  • Specificity – One of the rules that the browser uses to define which CSS property should be applied to a given element is specificity. Many CSS selectors can be used to target different elements in the document and calculate which CSS declaration will be applied. The list of selectors is as follows:

    • Type selectors – Here we directly write which tag we want to select, like p { } to target <p />.

    • Pseudo-elements – These elements don’t really exist, but with that, it is possible to style a specific part of a targeted element. For example, we can use p::first-line { } to apply the styles to the first line of a paragraph.

    • Class selectors – They are the classes we can create to add to an element and use to target them, like <h1 class="title">.

    • Pseudo-classes – We can use this to select an element in a specific state. For example, :hover, :active, :checked.

    • Attribute selectors – With that, we can target elements with a particular attribute. For example, typing input[type="radio"], we would select all the inputs that have type=radio in them.

    • ID selectors – Here we target an element through its ID attribute, like <p id="example" />.

    • Inline styles – We can use this to add styles directly to an element with the style attribute. For example, we can set the font size of a paragraph like this <p style="font-size: 16px">.

    For each kind of selector, we have a weight that is applied to it that the browser uses to calculate which CSS rules will have the highest score and be set to an element. With that, we can now check the following list of selectors in order of weight:

    • Lightest weight – Type selectors and pseudo-elements.

    • Light weight – Classes, pseudo-classes and attribute selectors.

    • Middle weight – ID selectors.

    • Heavy weight – Inline styles.

    How does it work? To decide who is the winner, the browser has some rules:

  • If two types of selectors target the same element, the heaviest weight selector wins.

// <p id="foo" class="bar">

#foo {
  color: red; // the red color will be applied
}

.bar {
  color: green;
}
  • When multiple selectors have equal weight, the last declaration found wins.
// <p class="foo bar">

.foo {
  color: red; 
}

.bar {
  color: green; // the green color will be applied
}
  • When two or more selectors with the same weight target the same element, the count is multiplied by the number of selectors. For example, tags with two classes targeting them will precede rules with only one class.
.foo.bar {
  color: blue; // this color will be applied
}

.bar {
  color: red;
}
  • If we have multiple selectors with the same type and one with a heavier weight targeting the same element, the heaviest one will always win, no matter how many declarations you use.
#foo {
  color: red; // this color will be applied
}

p.class1.class2.class3.class4 {
  color: blue;
}
  • Using inline styles.
#title {
  color: blue;
}

<!-- even though we have an ID, inline styles will overlap id -->
<!-- the color red will be applied -->
<h1 id="title" style="color:red">hello world</h1>
  • There’s a rule that can beat any other ones. When !important rule is used, it can overlap any other declarations.
    #title {
      color: green;
    }

    h1 {
      color: blue !important; // this color will be applied
    }

    <h1 id="title" style="color: red">important</h1>

After all that, what issues could this bring us? When we’re working on projects that are so big we can’t even know all the application’s components, it’s easy to cause unintended consequences or clashes by adding new CSS properties to elements. Since we don’t have 100% oversight into what is being used throughout the document, and due to the algorithm browsers use to decide which style declarations will be set, changing CSS properties can have an unpredictable result.

Another issue it can generate is the raising of specificity in our code. Sometimes, when we are changing a style, we might have to raise our specificity a little. By doing that, if you need to write something that will override this rule, you will have to code an even more specific rule, like adding more selectors before the element you are targeting. This can even lead us to use the !important rule. By the time that happens, for us to overlap this declaration, we’d be required to write a new rule with greater specificity to apply the element, and that makes maintaining a project a lot more difficult.

  • Unused CSS – As mentioned before, we might work on big projects in which we don’t have full knowledge of all components built into the application. Different teams or areas may maintain some parts of the same system. As the project grows, new features come up, and not all CSS implemented will be needed anymore. In companies, having new people working on projects is very common, but there isn’t always a person that understands how a component was implemented, so people tend to avoid deleting existing CSS. We can’t just remove something when we don’t know if it’s safe, so we just add stuff. This practice makes our CSS files bigger, which is worse for performance, since the final user will need more time to download the files.

There are other issues with CSS besides what we’ve just talked about, but in order to deal with them, the community came up with different approaches that try to address some of these problems. Methodologies were invented to make our CSS more maintainable, like BEM and RSCSS, that attempt to solve issues with specificity. Libraries like CSS Modules were created. Using it, our styles can be locally scoped by default. CSS preprocessors also emerged, such as SASS, LESS, and STYLUS. With that, we can use variables and write nested CSS code, which also causes problems like this.

nav {
    ul {
        li {
            a {
            }
        }
    }
}

The code above has the same problem a CSS file with high specificity would have. If any changes are needed, you have to write a rule with a greater priority.

We saw above that CSS has some limitations and communities created a variety of methods throughout time to address them. In this post, we’ll see a technology that allows us to write our styles in Javascript instead of .css files, called CSS-in-JS.

What is CSS-in-JS?

When I first heard about CSS-in-JS, the first thing that crossed my mind was that it was a library. However, CSS-in-JS refers to a collection of ideas to handle the problems we find using CSS, such as lack of modules, specificity, and class names collision.

Although CSS-in-JS is not a specific library, several libs address the CSS issues with different approaches depending on how they are implemented. We can see some of them in the list below:

  • Styled components
  • Emotion
  • Styled JSX
  • JSS

What all these libraries have in common is that instead of writing our CSS as we do traditionally, we would use an API to do the job. Using styled-components with React, the code looks like this:

import styled from 'styled-components'

export const Title = styled.h1'
    font-size: 24px;
    color: blue;
'

const App = () => <Title>Hello World!</Title>;

This renders a <h1 /> element with a font size of 24px and a blue color.

Why use CSS-in-JS?

  • Scoped styles – We mentioned that the community created methodologies to deal with CSS, such as BEM. In practice, this methodology is a naming convention in that we choose the name of our classes following the .Block__element--modifier pattern.


    These conventions also have drawbacks. If you want to make them work consistently, the people contributing to the project need to be aware of all BEM’s rules and follow them strictly at all times, since the methodology doesn’t enforce how it should be used. Despite this, naming conventions don’t avoid common CSS frustration. We still can have class name collisions as nothing prevent us from having the same class name for two different components.

    Taking a closer look at the code above, we no longer need a hard-coded reference to a class defined elsewhere in our system when using CSS-in-JS. Instead, the library itself is responsible for generating unique selectors automatically. We no longer have to worry about class name collisions in the global scope.

    On making use of CSS-in-JS, the scope of all style rules created only refers to that specific selector of the same scope. If you need to make this rule available throughout the application, you’ll need to use the Javascript module and import it where required. That makes it easier for our system to be maintainable as locally scoped styles are enforced by default.

  • Dead code – Websites are getting more complex by the day, and with that, new approaches have emerged to make the developer’s life a bit easier, such as the module system inside Javascript. When modules are applied to writing both CSS and Javascript, we can split our application into smaller pieces that can be reusable in other parts of the application.

    Due to this approach, we make sure that a given style can be traced like any other code in our application. It simplifies the way we change our styles. We can add and change CSS without any unexpected behaviors as every change in that component will not affect anything else in the system, just that single piece we are changing. If you delete a component, you remove its styles too. No more style sheets that we only append new codes. That helps us avoid dead code on our site.

  • Critical CSS – When we access a website, the browser is forced to delay the page rendering until it has finished downloading, parsing, and executing all CSS files referred to by the tag head inside the page. This is necessary because, without all these files, the browser is not able to calculate the page’s layout. It means that the larger the CSS file is, the more users end up waiting until the file is downloaded before the browser can start rendering the page.

    Some techniques help us optimize the delivery of CSS and reduce the blocking content, like the critical rendering path technique. This approach represents the steps that a browser needs to follow to render a page. When using this, we try to find the minimum blocking CSS or critical CSS the browser requires for the initial render. The idea is that the website should render the above-the-fold content as fast as possible. Above-the-fold, in a brief explanation, is all the content the user sees before scrolling the page, like the image below.

    We don’t have a universal way to define the blocking CSS to load the top portion of the screen since several types of devices and screen sizes make it hard to do and maintain over time.

    While we need to decide manually what the critical CSS would be required for the first render, or use libraries like critical, some CSS-in-JS libraries can do it automatically. CSS-in-JS keeps track of the components being used on the page and only injects their CSS into the DOM. With that, users can download the smallest amount of required CSS, making them get a faster first paint.

  • Dynamic styles – Components that have style variations are common in applications. We can have the same elements with different colors, fonts, and shapes. With CSS-in-JS libraries, we can adjust our styles based on a state we can pass. For example, let’s say we have a component that accepts primary and secondary values. When we render the element giving one of these values, the component can get the value and apply the correct styles automatically when the context is changed dynamically.

Conclusion

In this first post, we got the chance to talk about some CSS fundamentals and the drawbacks when working with this technology. On the other hand, we could also see that with CSS-in-JS, we have advantages over the traditional way because we can automatically sidestep common CSS frustrations such as class name collisions and specificity wars. In the next post, we will be able to see how styled-components work under the hood and start the setup of our project.

We want to work with you. Check out our "What We Do" section!