14

Cutting-Edge CSS Features

Depending on when you are reading this, the contents of this chapter may seem commonplace and less cutting-edge in your daily CSS writing. However, as these words are written, these are some of the latest additions to the CSS language.

So, here is what we will take a look at in this chapter:

  • What cascade layers are and the problems they solve
  • The syntax and typical applications of cascade layers
  • The rules for layer ordering
  • Adding imported style sheets to a layer
  • Understanding the importance of !important
  • CSS nesting and its origins
  • Examples of syntax and its use cases
  • The @nest at-rule
  • Nesting media queries
  • How to write CSS nesting today
  • How to tell browser vendors you want nesting!

In the case of CSS nesting, which we will cover shortly, there are no browser implementations, merely a specification. However, it is an update to the language that I am genuinely excited about – an addition I believe will fundamentally change the way we all write CSS. Therefore, I feel it is worth covering ahead of time.

But before we get to CSS nesting, I want to cover something that is both new to the language and is actually available for use in all modern browsers: CSS cascade layers.

Cascade layers

Cascade layers exist as a high-level tool to manage the grouping, and priority, of style rules. When writing CSS, I make a point of only using classes as the styling hooks to make my changes. To exemplify, if I need to style a button, I won’t use an isolated element selector as a styling hook like this:

button {
    /* styles */
}

This is because, when using element selectors, it isn’t possible to create a specificity equality across all the selectors. As soon as I want a different style button elsewhere, I will need to create a more specific selector to override the styles I have already created. Instead, I would add a class to the button and do this:

.btn {
    /* styles */
}

If the nuance of why this is important when authoring projects is lost on you, I’d recommend reading my shorter book, Enduring CSS, free online at https://ecss.benfrain.com (or buy yourself an eBook/hardcopy).

However, there may be scenarios where things aren’t entirely within your control. To give you a common scenario where this situation plays out, consider the following.

Imagine that, despite this author’s advice against using frameworks in the next chapter, you decide you want to include a CSS framework called “Shoe Lace” in your project. This framework has rules set for the display of common elements such as buttons, lists, and the like.

You include the framework CSS and get working on your project. However, when looking at the buttons on the page, you decide you’d like to tweak the styling a little. Maybe that border needs to be a little thinner?

You look at your markup:

<button class="btn">Buy Scone</button>

And you do the sensible thing and apply the lightest possible selector in your own style sheet to make a change:

.btn {
    border: 1px dotted goldenrod;
}

Then you check the change in the browser and… nothing has happened. What is going on?

Eventually, you look inside the “Shoe Lace” framework files and uncover the problem. They have a selector like this:

/* CSS framework code */
button.btn {
    font-size: 1.3em;
    border: 2px solid #333;
}

This means that every time you want to override this set of styles, you need to use a more specific selector. That’s far from ideal, as now your own styles and selectors have become more specific, which is sure to lead to more issues in the future.

Cascade layers exist to solve these kinds of problems. They can also be a useful tool in organizing your own groups of styles. But before we consider that, let’s take our first look at cascade layers by seeing how we could solve our prior issue:

@layer framework {
    /* CSS framework code */
    button.btn {
        font-size: 1.3em;
        border: 2px solid #333;
    }
}
.btn {
    border: 1px dotted goldenrod;
}

And just like that, our problem has been solved. Using the @layer at-rule establishes a cascade layer, meaning that the styles inside the layer are immediately deemed subordinate to those outside the layer. And we will get into the nitty-gritty of how layers do that momentarily.

The syntax should be relatively familiar to you. In much the same way that styles inside a media query are encapsulated by a condition, the @layer encompasses the styles within a layer. Cascade layers can also be applied on file imports too, allowing you to import a style sheet into a layer. This is something quite likely to be useful if we are dealing with a third-party library. Importing a style sheet into a cascade layer is done like this:

@layer framework;
@import url("shoelace-framework.css") layer(framework);

In that instance, we still make our layer first, and then import our style sheet into it with the layer() function.

Right. At this point, it’s time we go through some ground rules when working with cascade layers.

Naming cascade layers

Naming a cascade layer is just like naming a set of keyframes. Within reason, name your layers however it suits your project. If you like grouping code by area, you can do that. If naming by types of styles, like “reset”, “utilities”, or “colors” is more your thing, then that is fine too. There is no right or wrong; it’s a case of doing whatever best suits the way you work.

You can also have anonymous layers, and they will work just the same. I can’t really recommend doing that, though. For how little it takes to name a layer, it will make it far easier to work with, and think about, if it is named.

Where to declare layers

Declare your layers near the start of your style sheet. As you can’t put layers between @import statements, I ensure that layers get defined at the top, then any @import statements, and then the styles. As we will see in a moment, that doesn’t mean all the styles for a layer need to be nested there too. It’s just for the initial setup.

When you declare layers, they work top to bottom, with the last one declared being a higher priority than the first. So, suppose we had layers declared like this:

@layer reset,
framework,
layout,
components;

The reset would get the lowest priority, followed by the framework, then layout, with the components layer having the highest priority. Whitespace isn’t important, so you can write them vertically or on one line.

This would work equally well:

@layer reset, framework, layout, components;

Personally, I think it’s easier to reason about them listed vertically, as it enforces the order of priority, but you do you!

It is also important to remember that the first instance of a layer defines its priority for perpetuity. Anywhere you subsequently add to, or re-declare that layer, it will remain in its current priority order.

Adding rules to layers

Because you don’t have to add styles to a layer when you define it, you can add rules to a layer any number of times from any location. After a few hundred lines of CSS, if you decide you need to add a rule to layout, you can simply do this:

@layer layout {
    .my-Thing {
        display: grid;
        grid-auto-flow: column;
    }
}

The rule will be added to the relevant layer and obey the priority assigned to that layer when it was defined.

The priority of rules in and outside of layers

A helpful feature of cascade layers is that any styles outside of a layer are always a greater priority than styles in one. This makes it helpful if you want to, effectively, quarantine styles. This is exactly the procedure we followed at the outset, moving our mythical “Shoe Lace” framework CSS into its own layer, allowing us to write styles for our button easily without needing to battle specificity.

The great thing with cascade layers is that you know that what is left outside any layers will be unaffected. So perhaps another helpful way to think of cascade layers is as a way to de-emphasize styles by grouping rules into tiers.

Nesting layers inside other layers

If you find yourself in a suitably complicated scenario, you can even nest layers inside other layers. Let’s suppose we have layouts split into areas like header, sidebars, content, and modals. You might decide these concerns would be better organized under a layout layer. We could set that up like this:

@layer framework;
@layer layout {
    @layer header,
    sidebars,
    content,
    modals;
}
@layer components;

And then we use a dot notation to add styles to the nested layers like this:

@layer layout.header {
    header {
        display: flex;
    }
}

We give the outer layer name, then a period character, and then the nested layer name.

Or if we wanted to @import into a nested layer, it would look like this:

@import url("header.css") layer(layout.header);

The top-to-bottom prioritizing of layers is applied to the nested layers too, so in this instance, layout.modals would have a higher priority than layout.sidebars, for example. It isn’t any more “specific,” though, just because it is nested. So something in layout.modals wouldn’t have greater priority than something in components, for example.

The importance of !important

I feel the use of !important has been a little vilified over the years. Like all of the best villains, that’s perhaps because it is a little misunderstood. I’ll exemplify. Can you explain to me what !important does here?

/* Author stylesheet */
body {
    font-size: 1.1em !important;
}

I mean, sure, we probably all know that if you are having trouble getting a rule to have a certain declaration applied, slapping an !important on there usually fixes it. But how and why does it actually do that?

Confession. I’ve been writing CSS for over 20 years, and it was only when reading the specification for cascade layers that I think I fully, truly, understood the mechanics of !important. Want to bore fellow developers next time you are sharing a coffee? Pay attention to this next bit; it will be !important (see what I did there?).

When styles are applied by the browser, onto the page, there is an order of priority:

  • Firstly, the browser applies its own set of styles; you often see these marked in your dev tools as User Agent Style Sheet.
  • Then, user style sheets are applied. You may have none set, but if you were to set a minimum font size in your browser preferences, that setting would go into your user style sheet.
  • Finally, at the greatest priority of the three are the author style sheets; the sheets you and I write day-to-day.

With that initial order of precedence established, we can envisage that if the font-size for the body was set by the browser, the user, and the author, the author styles (that we as developers write) would “win” and take precedence. This works well enough for the most part, but what if a user needs a large minimum font-size for accessibility or preference? And, in our ignorance, we thought that 12 px was absolutely fine for all? That would be a problem for many! The solution to this issue is the !important declaration.

The !important declaration inverts the established order. So, not only will an !important declaration take precedence over a normal declaration in the same style sheet, but if an !important declaration is added to a user style sheet, such as for our aforementioned font-size preference, this will take precedence over any author style sheet declaration.

This way, users have a guaranteed mechanism to ensure their needs are applied.

In addition, any user-agent (e.g. browser) !important declarations trump even the user !important styles. There is further nuance if needing to factor in encapsulation contexts such as shadow trees, but for our purposes, this mental model will suffice.

The specification for cascade layers offers additional information regarding encapsulation: https://www.w3.org/TR/css-cascade-5/#cascade-context.

In addition, Level 6 of the CSS Cascade specification also introduces the notion of scoped styles, which are sure to make isolating groups of rules even more flexible. You can read that draft specification here: https://www.w3.org/TR/css-cascade-6/#scoped-styles.

So, what does this have to do with cascade layers?

!important in cascade layers

Let’s extend our earlier, simplistic example:

@layer reset,
framework,
layout,
components;

In this instance, as we have established, in terms of the layers, reset will have the least priority, and components the most. However, should we add an !important to one of the declarations in the reset layer, this is what would effectively happen:

reset,
framework,
layout,
components;
!components
!layout
!framework,
!reset,

The browser effectively makes important “versions” of each layer, crucially in reverse order. The outcome of this, practically, is that an !important rule in the reset becomes the highest priority, despite normal rules being the least priority in the “normal” reset layer.

So, all this is to help you understand that when using cascade layers, you need to be even more vigilant in your use of !important. If you can avoid it in your layers, so much the better and the more predictable they will be.

As an exercise, take a look at the CSS of example_14-01:

@layer reset,
type;
@layer reset {
    .para {
        font-size: 2rem;
        color: hotpink !important;
        text-decoration-style: dotted;
        text-decoration: line-through orange !important;
    }
}
@layer type {
    .para {
        font-size: 1.2rem !important;
        color: goldenrod !important;
        text-decoration: solid overline lime !important;
    }
}
.para {
    font-size: 1rem;
    color: brown;
    text-decoration: wavy underline lime !important;
}

Before you open up this page in a browser and see which styles get applied, consider what you have just learned. What do you think will be the applied font-size, color, and text-decoration?

Old browsers

All modern browsers have shipped cascade layers but, depending on the browsers you need to support, it may be some time before you can make use of them in production. Remember, you can always check support at https://caniuse.com/css-cascade-layers.

Unfortunately, there is no way of providing any kind of fallback for cascade layers in older browsers; the layers will simply be ignored. You can test for cascade layers with @supports, which we covered in Chapter 6, CSS Selectors, Typography, and More. That would look like this:

@supports not at-rule(@layer) {
    /* Styles added here for browsers that don't support layers */
}

That concludes our overview of cascade layers.

Now onto something that, at this time, is a little more conceptual. Regardless, a feature that will hopefully be with us soon: CSS nesting.

CSS nesting

If you have done any work with CSS in the last 10–15 years, you will have come across CSS pre-processors, the most enduring of which is Sass.

When Sass first appeared, there was nothing like variables, color functions, or nesting in CSS. I don’t think it’s even debatable that had it not been for the popularity and ultimate ubiquity of Sass and other pre-processors like LESS, we wouldn’t now have CSS custom properties, color manipulation functions, and hopefully soon, the topic of this section: native CSS nesting.

CSS nesting does not provide anything new in the browser. It’s purely a developer experience improvement; syntactic improvements, if you will. It’s also subtly different than how Sass implements nesting; just different enough that it might catch you out at first.

Let’s consider how CSS nesting works and you can decide whether it’s something you feel will improve your development experience or not.

I was so enamored with Sass that back in 2013 I wrote a book on it, Sass and Compass for Designers. Despite being nearly a decade old, it’s still largely relevant today. You can find out more about it here: https://sassandcompass.com.

If you have never used Sass before, and the concept of nesting is entirely lost on you, consider CSS like this:

.parent {
    font-size: 16px;
}
.parent .child {
    font-size: 1em;
}
.ancestor .parent {
    font-size: 1.5rem;
}

In those three rules, the common selector is the .parent class, which is repeated in each of the rules. With nesting, we could compact those three selectors like this:

.parent {
    font-size: 16px;
    & .child {
        font-size: 1em;
    }
    @nest .ancestor & {
        font-size: 1.5rem;
    }
}

Let’s unpack what’s going on here. Firstly, by putting a selector inside the first set of braces, prefixed by an ampersand &, we establish a parent-child relationship. The & symbol is used in nesting to represent the context, or selector, of the rule it is nested within.

To make something a child of something else, you must use the & as the first non-whitespace character of the nested selector. While a direct child doesn’t require anything further than the initial ampersand, expressing other relationships does require additional syntax.

For example, in the second nested rule in the example above, we express the relationship between the parent selector and ancestor selector above it with the @nest keyword and the ampersand & selector after the selector.

The @nest at-rule is used to make explicit to both the author and the browser that some nesting context, indicated by the &, is going to be present on that line, even though it isn’t the first character. Remember, the ampersand, &, is a symbolic reference to the outer, parent selector of the rule. By placing the & after the selector, we are making the nested selector be “printed” first, and then the outer selector. Just remember that whenever you see the &, that will be replaced by the outer selector in the eyes of the browser.

With @nest and &, the full gamut of selector relationships is possible to express. For example, here is an adjacent sibling relationship and a selector where a data attribute and class are combined:

.item + .item {
    /* styles */
}
[data-scones].item {
    /* styles */
}

That could be written nested like this:

.item {
    @nest & + .item {
        /* styles */
    }
    @nest &[data-scones] {
        /* styles */
    }
}

You do need to be aware that every selector in a list needs an &. So, for example, this would not work:

.item {
    color: #fff;
    & .child,
    .child-2 {
        color: #ddd;
    }
}

It’s subtle, but notice that the .child-2 selector is nested but doesn’t have an & as the first character. What we actually need would be:

.item {
    color: #fff;
    & .child,
    & .child-2 {
        color: #ddd;
    }
}

Nesting conditionals such as media queries

You can also nest media queries inside a selector. We mentioned this technique briefly in Chapter 3, Media Queries and Container Queries, as it is a great way to encapsulate all the variations that happen to a selector.

Consider this:

.item {
    width: 100%;
    @media (min-width: 40em) {
        width: 90%;
        margin-inline: auto;
    }
}

Notice that with the conditional/media query, we don’t need to use the & symbol; we can just directly nest it.

Whether an element needs to have styles changed due to changes above it in the DOM tree, different media situations, or we need to express new styles in the situation where it has certain sibling selectors present, with CSS nesting we can contain it all within a single nesting rule. Here is an example:

.item {
    color: #333;
    @media (prefers-color-scheme: dark) {
        color: #e8e8e8;
    }
    @nest .container-big & {
        width: 100%;
    }
    & + & {
        margin-left: 10px;
    }
}

Here, we have created “one rule to, err…, rule them all.” The entire life of .item is inside a set of curly braces, so you only need to find that one string in your CSS codebase to be able to understand and control everything that ever happens to that element.

Trying CSS nesting syntax today

If you are used to using Sass in your projects, there is probably little need to do anything right now. However, if you are using PostCSS in your setup, which often comes built-in with modern build tools like Vite (https://vitejs.dev), you can try the CSS nesting way of writing nested styles right now with the aptly named “PostCSS Nesting” plugin: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-nesting.

Giving your feedback on the proposal

As I write this, the CSS nesting proposal is in flux. You can look at the issues and discussions around the specification at https://github.com/w3c/csswg-drafts/issues?q=is%3Aissue+is%3Aopen+label%3Acss-nesting+c, so if there is something you particularly want to see in the specification, there is nothing to stop you from making your voice heard.

Asking browser vendors to implement CSS nesting

I would welcome CSS nesting, but as there are currently no browser implementations, you can voice your interest in the feature at the following browser support tickets:

The more interest in the feature that browser vendors see, the more likely it is we will see it in browsers sooner rather than later.

Summary

In this chapter, we have looked in detail at two big changes to CSS. The first, cascade layers, gives us a fundamentally different set of tools to organize our code, and we can use it in browsers today. It’s the perfect way of keeping third-party code from causing unwanted side effects.

We then looked at CSS nesting, new functionality still in the mere specification stage that promises to offer us new ways to write more compact and expressive code. There are tools to write it today, and hopefully, in the not-too-distant future, it will be something we can write without thinking whenever we write CSS.

CSS has moved at an incredible pace in the last few years, and it seems that never a week goes by without me coming across some new functionality I have missed or that is coming to browsers soon. It’s certainly easy to feel overwhelmed with the pace of changes in front-end development. However, you shouldn’t. With some common sense and a basic interest in what’s going on, I feel it is easy enough to stay in the game.

Anyway, the more philosophical musings on how to deal with modern front-end development, and many of the other new things coming to CSS and HTML, could be a chapter all of its own.

Which is handy, because that’s exactly what our last chapter is all about.

You’ve made it this far; walk this last mile with me, will you?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset