:is – 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.4.3 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 :is – 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
Platform News: Prefers Contrast, MathML, :is(), and CSS Background Initial Values https://css-tricks.com/platform-news-prefers-contrast-mathml-is-and-css-background-initial-values/ https://css-tricks.com/platform-news-prefers-contrast-mathml-is-and-css-background-initial-values/#comments Fri, 19 Mar 2021 15:05:14 +0000 https://css-tricks.com/?p=336679 In this week’s round-up, prefers-contrast lands in Safari, MathML gets some attention, :is() is actually quite forgiving, more ADA-related lawsuits, inconsistent initial values for CSS Backgrounds properties can lead to unwanted — but sorta neat — patterns.

The prefers-contrast: more


Platform News: Prefers Contrast, MathML, :is(), and CSS Background Initial Values originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In this week’s round-up, prefers-contrast lands in Safari, MathML gets some attention, :is() is actually quite forgiving, more ADA-related lawsuits, inconsistent initial values for CSS Backgrounds properties can lead to unwanted — but sorta neat — patterns.

The prefers-contrast: more media query is supported in Safari Preview

After prefers-reduced-motion in 2017, prefers-color-scheme in 2019, and forced-colors in 2020, a fourth user preference media feature is making its way to browsers. The CSS prefers-contrast: more media query is now supported in the preview version of Safari. This feature will allow websites to honor a user’s preference for increased contrast.

Screenshot of the iPhone 12 landing page on Apple's website. A big red arrow points out light grey text on the page.
Apple could use this new media query to increase the contrast of gray text on its website
.pricing-info {
  color: #86868b; /* contrast ratio 3.5:1 */
}

@media (prefers-contrast: more) {
  .pricing-info {
    color: #535283; /* contrast ratio 7:1 */
  }
}

Making math a first-class citizen on the web

One of the earliest specifications developed by the W3C in the mid-to-late ’90s was a markup language for displaying mathematical notations on the web called MathML. This language is currently supported in Firefox and Safari. Chrome’s implementation was removed in 2013 because of “concerns involving security, performance, and low usage on the Internet.”

If you’re using Chrome or Edge, enable “Experimental Web Platform features” on the about:flags page to view the demo.

There is a renewed effort to properly integrate MathML into the web platform and bring it to all browsers in an interoperable way. Igalia has been developing a MathML implementation for Chromium since 2019. The new MathML Core Level 1 specification is a fundamental subset of MathML 3 (2014) that is “most suited for browser implementation.” If approved by the W3C, a new Math Working Group will work on improving the accessibility and searchability of MathML.

The mission of the Math Working Group is to promote the inclusion of mathematics on the Web so that it is a first-class citizen of the web that displays well, is accessible, and is searchable.

CSS :is() upgrades selector lists to become forgiving

The new CSS :is() and :where() pseudo-classes are now supported in Chrome, Safari, and Firefox. In addition to their standard use cases (reducing repetition and keeping specificity low), these pseudo-classes can also be used to make selector lists “forgiving.”

For legacy reasons, the general behavior of a selector list is that if any selector in the list fails to parse […] the entire selector list becomes invalid. This can make it hard to write CSS that uses new selectors and still works correctly in older user agents.

In other words, “if any part of a selector is invalid, it invalidates the whole selector.” However, wrapping the selector list in :is() makes it forgiving: Unsupported selectors are simply ignored, but the remaining selectors will still match.

Unfortunately, pseudo-elements do not work inside :is() (although that may change in the future), so it is currently not possible to turn two vendor-prefixed pseudo-elements into a forgiving selector list to avoid repeating styles.

/* One unsupported selector invalidates the entire list */
::-webkit-slider-runnable-track, ::-moz-range-track {
  background: red;
}

/* Pseudo-elements do not work inside :is() */
:is(::-webkit-slider-runnable-track, ::-moz-range-track) {
  background: red;
}

/* Thus, the styles must unfortunately be repeated */
::-webkit-slider-runnable-track {
  background: red;
}
::-moz-range-track {
  background: red;
}

Dell and Kraft Heinz sued over inaccessible websites

More and more American businesses are facing lawsuits over accessibility issues on their websites. Most recently, the tech corporation Dell was sued by a visually impaired person who was unable to navigate Dell’s website and online store using the JAWS and VoiceOver screen readers.

The Defendant fails to communicate information about its products and services effectively because screen reader auxiliary aids cannot access important content on the Digital Platform. […] The Digital Platform uses visual cues to convey content and other information. Unfortunately, screen readers cannot interpret these cues and communicate the information they represent to individuals with visual disabilities.

Earlier this year, Kraft Heinz Foods Company was sued for failing to comply with the Web Content Accessibility Guidelines on one of the company’s websites. The complaint alleges that the website did not declare a language (lang attribute) and provide accessible labels for its image links, among other things.

In the United States, the Americans with Disabilities Act (ADA) applies to websites, which means that people can sue retailers if their websites are not accessible. According to the CEO of Deque Systems (the makers of axe), the recent increasing trend of web-based ADA lawsuits can be attributed to a lack of a single overarching regulation that would provide specific compliance requirements.

background-clip and background-origin have different initial values

By default, a CSS background is painted within the element’s border box (background-clip: border-box) but positioned relative to the element’s padding box (background-origin: padding-box). This inconsistency can result in unexpected patterns if the element’s border is semi-transparent or dotted/dashed.

A pink and triple rectangle with rounded edges. The colors overlap in a pattern.
.box {
  /* semi-transparent border */
  border: 20px solid rgba(255, 255, 255, 0.25);

  /* background gradient */
  background: conic-gradient(
    from 45deg at bottom left,
    deeppink,
    rebeccapurple
  );
}

Because of the different initial values, the background gradient in the above image is repeated as a tiled image on all sides under the semi-transparent border. In this case, positioning the background relative to the border box (background-origin: border-box) makes more sense.


Platform News: Prefers Contrast, MathML, :is(), and CSS Background Initial Values originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/platform-news-prefers-contrast-mathml-is-and-css-background-initial-values/feed/ 1 336679
The CSS :has Selector (and 4+ Examples) https://css-tricks.com/the-css-has-selector/ https://css-tricks.com/the-css-has-selector/#comments Wed, 17 Mar 2021 18:40:16 +0000 https://css-tricks.com/?p=336424 The CSS :has selector helps you select elements when they contain other elements that match the selector you pass into :has().


The CSS :has Selector (and 4+ Examples) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The CSS :has selector helps you select elements that contain elements that match the selector you pass into the :has() function. It’s essentially a “parent” selector, although far more useful than just that. For example, imagine being able to select all <div>s but only when they contain a <p>. That’s what we can do:

div:has(p) {
  background: red;
}
/*
  <div> <!-- selected! -->
     <p></p>
  <div>

  <div></div> <!-- not selected -->
  <div> <!-- not selected -->
    <section></section>
  </div>
*/

Although it’s not supported in any browser as I write, it has now dropped in Safari Technical Preview 137, so it’s starting to happen!

Pretty darn handy right! Here’s another example. Say you want space after your headers. Of course! A bit of margin-block-end on your h2 should do it. But… what if there is a subtitle? Now we can select a parent on the condition a subtitle is present and adjust the spacing.

h2,
.subtitle {
  margin: 0 0 1.5rem 0;
}
.header-group:has(h2):has(.subtitle) h2 {
  margin: 0 0 0.2rem 0; /* reduce spacing on header, because subtitle will handle it */
}

/*
  <div class="header-group">
    <h2>Blog Post Title</h2> <!-- main spacing applied here -->
  </div>

  <div class="header-group">
    <h2>Blog Post Title</h2>
    <div class="subtitle"> <!-- main spacing applied here -->
      This is a subtitle
    </div>
  </div>
*/
The CSS :has selector being helpful in spacing headers with subtitles (or not).
On the left, the main spacing happens after the h2, on the right, the main spacing happens after the subtitle.

The way I think about :has is this: it’s a parent selector pseudo-class. That is CSS-speak for “it lets you change the parent element if it has a child or another element that follows it.” This might feel weird! It might break your mental model of how CSS works. This is how I’m used to thinking about CSS:

.parent .child {
  color: red;
}

You can only style down, from parent to child, but never back up the tree. :has completely changes this because up until now there have been no parent selectors in CSS and there are some good reasons why. Because of the way in which browsers parse HTML and CSS, selecting the parent if certain conditions are met could lead to all sorts of performance concerns.

If I sit down and think about all the ways I might use :has today, I sort of get a headache. It would open up this pandora’s box of opportunities that have never been possible with CSS alone.

Another example: let’s say we want to only apply styles to links that have images in them:

a:has(> img) {
  border: 20px solid white;
}

This would be helpful from time to time. I can also see :has being used for conditionally adding margin and padding to elements depending on their content. That would be neat.

The :has selector is part of the CSS Selectors Level 4 specification which is the same spec that has the extremely useful :not pseudo-class.


You could argue that the CSS :has selector is more powerful than just a “parent” selector, which is exactly what Bramus has done! Like in the subheadings example above, you aren’t necessarily ultimately selecting the parent, you might select the parent in a has-condition, but then ultimately select a child element from there.

/*  Matches <figure> elements that have a <figcaption> as a child element */
figure:has(figcaption) { … }

/* Matches <img> elements that is a child of a <figure> that contains a <figcaption> child element */
figure:has(figcaption) img { … }

There you can quickly see that the second selector is selecting a child <img>, not just the parent of the <figcaption>.

Selector List

You can chain it:

article:has(h2):has(ul) {

}

Or give it a selector list:

article:has(h2, ul) {

}

And the list is forgiving: The list is no longer “forgiving” after the W3C adopted a resolution in December 2020 in response to a reported issue. So, if the selector list contains even one invalid argument, the entire list is ignored:

/* 👎 */
article:has(h2, ul, ::-blahdeath) {
  /* ::blahdeath is invalid, making the entire selector invalid. */
}

A workaround is to nest a more forgiving selector in there, such as :is() or :where():

/* 👍 */
article:has(:where(h2, ul, ::-blahdeath)) {
  /* :where is a forgiving selector, making this valid. */
}

Testing for Support

@supports(selector(:has(p))) {
  /* Supported! */
}

The :not() selector is part of the same spec…

Unlike :has, :not does have pretty decent browser support and I used it for the first time the other day:

ul li:not(:first-of-type) {
  color: red;
}

That’s great I also love how gosh darn readable it is; you don’t ever have to have seen this line of code to understand what it does.

Another way you can use :not is for margins:

ul li:not(:last-of-type) {
  margin-bottom: 20px;
}

So every element that is not the last item gets a margin. This is useful if you have a bunch of elements in a card, like this:

… and also :is() and :where()

CSS Selectors Level 4 is also the same spec that has the :is selector that can be used like this today in a lot of browsers:

: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;
}

More Info

So that’s it! :has should be quite useful to use soon, and its cousins :is and :not can be fabulously helpful already and that’s only a tiny glimpse — just three CSS pseudo-classes — that are available in this new spec.


The CSS :has Selector (and 4+ Examples) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/the-css-has-selector/feed/ 29 336424
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 “is” Useful? https://css-tricks.com/is-is-useful/ https://css-tricks.com/is-is-useful/#comments Fri, 03 Jan 2020 17:05:31 +0000 https://css-tricks.com/?p=300774 God I’m funny.

Anytime we have fairly repetitive selectors that have a common parent, it’s probably a place we can use the :is() pseudo-selector.

Holger Bartel demonstrates like this:

section section h1, section article h1, section aside h1, section nav 


Is “is” Useful? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
God I’m funny.

Anytime we have fairly repetitive selectors that have a common parent, it’s probably a place we can use the :is() pseudo-selector.

Holger Bartel demonstrates like this:

section section h1, section article h1, section aside h1, section nav h1,
article section h1, article article h1, article aside h1, article nav h1,
aside section h1, aside article h1, aside aside h1, aside nav h1,
nav section h1, nav article h1, nav aside h1, nav nav h1 {
  font-size: 20px;
}

Becomes:

:is(section, article, aside, nav)
:is(section, article, aside, nav) h1 {
  font-size: 20px;
}

Adam Argyle demonstrated like this:

MDN has an extra dramatic one:

ol ol ul,     ol ul ul,     ol menu ul,     ol dir ul,
ol ol menu,   ol ul menu,   ol menu menu,   ol dir menu,
ol ol dir,    ol ul dir,    ol menu dir,    ol dir dir,
ul ol ul,     ul ul ul,     ul menu ul,     ul dir ul,
ul ol menu,   ul ul menu,   ul menu menu,   ul dir menu,
ul ol dir,    ul ul dir,    ul menu dir,    ul dir dir,
menu ol ul,   menu ul ul,   menu menu ul,   menu dir ul,
menu ol menu, menu ul menu, menu menu menu, menu dir menu,
menu ol dir,  menu ul dir,  menu menu dir,  menu dir dir,
dir ol ul,    dir ul ul,    dir menu ul,    dir dir ul,
dir ol menu,  dir ul menu,  dir menu menu,  dir dir menu,
dir ol dir,   dir ul dir,   dir menu dir,   dir dir dir {
  list-style-type: square;
}
:is(ol, ul, menu, dir) :is(ol, ul, menu, dir) ul,
:is(ol, ul, menu, dir) :is(ol, ul, menu, dir) menu,
:is(ol, ul, menu, dir) :is(ol, ul, menu, dir) dir {
  list-style-type: square;
}

It’s less code and easier to reason.

Kezz Bracey notes that pairing it with :not() can be nice as well:

:not(article, section, aside) :is(h1, h2, h3, h4, h5, h6) {
  font-weight: 400;
}

Browser support is just starting to get there and polyfilling is hard, so we aren’t at day-to-day no-brainer use levels quite yet. I’d bet it’s not too far away.


Is “is” Useful? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/is-is-useful/feed/ 15 300774
: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
12312412314.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