Liam Johnston – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Mon, 23 Jan 2023 16:00:57 +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 Liam Johnston – CSS-Tricks https://css-tricks.com 32 32 45537868 More Real-World Uses for :has() https://css-tricks.com/more-real-world-uses-for-has/ https://css-tricks.com/more-real-world-uses-for-has/#comments Fri, 20 Jan 2023 15:34:09 +0000 https://css-tricks.com/?p=376331 The :has() pseudo-class is, hands-down, my favorite new CSS feature. I know it is for many of you as well, at least those of you who took the State of CSS survey. The ability to write selectors upside down …


More Real-World Uses for :has() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The :has() pseudo-class is, hands-down, my favorite new CSS feature. I know it is for many of you as well, at least those of you who took the State of CSS survey. The ability to write selectors upside down gives us more superpowers I’d never thought possible.

I say “more superpowers” because there have already been a ton of really amazing clever ideas published by a bunch of super smart people, like:

This article is not a definitive guide to :has(). It’s also not here to regurgitate what’s already been said. It’s just me (hi 👋) jumping on the bandwagon for a moment to share some of the ways I’m most likely to use :has() in my day-to-day work… that is, once browser support is good enough. (Firefox is the last holdout, but will support it, which is imminent.)

When that does happen, you can bet I’ll start using :has() all over the place. Here are some real-world examples of things I’ve built recently and thought to myself, “Gee, this’ll be so much nicer once :has() is fully supported.”

Avoid having to reach outside your JavaScript component

Have you ever built an interactive component that sometimes needs to affect styles somewhere else on the page? Take the following example, where <nav> is a mega menu, and opening it changes the colors of the <header> content above it.

I feel like I need to do this kind of thing all the time.

This particular example is a React component I made for a site. I had to “reach outside” the React part of the page with document.querySelector(...) and toggle a class on the <body>, <header>, or another component. That’s not the end of the world, but it sure feels a bit yuck. Even in a fully React site (a Next.js site, say), I’d have to choose between managing a menuIsOpen state way higher up the component tree, or do the same DOM element selection — which isn’t very React-y.

With :has(), the problem goes away:

header:has(.megamenu--open) {
  /* style the header differently if it contains 
    an element with the class ".megamenu--open"
  */
}

No more fiddling with other parts of the DOM in my JavaScript components!

Better table striping UX

Adding alternate row “stripes” to your tables can be a nice UX improvement. They help your eyes keep track of which row you’re on as you scan the table.

But in my experience, this doesn’t work great on tables with just two or three rows. If you have, for example, a table with three rows in the <tbody> and you’re “striping” every “even” row, you could end up with just one stripe. That’s not really worth a pattern and might have users wondering what’s so special about that one highlighted row.

Using this technique where Bramus uses :has() to apply styles based on the number of children, we can apply table stripes when there are more than, say, three rows:

What to get fancier? You could also decide to only do this if the table has at least a certain number of columns, too:

table:has(:is(td, th):nth-child(3)) {
  /* only do stuff if there are three or more columns */
}

Remove conditional class logic from templates

I often need to change a page layout depending on what’s on the page. Take the following Grid layout, where the placement of the main content changes grid areas depending on whether there’s a sidebar present.

Layout with left sidebar above a layout with no sidebar.

That’s something that might depend on whether there are sibling pages set in the CMS. I’d normally do this with template logic to conditionally add BEM modifier classes to the layout wrapper to account for both layouts. That CSS might look something like this (responsive rules and other stuff omitted for brevity):

/* m = main content */
/* s = sidebar */
.standard-page--with-sidebar {
  grid-template-areas: 's s s m m m m m m m m m';
}
.standard-page--without-sidebar {
  grid-template-areas: '. m m m m m m m m m . .';
}

CSS-wise, this is totally fine, of course. But it does make the template code a little messy. Depending on your templating language it can get pretty ugly to conditionally add a bunch of classes, especially if you have to do this with lots of child elements too.

Contrast that with a :has()-based approach:

/* m = main content */
/* s = sidebar */
.standard-page:has(.sidebar) {
  grid-template-areas: 's s s m m m m m m m m m';
}
.standard-page:not(:has(.sidebar)) {
  grid-template-areas: '. m m m m m m m m m . .';
}

Honestly, that’s not a whole lot better CSS-wise. But removing the conditional modifier classes from the HTML template is a nice win if you ask me.

It’s easy to think of micro design decisions for :has()like a card when it has an image in it — but I think it’ll be really useful for these macro layout changes too.

Better specificity management

If you read my previous article, you’ll know I’m a stickler for specificity. If, like me, you don’t want your specificity scores blowing out when adding :has() and :not() throughout your styles, be sure to use :where().

That’s because the specificity of :has() is based on the most specific element in its argument list. So, if you have something like an ID in there (why, I’m not sure!), your selector is going to be tough to override in the cascade.

On the other hand, the specificity of :where() is always zero, never adding to the specificity score.

/* specificity score: 0,1,0.
  Same as a .standard-page--with-sidebar 
  modifier class
*/
.standard-page:where(:has(.sidebar)) {
  /* etc */
}

The future’s bright

These are just a few things I can’t wait to be able to use in production. The CSS-Tricks Almanac has a bunch of examples, too. What are you looking forward to doing with :has()? What sort of some real-world examples have you run into where :has() would have been the perfect solution?


More Real-World Uses for :has() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/more-real-world-uses-for-has/feed/ 5 https://www.youtube.com/embed/ljD_ENTYuB8 Practical Use Cases for :has() Pseudo-Class nonadult 376331
Solved With :has(): Vertical Spacing in Long-Form Text https://css-tricks.com/solved-with-has-vertical-spacing-in-long-form-text/ https://css-tricks.com/solved-with-has-vertical-spacing-in-long-form-text/#comments Wed, 18 Jan 2023 13:44:00 +0000 https://css-tricks.com/?p=376435 If you’ve ever worked on sites with lots of long-form text — especially CMS sites where people can enter screeds of text in a WYSIWYG editor — you’ve likely had to write CSS to manage the vertical spacing between different …


Solved With :has(): Vertical Spacing in Long-Form Text originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
If you’ve ever worked on sites with lots of long-form text — especially CMS sites where people can enter screeds of text in a WYSIWYG editor — you’ve likely had to write CSS to manage the vertical spacing between different typographic elements, like headings, paragraphs, lists and so on.

It’s surprisingly tricky to get this right. And it’s one reason why things like the Tailwind Typography plugin and Stack Overflow’s Prose exist — although these handle much more than just vertical spacing.

Firefox supports :has() behind the layout.css.has-selector.enabled flag in about:config at the time of writing.

What makes typographic vertical spacing complicated?

Surely it should just be as simple as saying that each element — p, h2, ul, etc. — has some amount of top and/or bottom margin… right? Sadly, this isn’t the case. Consider this desired behavior:

  • The first and last elements in a block of long-form text shouldn’t have any extra space above or below (respectively). This is so that other, non-typographic elements are still placed predictably around the long-form content.
  • Sections within the long-form content should have a nice big space between them. A “section” being a heading and all the following content that belongs to that heading. In practice, this means having a nice big space before a heading… but not if that heading is immediately preceded by another heading!
Example of a Heading 3 following a paragraph and another following a Heading 2.
We want to more space above the Heading 3 when it follows a typographic element, like a paragraph, but less space when it immediately follows another heading.

You need to look no further than right here at CSS-Tricks to see where this could come in handy. Here are a couple of screenshots of spacing I pulled from another article.

A Heading 2 element directly above a Heading 3.
The vertical spacing between Heading 2 and Heading 3
A Heading 3 element directly following a paragraph element.
The vertical space between Heading 3 and a paragraph

The traditional solution

The typical solution I’ve seen involves putting any long-form content in a wrapping div (or a semantic tag, if appropriate). My go-to class name has been .rich-text, which I think I use as a hangover from older versions of the Wagtail CMS, which would add this class automatically when rendering WYSIWYG content. Tailwind Typography uses a .prose class (plus some modifier classes).

Then we add CSS to select all typographic elements in that wrapper and add vertical margins. Noting, of course, the special behavior mentioned above to do with stacked headings and the first/last element.

The traditional solution sounds reasonable… what’s the problem? I think there are a few…

Rigid structure

Having to add a wrapper class like .rich-text in all the right places means baking in a specific structure to your HTML code. That’s sometimes necessary, but it feels like it shouldn’t have to be in this particular case. It can also be easy to forget to do this everywhere you need to, especially if you need to use it for a mix of CMS and hard-coded content.

The HTML structure gets even more rigid when you want to be able to trim the top and bottom margin off the first and last elements, respectively, because they need to be immediate children of the wrapper element, e.g., .rich-text > *:first-child. That > is important — after all, we don’t want to accidentally select the first list item in each ul or ol with this selector.

Mixing margin properties

In the pre-:has() world, we haven’t had a way to select an element based on what follows it. Therefore, the traditional approach to spacing typographic elements involves using a mix of both margin-top and margin-bottom:

  1. We start by setting our default spacing to elements with margin-bottom.
  2. Next, we space out our “sections” using margin-top — i.e. very big space above each heading
  3. Then we override those big margin-tops when a heading is followed immediately by another heading using the adjacent sibling selector (e.g. h2 + h3).

Now, I don’t know about you, but I’ve always felt it’s better to use a single margin direction when spacing things out, generally favoring margin-bottom (that’s assuming the CSS gap property isn’t feasible, which it is not in this case). Whether this is a big deal, or even true, I’ll let you decide. But personally, I’d rather be setting margin-bottom for spacing long-form content.

Collapsing margins

Because of collapsing margins, this mix of top and bottom margins isn’t a big problem per se. Only the larger of two stacked margins will take effect, not the sum of both margins. But… well… I don’t really like collapsing margins.

Collapsing margins are yet one more thing to be aware of. It might be confusing for junior devs who aren’t up to speed with that CSS quirk. The spacing will totally change (i.e. stop collapsing) if you were to change the wrapper to a flex layout with flex-direction: column for instance, which is something that wouldn’t happen if you set your vertical margins in a single direction.

I more-or-less know how collapsing margins work, and I know that they’re there by design. I also know they’ve made my life easier on occasion. But they’ve also made it harder other times. I just think they’re kinda weird, and I’d generally rather avoid relying on them.

The :has() solution

And here is my attempt at solving these issues with :has().

To recap the improvements this aims to make:

  • No wrapper class is required.
  • We’re working with a consistent margin direction.
  • Collapsing margins are avoided (which may or may not be an improvement, depending on your stance).
  • There’s no setting styles and then immediately overriding them.

Notes and caveats on the :has() solution

  • Always check browser support. At time of writing, Firefox only supports :has() behind an experimental flag.
  • My solution doesn’t include all possible typographic elements. For instance, there’s no <blockquote> in my demo. The selector list is easy enough to extend though.
  • My solution also doesn’t handle non-typographic elements that may be present in your particular long-form text blocks, e.g. <img>. That’s because for the sites I work on, we tend to lock down the WYSIWYG as much as possible to core text nodes, like headings, paragraphs, and lists. Anything else — e.g. quotes, images, tables, etc. — is a separate CMS component block, and those blocks themselves are spaced apart from each other when rendered on a page. But again, the selector list can be extended.
  • I’ve only included h1 for the sake of completeness. I usually wouldn’t allow a CMS user to add an h1 via WYSIWYG, as the page title would be baked into the page template somewhere rather than entered in the CMS page editor.
  • I’m not catering for a heading followed immediately by the same level heading (h2 + h2). This would mean that the first heading wouldn’t “own” any content, which seems like a misuse of headings (and, correct me if I’m wrong, but it might violate WCAG 1.3.1 Info and Relationships). I’m also not catering for skipped heading levels, which are invalid.
  • I am in no way knocking the existing approaches I mentioned. If and when I build another Tailwind site I’ll use the excellent Typography plugin, no question!
  • I’m not a designer. I came up with these spacing values by eyeballing it. You probably could (and should) use better values.

Specificity and project structure

I was going to write a whole big thing here about how the traditional method and the new :has() way of doing it might fit into the ITCSS methodology… But now that we have :where() (the zero-specificity selector) you can pretty much choose your preferred level of specificity for any selector now.

That said, the fact that we’re no longer dealing with a wrapper — .prose, .rich-text, etc. — to me makes it feel like this should live in the “elements” layer, i.e. before you start dealing with class-level specificity. I’ve used :where() in my examples to keep specificity consistent. All the selectors in both of my examples have a specificity score of 0,0,1 (except for the bare-bones reset).

Wrapping up

So there you have it, a bleeding-edge solution to a very boring problem! This newer approach is still not what I’d call “simple” CSS — as I said at the beginning, it’s a more complex topic than it might seem at first. But aside from having a few slightly complex selectors, I think the new approach makes more sense overall, and the less rigid HTML structure seems very appealing.

If you end up using this, or something like it, I’d love to know how it works out for you. And if you can think of ways to improve it, I’d love to hear those too!


Solved With :has(): Vertical Spacing in Long-Form Text originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/solved-with-has-vertical-spacing-in-long-form-text/feed/ 10 376435
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