:where – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Tue, 17 Jan 2023 15:13:00 +0000 en-US hourly 1 https://wordpress.org/?v=6.2.2 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 :where – CSS-Tricks https://css-tricks.com 32 32 45537868 :has is an unforgiving selector https://css-tricks.com/has-is-an-unforgiving-selector/ https://css-tricks.com/has-is-an-unforgiving-selector/#comments Wed, 11 Jan 2023 14:18:13 +0000 https://css-tricks.com/?p=376342 A little thing happened on the way to publishing the CSS :has() selector to the ol’ Almanac. I had originally described :has() as a “forgiving” selector, the idea being that anything in its argument is evaluated, even if one or …


:has is an unforgiving selector originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
A little thing happened on the way to publishing the CSS :has() selector to the ol’ Almanac. I had originally described :has() as a “forgiving” selector, the idea being that anything in its argument is evaluated, even if one or more of the items is invalid.

/* Example: Do not use! */
article:has(h2, ul, ::-scoobydoo) { }

See ::scoobydoo in there? That’s totally invalid. A forgiving selector list ignores that bogus selector and proceeds to evaluate the rest of the items as if it were written like this:

article:has(h2, ul) { }

:has() was indeed a forgiving selector in a previous draft dated May 7, 2022. But that changed after an issue was reported that the forgiving nature conflicts with jQuery when :has() contains a complex selector (e.g. header h2 + p). The W3C landed on a resolution to make :has() an “unforgiving” selector just a few weeks ago.

So, our previous example? The entire selector list is invalid because the bogus selector is invalid. But the other two forgiving selectors, :is() and :where(), are left unchanged.

There’s a bit of a workaround for this. Remember, :is() and :where()are forgiving, even if :has() is not. That means we can nest either of the those selectors in :has() to get more forgiving behavior:

article:has(:where(h2, ul, ::-scoobydoo)) { }

Which one you use might matter because the specificity of :is() is determined by the most specific item in its list. So, if you need to something less specific you’d do better reaching for :where() since it does not add to the specificity score.

/* Specificity: (0,0,1) */
article:has(:where(h2, ul, ::-scoobydoo)) { }

/* Specificity: (0,0,2) */
article:has(:is(h2, ul, ::-scoobydoo)) { }

We updated a few of our posts to reflect the latest info. I’m seeing plenty of others in the wild that need to be updated, so just a little PSA for anyone who needs to do the same.


:has is an unforgiving selector originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/has-is-an-unforgiving-selector/feed/ 4 376342
Taming the Cascade With BEM and Modern CSS Selectors https://css-tricks.com/taming-the-cascade-with-bem-and-modern-css-selectors/ https://css-tricks.com/taming-the-cascade-with-bem-and-modern-css-selectors/#comments Mon, 21 Nov 2022 13:59:15 +0000 https://css-tricks.com/?p=375144 BEM. Like seemingly all techniques in the world of front-end development, writing CSS in a BEM format can be polarizing. But it is – at least in my Twitter bubble – one of the better-liked CSS methodologies.

Personally, I think …


Taming the Cascade With BEM and Modern CSS Selectors originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
BEM. Like seemingly all techniques in the world of front-end development, writing CSS in a BEM format can be polarizing. But it is – at least in my Twitter bubble – one of the better-liked CSS methodologies.

Personally, I think BEM is good, and I think you should use it. But I also get why you might not.

Regardless of your opinion on BEM, it offers several benefits, the biggest being that it helps avoid specificity clashes in the CSS Cascade. That’s because, if used properly, any selectors written in a BEM format should have the same specificity score (0,1,0). I’ve architected the CSS for plenty of large-scale websites over the years (think government, universities, and banks), and it’s on these larger projects where I’ve found that BEM really shines. Writing CSS is much more fun when you have confidence that the styles you’re writing or editing aren’t affecting some other part of the site.

There are actually exceptions where it is deemed totally acceptable to add specificity. For instance: the :hover and :focus pseudo classes. Those have a specificity score of 0,2,0. Another is pseudo elements — like ::before and ::after — which have a specificity score of 0,1,1. For the rest of this article though, let’s assume we don’t want any other specificity creep. 🤓

But I’m not really here to sell you on BEM. Instead, I want to talk about how we can use it alongside modern CSS selectors — think :is(), :has(), :where(), etc. — to gain even more control of the Cascade.

What’s this about modern CSS selectors?

The CSS Selectors Level 4 spec gives us some powerful new(ish) ways to select elements. Some of my favorites include :is(), :where(), and :not(), each of which is supported by all modern browsers and is safe to use on almost any project nowadays.

:is() and :where() are basically the same thing except for how they impact specificity. Specifically, :where() always has a specificity score of 0,0,0. Yep, even :where(button#widget.some-class) has no specificity. Meanwhile, the specificity of :is() is the element in its argument list with the highest specificity. So, already we have a Cascade-wrangling distinction between two modern selectors that we can work with.

The incredibly powerful :has() relational pseudo-class is also rapidly gaining browser support (and is the biggest new feature of CSS since Grid, in my humble opinion). However, at time of writing, browser support for :has() isn’t quite good enough for use in production just yet.

Lemme stick one of those pseudo-classes in my BEM and…

/* ❌ specificity score: 0,2,0 */
.something:not(.something--special) {
  /* styles for all somethings, except for the special somethings */
}

Whoops! See that specificity score? Remember, with BEM we ideally want our selectors to all have a specificity score of 0,1,0. Why is 0,2,0 bad? Consider a similar example, expanded:

.something:not(a) {
  color: red;
}
.something--special {
  color: blue;
}

Even though the second selector is last in the source order, the first selector’s higher specificity (0,1,1) wins, and the color of .something--special elements will be set to red. That is, assuming your BEM is written properly and the selected element has both the .something base class and .something--special modifier class applied to it in the HTML.

Used carelessly, these pseudo-classes can impact the Cascade in unexpected ways. And it’s these sorts of inconsistencies that can create headaches down the line, especially on larger and more complex codebases.

Dang. So now what?

Remember what I was saying about :where() and the fact that its specificity is zero? We can use that to our advantage:

/* ✅ specificity score: 0,1,0 */
.something:where(:not(.something--special)) {
  /* etc. */
}

The first part of this selector (.something) gets its usual specificity score of 0,1,0. But :where() — and everything inside it — has a specificity of 0, which does not increase the specificity of the selector any further.

:where() allows us to nest

Folks who don’t care as much as me about specificity (and that’s probably a lot of people, to be fair) have had it pretty good when it comes to nesting. With some carefree keyboard strokes, we may wind up with CSS like this (note that I’m using Sass for brevity):

.card { ... }

.card--featured {
  /* etc. */  
  .card__title { ... }
  .card__title { ... }
}

.card__title { ... }
.card__img { ... }

In this example, we have a .card component. When it’s a “featured” card (using the .card--featured class), the card’s title and image needs to be styled differently. But, as we now know, the code above results in a specificity score that is inconsistent with the rest of our system.

A die-hard specificity nerd might have done this instead:

.card { ... }
.card--featured { ... }
.card__title { ... }
.card__title--featured { ... }
.card__img { ... }
.card__img--featured { ... }

That’s not so bad, right? Frankly, this is beautiful CSS.

There is a downside in the HTML though. Seasoned BEM authors are probably painfully aware of the clunky template logic that’s required to conditionally apply modifier classes to multiple elements. In this example, the HTML template needs to conditionally add the --featured modifier class to three elements (.card, .card__title, and .card__img) though probably even more in a real-world example. That’s a lot of if statements.

The :where() selector can help us write a lot less template logic — and fewer BEM classes to boot — without adding to the level of specificity.

.card { ... }
.card--featured { ... }

.card__title { ... }
:where(.card--featured) .card__title { ... }

.card__img { ... }
:where(.card--featured) .card__img { ... }

Here’s same thing but in Sass (note the trailing ampersands):

.card { ... }
.card--featured { ... }
.card__title { 
  /* etc. */ 
  :where(.card--featured) & { ... }
}
.card__img { 
  /* etc. */ 
  :where(.card--featured) & { ... }
}

Whether or not you should opt for this approach over applying modifier classes to the various child elements is a matter of personal preference. But at least :where() gives us the choice now!

What about non-BEM HTML?

We don’t live in a perfect world. Sometimes you need to deal with HTML that is outside of your control. For instance, a third-party script that injects HTML that you need to style. That markup often isn’t written with BEM class names. In some cases those styles don’t use classes at all but IDs!

Once again, :where() has our back. This solution is slightly hacky, as we need to reference the class of an element somewhere further up the DOM tree that we know exists.

/* ❌ specificity score: 1,0,0 */
#widget {
  /* etc. */
}

/* ✅ specificity score: 0,1,0 */
.page-wrapper :where(#widget) {
  /* etc. */
}

Referencing a parent element feels a little risky and restrictive though. What if that parent class changes or isn’t there for some reason? A better (but perhaps equally hacky) solution would be to use :is() instead. Remember, the specificity of :is() is equal to the most specific selector in its selector list.

So, instead of referencing a class we know (or hope!) exists with :where(), as in the above example, we could reference a made up class and the <body> tag.

/* ✅ specificity score: 0,1,0 */
:is(.dummy-class, body) :where(#widget) {
  /* etc. */
}

The ever-present body will help us select our #widget element, and the presence of the .dummy-class class inside the same :is() gives the body selector the same specificity score as a class (0,1,0)… and the use of :where() ensures the selector doesn’t get any more specific than that.

That’s it!

That’s how we can leverage the modern specificity-managing features of the :is() and :where() pseudo-classes alongside the specificity collision prevention that we get when writing CSS in a BEM format. And in the not too distant future, once :has() gains Firefox support (it’s currently supported behind a flag at the time of writing) we’ll likely want to pair it with :where() to undo its specificity.

Whether you go all-in on BEM naming or not, I hope we can agree that having consistency in selector specificity is a good thing!


Taming the Cascade With BEM and Modern CSS Selectors originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/taming-the-cascade-with-bem-and-modern-css-selectors/feed/ 21 375144
Don’t Fight the Cascade, Control It! https://css-tricks.com/dont-fight-the-cascade-control-it/ https://css-tricks.com/dont-fight-the-cascade-control-it/#comments Mon, 10 Jan 2022 15:22:08 +0000 https://css-tricks.com/?p=359886 If you’re disciplined and make use of the inheritance that the CSS cascade provides, you’ll end up writing less CSS. But because our styles often comes from all kinds of sources — and can be a pain to structure and …


Don’t Fight the Cascade, Control It! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
If you’re disciplined and make use of the inheritance that the CSS cascade provides, you’ll end up writing less CSS. But because our styles often comes from all kinds of sources — and can be a pain to structure and maintain—the cascade can be a source of frustration, and the reason we end up with more CSS than necessary.

Some years ago, Harry Roberts came up with ITCSS and it’s a clever way of structuring CSS.

Mixed with BEM, ITCSS has become a popular way that people write and organize CSS.

However, even with ITCSS and BEM, there are still times where we still struggle with the cascade. For example, I’m sure you’ve had to @import external CSS components at a specific location to prevent breaking things, or reach for the dreaded !important at some point in time.

Recently, some new tools were added to our CSS toolbox, and they allow us to finally control the cascade. Let’s look at them.

O cascade, :where art thou?

Using the :where pseudo-selector allows us to remove specificity to “just after the user-agent default styles,” no matter where or when the CSS is loaded into the document. That means the specificity of the whole thing is literally zero — totally wiped out. This is handy for generic components, which we’ll look into in a moment.

First, imagine some generic <table> styles, using :where:

:where(table) {
  background-color: tan;
}

Now, if you add some other table styles before the :where selector, like this:

table {
  background-color: hotpink;
}

:where(table) {
  background-color: tan;
}

…the table background becomes hotpink, even though the table selector is specified before the :where selector in the cascade. That’s the beauty of :where, and why it’s already being used for CSS resets.

:where has a sibling, which has almost the exact opposite effect: the :is selector.

The specificity of the :is() pseudo-class is replaced by the specificity of its most specific argument. Thus, a selector written with :is() does not necessarily have equivalent specificity to the equivalent selector written without :is(). Selectors Level 4 specification

Expanding on our previous example:

:is(table) {
  --tbl-bgc: orange;
}
table {
  --tbl-bgc: tan;
}
:where(table) {
  --tbl-bgc: hotpink;
  background-color: var(--tbl-bgc);
}

The <table class="c-tbl"> background color will be tan because the specificity of :is is the same as table, but table is placed after.

However, if we were to change it to this:

:is(table, .c-tbl) {
  --tbl-bgc: orange;
}

…the background color will be orange, since :is has the weight of it’s heaviest selector, which is .c-tbl.

Example: A configurable table component

Now, let’s see how we can use :where in our components. We’ll be building a table component, starting with the HTML:

Let’s wrap .c-tbl in a :where-selector and, just for fun, add rounded corners to the table. That means we need border-collapse: separate, as we can’t use border-radius on table cells when the table is using border-collapse: collapse:

:where(.c-tbl) {
  border-collapse: separate;
  border-spacing: 0;
  table-layout: auto;
  width: 99.9%;
}

The cells use different styling for the <thead> and <tbody>-cells:

:where(.c-tbl thead th) {
  background-color: hsl(200, 60%, 40%);
  border-style: solid;
  border-block-start-width: 0;
  border-inline-end-width: 1px;
  border-block-end-width: 0;
  border-inline-start-width: 0;
  color: hsl(200, 60%, 99%);
  padding-block: 1.25ch;
  padding-inline: 2ch;
  text-transform: uppercase;
}
:where(.c-tbl tbody td) {
  background-color: #FFF;
  border-color: hsl(200, 60%, 80%);
  border-style: solid;
  border-block-start-width: 0;
  border-inline-end-width: 1px;
  border-block-end-width: 1px;
  border-inline-start-width: 0;
  padding-block: 1.25ch;
  padding-inline: 2ch;
}

And, because of our rounded corners and the missing border-collapse: collapse, we need to add some extra styles, specifically for the table borders and a hover state on the cells:

:where(.c-tbl tr td:first-of-type) {
  border-inline-start-width: 1px;
}
:where(.c-tbl tr th:last-of-type) {
  border-inline-color: hsl(200, 60%, 40%);
}
:where(.c-tbl tr th:first-of-type) {
  border-inline-start-color: hsl(200, 60%, 40%);
}
:where(.c-tbl thead th:first-of-type) {
  border-start-start-radius: 0.5rem;
}
:where(.c-tbl thead th:last-of-type) {
  border-start-end-radius: 0.5rem;
}
:where(.c-tbl tbody tr:last-of-type td:first-of-type) {
  border-end-start-radius: 0.5rem;
}
:where(.c-tbl tr:last-of-type td:last-of-type) {
  border-end-end-radius: 0.5rem;
}
/* hover */
@media (hover: hover) {
  :where(.c-tbl) tr:hover td {
    background-color: hsl(200, 60%, 95%);
  }
}

Now we can create variations of our table component by injecting other styles before or after our generic styles (courtesy of the specificity-stripping powers of :where), either by overwriting the .c-tbl element or by adding a BEM-style modifier-class (e.g. c-tbl--purple):

<table class="c-tbl c-tbl--purple">
.c-tbl--purple th {
  background-color: hsl(330, 50%, 40%)
}
.c-tbl--purple td {
  border-color: hsl(330, 40%, 80%);
}
.c-tbl--purple tr th:last-of-type {
  border-inline-color: hsl(330, 50%, 40%);
}
.c-tbl--purple tr th:first-of-type {
  border-inline-start-color: hsl(330, 50%, 40%);
}

Cool! But notice how we keep repeating colors? And what if we want to change the border-radius or the border-width? That would end up with a lot of repeated CSS.

Let’s move all of these to CSS custom properties and, while we’re at it, we can move all configurable properties to the top of the component’s “scope“ — which is the table element itself — so we can easily play around with them later.

CSS Custom Properties

I’m going to switch things up in the HTML and use a data-component attribute on the table element that can be targeted for styling.

<table data-component="table" id="table">

That data-component will hold the generic styles that we can use on any instance of the component, i.e. the styles the table needs no matter what color variation we apply. The styles for a specific table component instance will be contained in a regular class, using custom properties from the generic component.

[data-component="table"] {
  /* Styles needed for all table variations */
}
.c-tbl--purple {
  /* Styles for the purple variation */
}

If we place all the generic styles in a data-attribute, we can use whatever naming convention we want. This way, we don’t have to worry if your boss insists on naming the table’s classes something like .BIGCORP__TABLE, .table-component or something else.

In the generic component, each CSS property points to a custom property. Properties, that have to work on child-elements, like border-color, are specified at the root of the generic component:

:where([data-component="table"]) {
  /* These will will be used multiple times, and in other selectors */
  --tbl-hue: 200;
  --tbl-sat: 50%;
  --tbl-bdc: hsl(var(--tbl-hue), var(--tbl-sat), 80%);
}

/* Here, it's used on a child-node: */
:where([data-component="table"] td) {
  border-color: var(--tbl-bdc);
}

For other properties, decide whether it should have a static value, or be configurable with its own custom property. If you’re using custom properties, remember to define a default value that the table can fall back to in the event that a variation class is missing.

:where([data-component="table"]) {
  /* These are optional, with fallbacks */
  background-color: var(--tbl-bgc, transparent);
  border-collapse: var(--tbl-bdcl, separate);
}

If you’re wondering how I’m naming the custom properties, I’m using a component-prefix (e.g. --tbl) followed by an Emmett-abbreviation (e.g. -bgc). In this case, --tbl is the component-prefix, -bgc is the background color, and -bdcl is the border collapse. So, for example, --tbl-bgc is the table component’s background color. I only use this naming convention when working with component properties, as opposed to global properties which I tend to keep more general.

Now, if we open up DevTools, we can play around with the custom properties. For example, We can change --tbl-hue to a different hue value in the HSL color, set --tbl-bdrs: 0 to remove border-radius, and so on.

A :where CSS rule set showing the custom properties of the table showing how the cascade’s specificity scan be used in context.

When working with your own components, this is the point in time you’ll discover which parameters (i.e. the custom property values) the component needs to make things look just right.

We can also use custom properties to control column alignment and width:

:where[data-component="table"] tr > *:nth-of-type(1)) {
  text-align: var(--ca1, initial);
  width: var(--cw1, initial);
  /* repeat for column 2 and 3, or use a SCSS-loop ... */
}

In DevTools, select the table and add these to the element.styles selector:

element.style {
  --ca2: center; /* Align second column center */
  --ca3: right; /* Align third column right */
}

Now, let’s create our specific component styles, using a regular class, .c-tbl (which stands for “component-table” in BEM parlance). Let’s toss that class in the table markup.

<table class="c-tbl" data-component="table" id="table">

Now, let’s change the --tbl-hue value in the CSS just to see how this works before we start messing around with all of the property values:

.c-tbl {
  --tbl-hue: 330;
}

Notice, that we only need to update properties rather than writing entirely new CSS! Changing one little property updates the table’s color — no new classes or overriding properties lower in the cascade.

Notice how the border colors change as well. That’s because all the colors in the table inherit from the --tbl-hue variable

We can write a more complex selector, but still update a single property, to get something like zebra-striping:

.c-tbl tr:nth-child(even) td {
  --tbl-td-bgc: hsl(var(--tbl-hue), var(--tbl-sat), 95%);
}

And remember: It doesn’t matter where you load the class. Because our generic styles are using :where, the specificity is wiped out, and any custom styles for a specific variation will be applied no matter where they are used. That’s the beauty of using :where to take control of the cascade!

And best of all, we can create all kinds of table components from the generic styles with a few lines of CSS.

Purple table with zebra-striped columns
Light table with a “noinlineborder” parameter… which we’ll cover next

Adding parameters with another data-attribute

So far, so good! The generic table component is very simple. But what if it requires something more akin to real parameters? Perhaps for things like:

  • zebra-striped rows and columns
  • a sticky header and sticky column
  • hover-state options, such as hover row, hover cell, hover column

We could simply add BEM-style modifier classes, but we can actually accomplish it more efficiently by adding another data-attribute to the mix. Perhaps a data-param that holds the parameters like this:

<table data-component="table" data-param="zebrarow stickyrow">

Then, in our CSS, we can use an attribute-selector to match a whole word in a list of parameters. For example, zebra-striped rows:

[data-component="table"][data-param~="zebrarow"] tr:nth-child(even) td {
  --tbl-td-bgc: var(--tbl-zebra-bgc);
}

Or zebra-striping columns:

[data-component="table"][data-param~="zebracol"] td:nth-of-type(odd) {
  --tbl-td-bgc: var(--tbl-zebra-bgc);
}

Let’s go nuts and make both the table header and the first column sticky:


[data-component="table"][data-param~="stickycol"] thead tr th:first-child,[data-component="table"][data-param~="stickycol"] tbody tr td:first-child {
  --tbl-td-bgc: var(--tbl-zebra-bgc);
  inset-inline-start: 0;
  position: sticky;
}
[data-component="table"][data-param~="stickyrow"] thead th {
  inset-block-start: -1px;
  position: sticky;
}

Here’s a demo that allows you to change one parameter at a time:

The default light theme in the demo is this:

.c-tbl--light {
  --tbl-bdrs: 0;
  --tbl-sat: 15%;
  --tbl-th-bgc: #eee;
  --tbl-th-bdc: #eee;
  --tbl-th-c: #555;
  --tbl-th-tt: normal;
}

…where data-param is set to noinlineborder which corresponds to these styles:

[data-param~="noinlineborder"] thead tr > th {
  border-block-start-width: 0;
  border-inline-end-width: 0;
  border-block-end-width: var(--tbl-bdw);
  border-inline-start-width: 0;
}

I know my data-attribute way of styling and configuring generic components is very opinionated. That’s just how I roll, so please feel free to stick with whatever method you’re most comfortable working with, whether it’s a BEM modifier class or something else.

The bottom line is this: embrace :where and :is and the cascade-controlling powers they provide. And, if possible, construct the CSS in such a way that you wind up writing as little new CSS as possible when creating new component variations!

Cascade Layers

The last cascade-busting tool I want to look at is “Cascade Layers.” At the time of this writing, it’s an experimental feature defined in the CSS Cascading and Inheritance Level 5 specification that you can access in Safari or Chrome by enabling the #enable-cascade-layers flag.

Bramus Van Damme sums up the concept nicely:

The true power of Cascade Layers comes from its unique position in the Cascade: before Selector Specificity and Order Of Appearance. Because of that we don’t need to worry about the Selector Specificity of the CSS that is used in other Layers, nor about the order in which we load CSS into these Layers — something that will come in very handy for larger teams or when loading in third-party CSS.

Perhaps even nicer is his illustration showing where Cascade Layers fall in the cascade:

Credit: Bramus Van Damme

At the beginning of this article, I mentioned ITCSS — a way of taming the cascade by specifying the load-order of generic styles, components etc. Cascade Layers allow us to inject a stylesheet at a given location. So a simplified version of this structure in Cascade Layers looks like this:

@layer generic, components;

With this single line, we’ve decided the order of our layers. First come the generic styles, followed by the component-specific ones.

Let’s pretend that we’re loading our generic styles somewhere much later than our component styles:

@layer components {
  body {
    background-color: lightseagreen;
  }
}

/* MUCH, much later... */

@layer generic { 
  body {
    background-color: tomato;
  }
}

The background-color will be lightseagreen because our component styles layer is set after the generic styles layer. So, the styles in the components layer “win” even if they are written before the generic layer styles.

Again, just another tool for controlling how the CSS cascade applies styles, allowing us more flexibility to organize things logically rather than wrestling with specificity.

Now you’re in control!

The whole point here is that the CSS cascade is becoming a lot easier to wrangle, thanks to new features. We saw how the :where and :is pseudo-selectors allows us to control specificity, either by stripping out the specificity of an entire ruleset or taking on the specificity of the most specific argument, respectively. Then we used CSS Custom Properties to override styles without writing a new class to override another. From there, we took a slight detour down data-attribute lane to help us add more flexibility to create component variations merely by adding arguments to the HTML. And, finally, we poked at Cascade Layers which should prove handy for specifying the loading order or styles using @layer.

If you leave with only one takeaway from this article, I hope it’s that the CSS cascade is no longer the enemy it’s often made to be. We are gaining the tools to stop fighting it and start leaning into even more.


Header photo by Stephen Leonardi on Unsplash


Don’t Fight the Cascade, Control It! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/dont-fight-the-cascade-control-it/feed/ 4 359886
@supports selector() https://css-tricks.com/supports-selector/ https://css-tricks.com/supports-selector/#comments Tue, 19 Oct 2021 21:12:18 +0000 https://css-tricks.com/?p=354115 I didn’t realize the support for @supports determining selector support was so good! I usually think of @supports as a way to test for property: value pair support. But with the selector() function, we can test for selector support …


@supports selector() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I didn’t realize the support for @supports determining selector support was so good! I usually think of @supports as a way to test for property: value pair support. But with the selector() function, we can test for selector support as well. It looks like this:

@supports selector(:nth-child(1 of .foo)) {

}

You just drop the selector right between the parens and that’s what it tests for.

That selector above is a pretty good test, actually. It’s a “selector list argument” that works for the :nth-child ‘n’ friends selectors. As I write, it’s only supported in Safari.

So let’s say your ideal situation is that the browser supports this selector. Here’s an example. You know that with <ol> and <ul> the only valid child element is <li>. But also say this list needs separators, so you (and I’m not saying this is a great idea) did this kind of thing:

<ul>
  <li class="list-item">List item</li>
  <li class="list-item">List item</li>
  <li class="separator"></li>
  /* ... */
</ul>

Then you also want to zebra-stripe the list. And, if you want zebra striping, you need to select every other .list-item, ignoring the .separator. So…

li:nth-child(odd of .list-item) {
  background: lightgoldenrodyellow;
}

But only Safari supports that… so you can do:

@supports selector(:nth-child(1 of .foo)) {
  li:nth-child(odd of .list-item) {
    background: lightgoldenrodyellow;
  }
}

If you didn’t care what the fallback was, you wouldn’t even have to bother with the @supports at all. But say you do care about the fallback. Perhaps in the supported situation, the zebra striping does the heavy lifting of the UX you are shooting for, so all you need for the seperator is a bit of space. But for non-supporting browsers, you’ll need something beefier because you don’t have the zebra striping.

So now you can style both situations:

@supports selector(:nth-child(1 of .foo)) {
  li {
    padding: 0.25em;
  }
  li:nth-child(odd of .list-item) {
    background: lightgoldenrodyellow;
  }
  li.separator {
    list-style: none;
    margin: 0.25em 0;
  }
}
@supports not selector(:nth-child(1 of .foo)) {
  li.separator {
    height: 1px;
    list-style: none;
    border-top: 1px dashed purple;
    margin: 0.25em 0;
  }
}

If we get the @when syntax, then we can write it a little cleaner:

/* Maybe? */
@when supports(selector(:nth-child(1 of .foo))) {

} @else {

}

Anyway. The end result is…

Supported
Not Supported

There is a JavaScript API for testing support as well. I wasn’t sure if this would actually work, but it appears to! This fails in Chrome and passes in Safari as I write:

CSS.supports("selector(:nth-child(1 of .foo))")

While I was putting this together, I was thinking… hmmmmmmm — what CSS selectors are out there that have weird cross-browser support? It’s really not that many. And even of those that do have weird cross-browser support, thinking of the number of use-cases where you care to actually wrap it in an @supports (rather than just let it fail) is fairly few.

The ::marker pseudo-element would have been a great one, but it’s pretty well supported now. I was thinking the case-insensitive attribute selector, like [href$="pdf" i], would have been a good one, but nope, also well supported. Same deal with the comma-separated :not(a, .b, [c]). Maybe something like :fullscreen / :-webkit-full-screen would be interesting and useful because it’s uniquely not supported in iOS Safari?


@supports selector() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/supports-selector/feed/ 2 354115
Using the Specificity of :where() as a CSS Reset https://css-tricks.com/using-the-specificity-of-where-as-a-css-reset/ https://css-tricks.com/using-the-specificity-of-where-as-a-css-reset/#comments Mon, 12 Jul 2021 14:30:34 +0000 https://css-tricks.com/?p=343613 I don’t know about you, but I write these three declarations many times in my CSS:

ul {
  padding: 0;
  margin: 0;
  list-style-type: none;
}

You might yell at me and say I can just put those in my CSS …


Using the Specificity of :where() as a CSS Reset originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I don’t know about you, but I write these three declarations many times in my CSS:

ul {
  padding: 0;
  margin: 0;
  list-style-type: none;
}

You might yell at me and say I can just put those in my CSS resets. I wish I could, but I don‘t want to and I’ll tell you why in a second.

User agents set values to those properties in a list for a purpose, and that is to make lists more readable. These are the default styles in chromium browsers for a <ul> element:

ul {
  list-style-type: disc;
  margin-block-start: 1em;
  margin-block-end: 1em;
  margin-inline-start: 0px;
  margin-inline-end: 0px;
  padding-inline-start: 40px;
}

So, without adding any class in HTML or style in CSS, we get those for free. That‘s a nice thing and I don‘t want to lose it. But I would appreciate it if I could make the browser understand that there is very high chance I don’t want that default feature in cases where I add a class to the element.

So here is a quick solution to reset a <ul> element that has a class:

ul[class] {
  padding: 0;
  margin: 0;
  list-style-type: none;
}

Now I don’t lose the default style except when I add a class to my <ul> element.

The problem

There is a problem with this solution. Imagine there is a list that we want to have a different list-style-type for it, like the following:

ul[class] {
  padding: 0;
  margin: 0;
  list-style-type: none;
}

.list {
  list-style-type: square;
}

This doesn’t work since ul[class] has higher specificity. That’s where our solution breaks down.

We could add more weight to the selector’s specificity:

ul.list {
  list-style-type: square; /* Specificity: 0, 1, 1 */
}

/* or */

.sidebar .list {
  list-style-type: square; /* Specificity: 0, 2, 0 */
}

If you are OK adding more weight to the selector, you are good to go. But I’m not OK with it, personally. For example, I don’t want to put the element’s name in my CSS most of the times due to a separation of concerns principle. Or, if you are following BEM methodology, problems will most certainly arise as this conflicts with it.

So what can we do?

The solution

A few months ago, I learned about some hot selectors, including :is() and :where(). One thing about these two functional pseudo selectors, is that they can change specificity, giving us the power to nullify or increase that specificity.

The key about :where() is that it always has 0 specificity. So we can get rid of our problem very easily like this:

:where(ul[class]) {
  list-style: none;
}

.list {
  list-style: square; /* Now this works like a charm! */
}

With the power of this selector, libraries can give us style with no specificity. So there would be no specificity to compete with when we as authors write CSS.

Demo

In the following demo, you can remove :where() to see what we talked about in action:


Using the Specificity of :where() as a CSS Reset originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/using-the-specificity-of-where-as-a-css-reset/feed/ 6 343613
:where https://css-tricks.com/almanac/selectors/w/where/ Tue, 23 Mar 2021 15:06:11 +0000 https://css-tricks.com/?page_id=336866 The :where() pseudo selector in CSS is functionally identical to the :is() psuedo selector in that it takes a comma-separated list of selectors to match against, except that where :is() takes the most specific among them as the specificity of …


:where originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The :where() pseudo selector in CSS is functionally identical to the :is() psuedo selector in that it takes a comma-separated list of selectors to match against, except that where :is() takes the most specific among them as the specificity of that whole part, the specificity of :where() is always zero (0).

For example:

main :where(h1, h2, h3) {
  color: orange;
}

Matches as if you wrote that selector like this:

main h1, main h2, main h3 {
  color: orange;
}

…except the specificity of the first example above is (0, 0, 0, 1) for the single element selector (main) and doesn’t include the specificity of anything in the :where() part. Whereas, in the second example, the specificity is (0, 0, 0, 2) because there are two element selectors.

Deciding on specificity handling

It’s a tough call deciding if you want the specificity of :is() or the zero-specificity of :where(), although I might argue that keeping specificity low in general leads to fewer headaches than working with elements that have a higher specificity.

Imagine a classic layout:

<header class="header"></header>
<main class="main"></header>
<aside class="aside"></aside>
<footer class="footer"></footer>

If I want to select the links in the header and footer, I could…

:is(.header, .footer) a {
  color: red;
}

That selector is (0, 0, 1, 0), meaning if I were to write:

header a {
  color: orange;
}

That selector is less specific at (0, 0, 0, 2), and will not override the other. But if I wrote:

:where(.header, .footer) a {
  color: red;
}

That is only (0, 0, 0, 1), so my override will work.

Perhaps a less-contrived example is selecting a header by ID without having to resort to ID-level specificity.

h3:where(#specific-header) {
  outline: 1px solid yellow;
}

There I’ve selected a header with a specific ID. IDs have very high specificity — (0, 1, 0, 0) — which is hard to override, but here I’ve managed to select just that header, but not use any super high specificity to do it.

Forgiving selectors

Each selector within the comma-separated list within :where(x, y, z) is forgiving in that it is ignored if it is invalid. That is important because it doesn’t wipe out the entire selector like invalid selectors normally do. For more information, see how it works with the :is() selector since it is identical.

:where(h1, totally:fake) {
  /* will still select h1 */
}

Browser support

IEEdgeFirefoxChromeSafariOpera
No8878881474
Android ChromeAndroid FirefoxAndroid BrowseriOS SafariOpera Mobile
AllAll9114.4No
Source: caniuse

More information


:where originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
336866
CSS :is() and :where() are coming to browsers https://css-tricks.com/css-is-and-where-are-coming-to-browsers/ Wed, 10 Jun 2020 14:45:00 +0000 https://css-tricks.com/?p=312524 Šime Vidas with the lowdown on what these pseudo-selectors are and why they will be useful:

  • :is() is to reduce repetition¹ of parts of comma-separated selectors.
  • :where() is the same, but nothing inside it affects specificity. The example of


CSS :is() and :where() are coming to browsers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Šime Vidas with the lowdown on what these pseudo-selectors are and why they will be useful:

  • :is() is to reduce repetition¹ of parts of comma-separated selectors.
  • :where() is the same, but nothing inside it affects specificity. The example of wrapping :where(:not()) is really great, as now there is a way to use :not() without bumping up the selector weight in a way you very likely don’t want.
  1. This is something that CSS preprocessors are good at (via nesting). Another nice little example of community-built tech pushing forward and native tech coming up later to help once the idea is fleshed out.

To Shared LinkPermalink on CSS-Tricks


CSS :is() and :where() are coming to browsers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
312524
:is https://css-tricks.com/almanac/selectors/i/is/ Thu, 21 Dec 2017 16:02:59 +0000 http://css-tricks.com/?page_id=263984 The pseudo-select :is() in CSS allows you to write compound selectors more tersely. For example, rather than writing:

ul li,
ol li {}

We could write:

:is(ul, ol) li {}

This can make quick work of otherwise extremely verbose, complex, …


:is originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The pseudo-select :is() in CSS allows you to write compound selectors more tersely. For example, rather than writing:

ul li,
ol li {}

We could write:

:is(ul, ol) li {}

This can make quick work of otherwise extremely verbose, complex, and error prone selectors. See:

:is(section, article, aside, nav) :is(h1, h2, h3, h4, h5, h6) {
  color: #BADA55;
}

/* ... which would be the equivalent of: */
section h1, section h2, section h3, section h4, section h5, section h6, 
article h1, article h2, article h3, article h4, article h5, article h6, 
aside h1, aside h2, aside h3, aside h4, aside h5, aside h6, 
nav h1, nav h2, nav h3, nav h4, nav h5, nav h6 {
  color: #BADA55;
}

You can attach the pseudo-selector to an element like you’d expect. Like to select a specific type of element when it has either of two classes:

div:is(.cool, .fun) {
  color: red;
}

/*
<div class="cool">match</div>
<div class="fun">match</div>
<p class="fun">not match</p>
*/

Hey, isn’t that like CSS preprocessing?

Simplifying selectors with :is() is similar to how CSS preprocessors handle nested rules:

/* SCSS written like this: */
div, p, ul, ol {
  span {
    /* ... */
  }
}

/* after processing becomes: */
div span, p span, ul span, ol span {
  /* ...*/
}

/* which is a lot like the effect of :is()! */

But beware! Preprocessors, like Sass, “unroll” your nested rules into a list of easily-understood selectors. :is() will handle specificity rules a little differently.

Specificity of :is()

According to the CSS4 Working Draft:

The specificity of the :is() pseudo-class is replaced by the specificity of its most specific argument. Thus, a selector written with :is() does not necessarily have equivalent specificity to the equivalent selector written without :is().

That means that the specificity of :is() gets auto-upgraded to the most specific item in the list of arguments provided:

/* This has higher precedence... */
:is(ol, .list, ul) li { /* ... */ }

/* ...than this, even though this is later... */
ol li  { /* ... */ }

/* ...because :is() has the weight of it's heaviest selector, which is `.list`! */

Forgiving selector lists

Normally if any part of a selector is invalid, the entire block is thrown out:

p, p::not-real {
  color: red; /* nothing will be red, as ::not-real is an invalid selector */
}

I’ve heard browsers might chill out on this in the future, but we aren’t there yet. If you are hoping to keep that and not have to break it into two separate blocks, is() can help because it’s “forgiving”:

:is(p, p::not-real) { /* this is fine */
  color: red;
}

I always think about ::selection and the ::-moz-selection vendor prefix version when I think of invalid comma-separated selectors, but…

:is(::selection, ::-moz-selection) { /* this doesn't work in Chrome for some reason 🤷‍♀️ */
  background: yellow;
}

Browser support

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
8878No8814

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
11511511514.0-14.4

To get the best support, you might look at also using :matches (with vendor-prefixed :any filling in some gaps) for the general functionality. And, :not() is another pseudo-class that can assist with matching.

What’s interesting to note is that :is() was introduced after :matches which was introduced after :any. It’s sort of like :any is being replaced by :matches which is being replaced by :is(), with the details changing along the way. Always neat to see how these things evolve.

To get maximum support for “Matches-Any” requires using a mix of the historical names, as browser handling is currently a hodgepodge of vendor-prefixes and experimental settings at this point.

/* These are deprecated, but still necessary in some browsers: */
:-moz-any(div, p) > em { /* ... */ }
:-webkit-any(div, p) > em { /* ... */ }

/* Has been replaced by :is() in CSS4, but still supported 
by some browsers with experimental features enabled */
:matches(div, p) > em { /* ... */ }

/* Latest syntax */
:is(div, p) > em { /* ... */ }

History

Originally, this pseudo-class was named :any() and was implemented with limited vendor-specific support:

/* Never actually worked */
:any(div, p) > em { /* ... */ }

The “Matches-Any” pseudo-class name was then changed to :matches() in early versions of the CSS4 working draft, with additional (incomplete) support being given to some browsers.

/* Sort of works */
:matches(div, p) > em { /* ... */ }

The goal of the “Matches Any” selector is to make complex groupings of selectors easier to write, which is exactly what we got and more with :is().

More information


:is originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
263984