Mastering CSS: Understanding the Cascade

Mustapha Aouas - Jul 15 - - Dev Community

Cascading Style Sheets (CSS) is a fundamental technology of the web, allowing developers to control the visual presentation of HTML documents. While CSS syntax may seem simple at first glance, the way styles are applied and inherited can be surprisingly complex. Understanding these intricacies is crucial for writing efficient, maintainable, and predictable CSS.
In this comprehensive guide, we'll explore the cascade and inheritance concepts of CSS.

The CSS Cascade

The cascade is the algorithm that determines which CSS rules are applied to elements when multiple conflicting rules exist. It's essential to understand how the cascade works to write CSS that behaves as expected. The cascade considers several factors in the following order:

  • 1 Stylesheet origin
  • 2 Inline styles
  • 3 Selector specificity
  • 4 Source order

To be completely exhaustive, we can add:

  • 2.5 Styles that are defined in layers read more
  • 3.5 Styles that are scoped to a portion of the DOM read more

Let's break down the factors that influence the cascade, in order of precedence:

1. Stylesheet Origin

CSS can come from three different sources:

  • 1 User-agent styles: These are the browser's default styles. Each browser has its own set of default styles, which is why unstyled HTML can look slightly different across browsers.
  • 2 User styles: These are custom styles set by the user. While rare, some users may have custom stylesheets to override default styles for accessibility or personal preference.
  • 3 Author styles: These are the styles you write as a web developer.

Generally, author styles take precedence over user styles, which in turn override user-agent styles. This allows developers to customise the appearance of elements while still respecting user preferences when necessary.

2. Inline Styles

Styles applied directly to an element using the style attribute have very high priority:

<p style="color: red;">This text will be red.</p>
Enter fullscreen mode Exit fullscreen mode

Inline styles will override any styles defined in external stylesheets or <style> tags, regardless of their specificity (That’s no longer true if you use the !important keyword. I’ll get on that in a second).

Using inline styles is generally discouraged as it mixes presentation with content and makes styles harder to maintain.

3. Selector Specificity

Specificity is a crucial concept in CSS that determines which styles are applied to an element when multiple conflicting rules exist. Each CSS selector has a specificity number, which can be calculated to predict which styles will take precedence.

Specificity is typically represented as a four-part number (a,b,c,d), where:

  • a: Number of inline styles (generally omitted)
  • b: Number of ID selectors
  • c: Number of class selectors, attribute selectors, and pseudo-classes
  • d: Number of element selectors and pseudo-elements

The resulting number is not base 10. Instead, think of it as separate columns that are compared from left to right. See the examples:

  • p = (0,0,0,1)
  • .class = (0,0,1,0)
  • #id = (0,1,0,0)
  • Inline style = (1,0,0,0)

Consider these two conflicting rules:

#header .nav li { color: blue; } /* (0,1,1,1) */ 
nav > li a { color: red; } /* (0,0,0,3) */
Enter fullscreen mode Exit fullscreen mode

The first rule (0,1,1,1) has higher specificity, so the text would be blue.

Pseudo-class selectors (such as :hover) and attribute selectors (such as [type="text"]) each have the same specificity as class selectors.

The universal selector (*) and combinators (>, +, ~) do not affect specificity.

Also, the :not() pseudo-class also doesn't add to the specificity value; only the selectors inside it are counted.

Several online tools can help calculate specificity (https://specificity.keegan.st/).

4. Source Order

If all else is equal, the rule that appears later in the stylesheet takes precedence:

.button { background-color: blue; }
.button { background-color: green; }  /* This one wins */
Enter fullscreen mode Exit fullscreen mode

In this example, buttons will have a green background.

A Powerful Override

While understanding the cascade is crucial for writing maintainable CSS, there's one more piece of the puzzle that can override all the rules we've discussed so far: the !important keyword.

How !important Works

The !important keyword can override all other considerations in the cascade, except for other !important declarations of higher origin precedence.

/* styles.css */ 
.button {
  background-color: blue !important;
}
Enter fullscreen mode Exit fullscreen mode
<!-- index.html -->
<head>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <button style="background-color: red"> My button </button> 
    <!-- The color will be blue due to !important above -->
</body>
Enter fullscreen mode Exit fullscreen mode

In this example, even though inline styles usually have the highest priority, the button will still have a blue background because of the !important declaration.

The Cascade and !important

The !important keyword actually introduces additional layers to the cascade. The full order of precedence, from highest to lowest, is:

  • User agent important declarations
  • User important declarations
  • Author important declarations
    • Important Inline styles
    • Important not inlined styles
  • Author normal declarations
    • Inline styles
    • Not inlined styles
  • User normal declarations
  • User agent normal declarations

When to Use it

While !important can be tempting as a quick fix, it's generally considered a last resort. Overuse can lead to specificity wars and make your CSS harder to maintain. Legitimate use cases include:

  • Overriding third-party styles you can't modify
  • Creating utility classes that should always apply
  • Ensuring critical accessibility styles are applied

A Potential Solution To Simplify Specificity Management

If you find yourself using !important often, consider refactoring your CSS to use more specific selectors or a more modern approach like utilising :is() and :where() to write more flexible and maintainable styles. (I talk about these two in more details here)

Also, the @layer at-rule, which is fairly supported, allows you to create "layers" of styles with explicitly defined order of precedence:

@layer base, components, utilities;

@layer utilities {
  .btn { padding: 10px 20px; }
}

@layer components {
  .btn { padding: 1rem 2rem; }
}
Enter fullscreen mode Exit fullscreen mode

This offers a more structured approach to managing style precedence without resorting to !important or engaging in a specificity arms race. However, I haven’t used this in a production project myself, if you do, I’d love to hear about your experience :)

Inheritance

Passing Styles Down the DOM Tree

Inheritance is another fundamental concept in CSS. Some CSS properties are inherited by default, meaning child elements will take on the computed values of their parents. This is particularly useful for text-related properties like color, font, font-family, font- size, font-weight, font-variant, font-style, line-height, letter-spacing, text-align, text-indent, text-transform, white-space, and word-spacing.

body {
  font-family: Arial, sans-serif;
  color: #333;
  line-height: 1.5;
}
Enter fullscreen mode Exit fullscreen mode

In this example, all text within the body will inherit these styles unless explicitly overridden. This allows for efficient styling of document-wide typography without having to repeat rules for every element.

A few others inherit as well, such as the list properties: list-style, list-style-type, list-style-position, list-style-image, and some other table related properties

Not all properties are inherited by default. For example, border and padding are not inherited, which makes sense – you wouldn't want every child element to automatically have the same border as its parent.

Inheritance keywords

CSS provides several keywords to give you fine-grained control over inheritance and to reset styles:

  • The inherit keyword forces a property to inherit its value from its parent element (This can be useful for properties that don't inherit by default, like border in this example).
  • The initial keyword resets a property to its initial value as defined by the CSS specification (This can be helpful when you want to completely reset an element's styling).
  • The unset keyword acts like inherit for inherited properties and initial for non-inherited properties (This provides a flexible way to reset properties without needing to know whether they're inherited or not).
  • The revert keyword resets the property to the value it would have had if no author styles were applied (This is useful when you want to fall back to browser defaults rather than CSS-defined initial values).

The initial and unset keywords override all styles, affecting both author and user-agent stylesheets. This means they reset the element's styling to its default state, ignoring any previous styling rules applied by the author or the browser.

However, there are scenarios where you only want to reset the styles you’ve defined in your author stylesheet, without disturbing the default styles provided by the browser (user-agent stylesheet). In such cases, the revert keyword is particularly useful. It specifically reverts the styles of an element back to the browser’s default styles, effectively undoing any custom author-defined styles while preserving the inherent browser styling.

Note that when using shorthand properties omitted values are implicitly set to their initial values. This can potentially override other styles you've set elsewhere.

Wrapping up

By understanding the intricacies of the cascade, inheritance, and modern CSS features, you'll be better equipped to write efficient, maintainable, and powerful stylesheets. Remember, CSS is not just about making things look good – it's about creating robust, flexible designs that work across a wide range of devices and browsers.

. . . . . . . . . . . . . . . . . . .