New CSS that can actually be used in 2024 | Thomasorus
Logical properties, container queries, :has
, :is
, :where
, min()
, max()
, clamp()
, nesting, cascade layers, subgrid, and more.
Logical properties, container queries, :has
, :is
, :where
, min()
, max()
, clamp()
, nesting, cascade layers, subgrid, and more.
Picture me holding Trys back and telling him, “Leave it alone, mate, it’s not worth it!”
I love it when I come across some bit of CSS I’ve never heard of before.
Take this article on the text-emphasis
property.
“The what property?”, I hear you ask. That was my reaction too. But look, it’s totally a thing.
Or take this article by David Bushell called CSS Button Styles You Might Not Know.
Sure enough, halfway through the article David starts talking about styling the button in an input type="file”
using the ::file-selector-button
pseudo-element:
All modern browsers support it. I had no idea myself until recently.
Then I remembered that I’ve got a file upload input in the form I use for posting my notes here on adactio.com (in case I want to add a photo). I immediately opened up my style sheet, eager to use this new-to-me bit of CSS.
I found the bit where I style buttons and this is the selector I saw:
button,
input[type="submit"],
::file-selector-button
Huh. I guess I did know about that pseudo-element after all. Clearly the knowledge exited my brain shortly afterwards.
There’s that tautological cryptic saying, “You don’t know what you don’t know.” But I don’t even know what I do know!
When I wrote about my time in Amsterdam last week, I mentioned the task that the students were given:
They’re given a PDF inheritance-tax form and told to convert it for the web.
Rich had a question about that:
I’m curious to know if they had the opportunity to optimise the user experience of the form for an online environment, eg. splitting it up into a sequence of questions, using progressive disclosure, branching based on inputs, etc?
The answer is yes, very much so. Progressive disclosure was a very clear opportunity for enhancement.
You know the kind of paper form where it says “If you answered no to this, then skip ahead to that”? On the web, we can do the skipping automatically. Or to put it another way, we can display a section of the form only when the user has ticked the appropriate box.
This is a classic example of progressive disclosure:
information is revealed when it becomes relevant to the current task.
But what should the mechanism be?
This is an interaction design pattern so JavaScript seems the best choice. JavaScript is for behaviour.
On the other hand, you can do this in CSS using the :checked
pseudo-class. And the principle of least power suggests using the least powerful language suitable for a given task.
I’m torn on this. I’m not sure if there’s a correct answer. I’d probably lean towards JavaScript just because it’s then possible to dynamically update ARIA attributes like aria-expanded
—very handy in combination with aria-controls
. But using CSS also seems perfectly reasonable to me.
It was interesting to see which students went down the JavaScript route and which ones used CSS.
It used to be that using the :checked
pseudo-class involved an adjacent sibling selector, like this:
input.disclosure-switch:checked ~ .disclosure-content {
display: block;
}
That meant your markup had to follow a specific pattern where the elements needed to be siblings:
<div class="disclosure-container">
<input type="checkbox" class="disclosure-switch">
<div class="disclosure-content">
...
</div>
</div>
But none of the students were doing that. They were all using :has()
. That meant that their selector could be much more robust. Even if the nesting of their markup changes, the CSS will still work. Something like this:
.disclosure-container:has(.disclosure-switch:checked) .disclosure-content
That will target the .disclosure-content
element anywhere inside the same .disclosure-container
that has the .disclosure-switch
. Much better! (Ignore these class names by the way—I’m just making them up to illustrate the idea.)
But just about every student ended up with something like this in their style sheets:
.disclosure-content {
display: none;
}
.disclosure-container:has(.disclosure-switch:checked) .disclosure-content {
display: block;
}
That gets my spidey-senses tingling. It doesn’t smell right to me. Here’s why…
The simpler selector is doing the more destructive action: hiding content. There’s a reliance on the more complex selector to display content.
If a browser understands the first ruleset but not the second, that content will be hidden by default.
I know that :has()
is very well supported now, but this still makes me nervous. I feel that the more risky action (hiding content) should belong to the more complex selector.
Thanks to the :not()
selector, you can reverse the logic of the progressive disclosure:
.disclosure-content {
display: block;
}
.disclosure-container:not(:has(.disclosure-switch:checked)) .disclosure-content {
display: none;
}
Now if a browser understands the first ruleset, but not the second, it’s not so bad. The content remains visible.
When I was explaining this way of thinking to the students, I used an analogy.
Suppose you’re building a physical product that uses electricity. What should happen if there’s a power cut? Like, if you’ve got a building with electric doors, what should happen when the power is cut off? Should the doors be locked by default? Or is it safer to default to unlocked doors?
It’s a bit of a tortured analogy, but it’s one I’ve used in the past when talking about JavaScript on the web. I like to think about JavaScript as being like electricity…
Take an existing product, like say, a toothbrush. Now imagine what you can do when you turbo-charge it with electricity: an electric toothbrush!
But also consider what happens when the electricity fails. Instead of the product becoming useless you want it to revert back to being a regular old toothbrush.
That’s the same mindset I’m encouraging for the progressive disclosure pattern. Make sure that the default state is safe. Then enhance.
This isn’t just a great explanation of :has()
, it’s an excellent way of understanding selectors in general. I love how the examples are interactive!
Heydon does a very good job of explaining why throwing away the power of selectors makes no sense.
Utility-first detractors complain a lot about how verbose this is and, consequently, how ugly. And it is indeed. But you’d forgive it that if it actually solved a problem, which it doesn’t. It is unequivocally an inferior way of making things which are alike look alike, as you should. It is and can only be useful for reproducing inconsistent design, wherein all those repeated values would instead differ.
He’s also right on the nose in explaining why something as awful at Tailwind could get so popular:
But CSS isn’t new, it’s only good. And in this backwards, bullshit-optimized economy of garbage and nonsense, good isn’t bad enough.
As well as a very welcome announcement, Jen has a really good question for you about nesting in CSS.
If you have an opinion on the answer, please chime in.
A collection of stylesheets that don’t use class selectors. Think of them as alternatives to default user-agent stylesheets.
A terrific tour of just some of the fantastic ways you can use :has()
in CSS.
The section on using it with sibling selectors blew my mind:
How often have you wanted to adjust the margins on a headline based on the element following it? Now it’s easy. This code allows us to select any h2 with a p immediately after it.
h2:has(+ p) { margin-bottom: 0; }
Amazing.
I feel like I’m starting to understand how the CSS :where
pseudo-class works and why it’s useful. The cogs are slowly turning in my brain.
This detailed proposal from Miriam for scoping CSS is well worth reading—it makes a lot of sense to me.
This is supposed to be a defence of utility classes …but it’s actually a great explanation of why classes in general are a great mechanism for styling.
I don’t think anyone has ever seriously suggested using inline styles—the actual disagreement is about how ludicrously rigid and wasteful the class names dictated by something like Tailwind are. When people criticise those classes they aren’t advocating for inline styles—they’re advocating for better class names and making more use of the power of the class selector in CSS, not less.
Anyway, if you removed every instance of the word “utility” from this article, it would still work.
This would be such a great addition to CSS—a parent/ancestor selector!
With the combined might of :has()
, :not()
, nth-child()
, and calc()
, CSS has become a powerful language for specifying rules to account for all kinds of situations.
Oh, this is smart! You can’t target pseudo-elements in JavaScript, but you can use custom properties as a proxy instead.
If we, as a community, start to appreciate the complexity of writing CSS, perhaps we can ask for help instead of blaming the language when we’re confused or stuck. We might also stop looking down on CSS specialists.
A handy tool for getting an overview of your site’s CSS:
CSS Stats provides analytics and visualizations for your stylesheets. This information can be used to improve consistency in your design, track performance of your app, and diagnose complex areas before it snowballs out of control.
I wrote a little something recently about using ARIA attributes as selectors in CSS. For me, one of the advantages is that because ARIA attributes are generally added via JavaScript, the corresponding CSS rules won’t kick in if something goes wrong with the JavaScript:
Generally, ARIA attributes—like
aria-hidden
—are added by JavaScript at runtime (rather than being hard-coded in the HTML).
But there’s one instance where I actually put the ARIA attribute directly in the HTML that gets sent from the server: aria-live
.
If you’re not familiar with it, aria-live
is extremely useful if you’ve got any dynamic updates on your page—via Ajax, for example. Let’s say you’ve got a bit of your site where filtered results will show up. Slap an aria-live
attribute on there with a value of “polite”:
<div aria-live="polite">
...dynamic content gets inserted here
</div>
You could instead provide a value of “assertive”, but you almost certainly don’t want to do that—it can be quite rude.
Anyway, on the face it, this looks like exactly the kind of ARIA attribute that should be added with JavaScript. After all, if there’s no JavaScript, there’ll be no dynamic updates.
But I picked up a handy lesson from Ire’s excellent post on using aria-live
:
Assistive technology will initially scan the document for instances of the aria-live attribute and keep track of elements that include it. This means that, if we want to notify users of a change within an element, we need to include the attribute in the original markup.
Good to know!
Sara tweeted something recently that resonated with me:
Also, Pro Tip: Using ARIA attributes as CSS hooks ensures your component will only look (and/or function) properly if said attributes are used in the HTML, which, in turn, ensures that they will always be added (otherwise, the component will obv. be broken)
Yes! I didn’t mention it when I wrote about accessible interactions but this is my preferred way of hooking up CSS and JavaScript interactions. Here’s old Codepen where you can see it in action:
[aria-hidden='true'] {
display: none;
}
In order for the functionality to work for everyone—screen reader users or not—I have to make sure that I’m toggling the value of aria-hidden
in my JavaScript.
There’s another advantage to this technique. Generally, ARIA attributes—like aria-hidden
—are added by JavaScript at runtime (rather than being hard-coded in the HTML). If something goes wrong with the JavaScript, the aria-hidden
value isn’t set to “true”, which means that the CSS never kicks in. So the default state is for content to be displayed. There’s no assumption that the JavaScript has to work in order for the CSS to make sense.
It’s almost as though accessibility and progressive enhancement are connected somehow…
An excellent and clear explanation of specificity in CSS.
I can see this coming in very handy at Codebar—pop any CSS selector in here and get a plain English explanation of what it’s doing.