back to module

Smashing Workshops

Meaningful Selectors

Arrows navigate, cmd/ctrl-k for settings
The Cascade (as a funnel): present, debug

Selector specificity is not the next step in the cascade, but it’s the cascade feature we talk about the most, and have to think about every time we write a selector.

Specificity is Always The Problem

At this point, most of the styles on the web are author styles.

All of our CSS comes from the author origin, they’re rarely important, exist mostly in the same context, get attached using selectors, and sit in the root layer by default.

So selector specificity is – and always has been – our primary interaction with the cascade.

avatar

Specificity is all hype, and it’s time for the bubble to pop.

— Miriam Suzanne

And I think we talk about it too much!

Inverted triangle with
range across the top,
and specificity/explicitness
down the sides
2014 [Harry Roberts, Inverted Triangle CSS]

Like every other cascade heuristic, the default assumption here is that more targeted rules should override more generic rules.

And this time we get to officially call that assumption specificity.

css
* { box-sizing: border-box; }
You probably don’t need this

The universal selector selects (almost) everything, so it has zero specificity. And then we get three specificity layers on top of that.

css
*,
::before,
::after {
  box-sizing: border-box;
}
You probably don’t need this either

But the universal selector doesn’t select pseudo-elements, so you’ll sometimes see this even-more-universal approach.

css
button, div, span {…}
::before, ::after, ::part() {…}
Element type selectors [MDN Pseudo-Elements]

Element names are pretty generic since the same element might be used in widely different situations.

This group includes pseudo-elements like ::before and ::after, ::first-line, ::first-letter, the ::marker on a list-item, or the ::backdrop on a dialog.

You can find the full list on MDN, and see some new form pseudo-elements are on the way.

css
.action, .summary {…}
:hover, :user-invalid {…}
[hidden] {…}
Class & attribute selectors

Class and attribute selectors are more targeted, since we’re using them to group reusable patterns – but they are still pretty broad and flexible.

This includes pseudo-classes, like hover, focus, active, and visited, or first, last, and nth-child, checked, blank, valid, invalid, user-invalid, and many more.

css
a[href*="://"] { /* external links */ }
a[href^="https:"] { /* secure links */ }
a[href$=".pdf"] { /* pdf links */ }

button[aria-pressed=true] { /* pressed buttons */ }
img:not([alt]) { /* images without alt text */ }

The attribute syntax gives us a lot of flexibility, with different ways to parse and select based on attribute values. I use these all the time, maybe more than classes.

css
#call-to-action {…}
ID selectors

And then ID selectors are the most specific selector, because they are supposed to be unique – only matching a single element per page.

css
* { color: red; }
p { color: orange; }
.fancy { color: blue; }
#page-summary { color: indigo; }
Simple Selectors

So if we have a cascade conflict between simple selectors in each category, the most specific selector will win. Id’s will override classes and attributes, which override element names, which override the universal selector.

These are called ‘simple selectors’, when they only contain one single piece of information – but selectors don’t have to be simple.

I think of selectors like words and sentences. These are the words that we can string together to express something more nuanced and complex.

css
form#contact button[type='submit']:active {
  border-color: seaGreen;
}
specificity: [1,2,2]

With a complex selector we have to add up how many words are in each layer of specificity. One ID, two attributes, and two elements.

Like Versioning… [1,0,2] > [0,3,2]

We can compare these like software versions. Version 1.2.0 is larger than any version starting with a 0.

So one ID is all it takes to override any number of classes. But if we have the same number of IDs and classes, then we have to compare the number of elements.

We never have to compare all the numbers at once, we only care about one column at a time.

And look, I think that’s great to understand in theory –

Calculating specificity… Is a Waste of Your Time

But don’t waste your time! This is like learning the rules of grammar because you want to write a haiku.

Specificity is just a heuristic. It’s just there to give us a good default, based on the selectors we’re writing. But as soon as we notice it, it’s not the right tool for the job!

Consider Layers or Scope

If you need a specificity calculator, stop what you’re doing, and reach for layers or scope instead.

I know we haven’t talked about layers or scope yet, but they exist so that you never have to calculate specificity ever again.

*cough* BEM *cough*

This is another of those places where popular conventions will tell you to spend all your energy worrying about specificity.

css
/* expressive browser-ux */
form:has(:invalid) [type="submit"] { … }

/* we do all the work??? */
.form__btn--submit--invalid { … }

People are taking perfectly good sentences, and trying to shove the whole thing into a single word – just to keep the specificity low.

No wonder everyone’s in a panic about naming conventions! Classes are all part of the same name-space, so only using one per selector, we need some pretty specific names to hit our target!

And then we complain that naming things is hard. But I didn’t name a single thing in the original sentence – it’s all right there in the HTML.

css
button { … }
.button { … }
#button { … }
[data-style=button] { … }

CSS only has a single global namespace if you limit yourself to class names. IDs have their own space, so do element names, and we can create any namespace we want by using attribute selectors.

Someone cutting wood
with a hand saw.

This is my hottest take, but I think we should use selectors to select things.

css
h2:has(> button[aria-expanded="false"]) + div {
  display: none;
}
Adrian Roselli, Disclosure Widgets

In fact I consider that the core feature – and the most powerful feature – of the language.

We can write some really good meaningful and expressive sentences without inventing new names for anything.

This excellent sentence comes from Adrian Roselli, and is useful for styling disclosure widgets.

css
.flow > * + * {
  margin-block-start: var(--flow-space, 1em);
}
Heydon Pickering, Axiomatic CSS and Owl Selectors

Or you might be familiar with Heydon Pickering’s owl selector, which adds styles to everything but the first element in a set of siblings.

css
.flow > :not(:first-child) {
  margin-block-start: var(--flow-space, 1em);
}

Although these days I think you might be better off using ‘not first-child’ to achieve the same result.

diff-css
/* doesn't have to be fancy */
- .btn { … }
+ button { … }
Craig Abbott, Our CSS isn’t opinionated enough

And it doesn’t have to be fancy. Craig Abbott has a new article pointing to the issues with making every style work everywhere. That flexibility makes things easier for developers… But often things that should not be easier!

We can use CSS to encourage correct usage of HTML.

I do Get It

I do understand how we got here. Layers and scopes are both fairly new, and without them we had no good way to sidestep specificity conflicts. Our only option was making the heuristic work –

!important

Or jumping all the way to importance. We had to get creative.

avatar

Specificity is all hype, and it’s time for the bubble to pop.

— Miriam Suzanne

But those days are over.

scope

Browser support for scope

Scope is new, but everywhere.

cascade-layers

Browser support for cascade-layers

And layers have been in every browser since March of 2022.

So let’s look at how they work.

Avoid Naming Conflicts

(across large teams & projects)

Express Membership

(through lower boundaries & proximity)

css
.title { /* global */ }
.post .title { /* nested */ }
.post__title { /* BEM */ }
Membership is distinct from ancestry

@scope (<root>) { … }

Cascade & Inheritance, Level 6

Different from CSS Nesting

Scope has… Defined Relationships

Always child or descendant

Scope… Doesn’t Add Specificity

Scope has… Lower Boundaries

Media component with contents that are out of scope
A “donut scope” with lower boundaries
wireframe of a site, with multiple nested components
Components with nested scopes

Build-tools Provide Scoped Styles

BEM, CSS Modules, Vue, JSX, Stylable, etc

css
.post__title { /* BEM */ }
.title[data-JKGHJ] { /* Vue */ }
Maintain uniqueness

@scope (<root>) to (<boundary>) {…}

Cascade & Inheritance, Level 6
css
@scope (.media) to (.content) {
  img { /* only images that are "in scope" */ }
}
Cascade & Inheritance, Level 6
Visualizing scope: present, debug
Scope boundary demo: present, debug

Nested <style> Scopes

html
<article>
  <style scoped>
    p { color: green; } 
  </style>
  <p>This paragraph will be green.</p>
</article>

<p>This paragraph won't!</p>
❌ This won’t work (yet)
html
<article>
  <style>
    @scope { p { color: green; } }
  </style>
  <p>This paragraph will be green.</p>
</article>

<p>This paragraph won't!</p>
✅ This works

Different from Shadow-DOM Encapsulation

Diagram shows a widget with solid boundaries, which cannot be penetrated in either direction (global styles can't get in, widget styles can't get out)
Encapsulation is designed for isolated DOM widgets
Diagram shows a component with porous boundaries, all styles can penetrate, or establish their own lower boundaries
Scope is designed for a unified system
css
@scope (.block-name) to (.block-content) {
  .title { /* only inside the block! */ }
}
Use scope to narrow context (BEM blocks)
css
button {
  &:hover { background: hotPink; }
  &[aria-pressed=true] { border: thick solid teal; }
  @media (width > 30em) { padding-inline: 1em; }
  & + & { margin-inline-start: 1ch; }
}
Use nesting to provide variants (BEM modifiers) and other relationships

In the cascade… Scope Proximity

Another heuristic!
css
.light-theme a { color: purple; }
.dark-theme a { color: plum; }
DOM Proximity
Scope proximity demo: present, debug
css
@scope (.light-theme) {
  a { /* similar to simple nesting… */ }
}
@scope (.dark-theme) {
  a { /* but the _closer_ scope root wins… */ }
}
Cascade & Inheritance, Level 6
Scope proximity demo: present, debug
The Cascade (as a funnel): present, debug
  • Remember, each of these steps is resolved
  • Before we move on to the next step
  • So if a single declaration wins after comparing
  • Origin, importance, context, or element attachment,
  • Then we never bother with layers or specificity and so on.
Inverted triangle with
range across the top,
and specificity/explicitness
down the sides
2014 [Harry Roberts, Inverted Triangle CSS]
Inverted triangle with layers,
wide top to narrow bottom:
settings, tools, generics, elements,
objects, components, overrides
2014 [Harry Roberts, Inverted Triangle CSS]
css
@layer settings { … }
@layer tools { … }
@layer generic { … }
@layer elements { … }
@layer objects { … }
@layer components { … }
@layer overrides { … }
  • They give us a way to organize our code
  • And help us manage the cascade
  • Or maybe more to the point…

Layers of Specificity

  • They can help us manage specificity
  • Because they come before specificity in the cascade
  • So the declaration in the highest layer wins
  • Before we bother to compare specificity
  • Inside the winning layer
Quick Layers Live Demo: present, debug
  • Source order is fragile, layers are explicit
  • Specificity only matters inside a layer
  • Un-layered styles win by default
  • Layers stack in order
  • Define the order up front
css
/* establish layer order */
@layer one, two, three;

/* add code to layers as needed */
@import url(two.css) layer(two);
@layer three { … }
@layer one { … }
@layer two { … }
  • So define the order up front
  • Generally, imports have to come first
  • Layer lists are allowed before imports
  • Order matters!
css
@layer components {
  @layer state { … }
}

/* access nested layers */
@layer components.state { … }
Nesting Layers
css
/* system.css */
@layer theme { … }
@layer components { … }
Nesting Layers (in files)
css
@import url(system.css) layer(system);

@layer system.theme { … }
@layer system.components { … }
Nesting Layers (from imports)
css
@import url(system.css) layer(system);

@layer system.theme { … }
@layer system.components { … }
@layer system.custom { … }
Nesting Layers (from imports)

Add your own sub-layer to an imported layer!

css
@import url(bootstrap.css) layer(bootstrap.theirs);

@layer bootstrap.ours {
  /* anything here will override bootstrap */
}
css
@layer components {
  @layer defaults, themes, state;
}
Order Nested Layers

Not Escalating ❗️importance

Managing Priorities

More Cascade Control
Fewer Specificity Hacks

Getting Started

my recommendations

(your mileage may vary)

Define a Layer Order

Miriam Suzanne, Example layer orders
css
/* add as the first styles */
@layer reset, framework, components, utilities;
Start simple, and append as needed…
html
<style>/* keep this before linked styles */
@layer reset, framework, components, utilities;
</style>
<link rel="stylesheet" href="…">
<link rel="stylesheet" href="…">
Can live in the HTML / template

If Possible Layer Overall Architecture

html
<style>
@layer reset, framework, components, utilities;
@import url(…) layer(reset);
@import url(…) layer(framework);
</style>
Use (non-nested) imports

Prioritize Dependencies

*Including inter-org dependencies

  • Like resets, design systems, component libraries
  • Anything not project-specific

Clearly define… Each Tool’s Priority

Clearly define… Tool-Specific Overrides

css
@import url(bootstrap.css) layer(bootstrap.vendor);

@layer bootstrap.overrides {
  /* anything here will override bootstrap */
}

As you go, Update Layer Order

Working from Lowest to Highest

A graph
with specificity on the vertical axis,
and code line numbers horizontal,
divided into layers,
and a line showing that
specificity should only increase
throughout the code base
2014 [Harry Roberts, Inverted Triangle CSS]
The same specificity graph,
with a solid line after the generic layer
where the specificity curve breaks
and starts lower
2014 [Harry Roberts, Inverted Triangle CSS]
The same specificity graph,
with solid lines after the generic and elements layers,
and the specificity curve
starting over lower down in each one
2014 [Harry Roberts, Inverted Triangle CSS]
The same specificity graph,
with solid lines after the generic, elements, and objects layers,
and the specificity curve
starting over lower down in each one
2014 [Harry Roberts, Inverted Triangle CSS]
The same specificity graph,
with solid lines between all five layers --
generic, elements, objects, components, and utilities --
and a separate specificity curve inside each layer
2014 [Harry Roberts, Inverted Triangle CSS]
html
<template>…</template>
<style>
@layer components {
  /* … */
}
</style>
Single File Components

Don’t (Usually) Need Layers Per-Component

As useful, Layer Inside Components

Once We Start Layering

*the styles will be hidden from older browsers

There’s No Harm Layering More

Encourages Nuanced & Explicit Priorities

For flexibility… Layer Everything

Use un-layered for Prototyping & DeBugging

Like Origins, ❗️important Layers Reverse

Like origins and context Important layers are reversed

  1. Resets (weakest)
  2. Themes
  3. Components
  4. un-layered (strongest)
  • So if we have three layers
  • Each one overriding the previous
  • And a fourth ‘un-layered’ layer

!important

And then we add important styles inside each layer

  1. Resets
  2. Themes
  3. Components
  4. un-layered
  5. !important un-layered
  6. !important Components
  7. !important Themes
  8. !important Resets
  • We now have eight layers
  • With the important layers reversed
Kylo Ren and Rey
standing in front of an entire star war
with spaceships and lasers
and AT-AT walkers

Again, the goal is balance

Protect Styles From Future Layers

Prioritize -> Layers
Protect -> Importance

  • If we just want to override
  • Or manage priorities
  • That’s what layers are for
  • Importance only when essential
css
@layer reset {
  [hidden] { display: none !important; }
}
  • So when I set the hidden attribute to display none
  • And add importance,
  • I can put that in my lowest layer,
  • I’ll call it the reset layer
  • And future, more powerful layers
  • won’t be able to override this declaration
  • It’s important, and so it’s protected.

When ordered… This is Not a Heuristic

Layering and importance are the only two cascade features entirely designed for cascade control – and not as a side-effect of some other feature.

When we run into conflicts, it can be helpful to look at things like source-order, scope, specificity, and attachment, to make sure that they match our intent.

But at the point where we start considering hacks to change specificity or scope, we’d be better off using one of the more explicit tools.

Prioritize -> Layers
Protect -> Importance

And each one serves a different purpose. If we have a low powered style that is essential, and needs protection from future styles – use importance. But if there’s any other reasoning to which style should win, layers are likely the best tool for the job.

Write Selectors
Ignore Specificity
Use Layers

Cascade conflicts are inevitable. And when they resolve in unexpected ways, it’s our job to clarify what should take priority. Layers provide that clarity in a way that nothing else can.

Next module… Normal Flow

« back to resources
SlideDeck Controls

View

Keyboard shortcuts
cmd/ctrl-k:
Toggle this control dialog
cmd/ctrl-shift-enter:
presentation (from first slide)
cmd/ctrl-enter:
presentation (from active slide)
alt-enter:
in speaker view (following active slide)

Color Settings