Embracing Modern CSS
Contents:
I recently stumbled upon a note on a page of Plain Vanilla in which I learned that nested CSS is a thing. It’s a thing that’s been around for quite some time, but I did not know about it (I’m quite late in my RSS reader).
This allowed me to catch up on some of the recent evolutions of CSS.
Nested CSS
Nesting CSS rules is one of the main reasons I’ve been using SASS for 15 years.
I’ve always preferred to write nested rules to group together coherent units of CSS. For example,
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
header {
nav {
ul {
list-style: none;
li {
text-align: center;
}
}
}
}
makes more sense to me than
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
header nav ul {
list-style: none;
}
header nav ul li {
text-align: center;
}
I have a few reasons for this:
- It’s easier to read because selectors are sorter and the hierarchy is easier to grasp;
- I can move around a group of selector without forgetting a declaration;
- I can use my IDE code folding based on indent to close a group and navigate long CSS files.
Since the CSS Nesting Module is Baseline Widely Available and is supported by 90% of users, It can be used to write nested CSS. So now, this is a thing in CSS:
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
header {
nav {
ul {
list-style: none;
li {
text-align: center;
}
}
}
}
The CSS file of this site has been rewritten using nested CSS.
CSS Variables
To me, variables are essential to ensure a coherent user interface. They allow to reuse colors, sizes, spacing, and so on.
This is also a reason why I’ve been using SASS. It allowed me to write CSS with reusable variables :
$color-error: red;
$color-success: green;
label {
&.error {
color: $color-error;
}
&.success {
color: $color-success;
}
}
.notification {
&.error {
background-color: $color-error;
}
&.success {
background-color: $color-success;
}
}
I missed the CSS Custom Properties for Cascading Variables Module Level 1 module from 2017, which allowed me to write the same thing in pure CSS:
:root {
--color-error: red;
--color-success: green;
}
label {
&.error {
color: var(--color-error);
}
&.success {
color: var(--color-success);
}
}
.notification {
&.error {
background-color: var(--color-error);
}
&.success {
background-color: var(--color-success);
}
}
Classless CSS
Maybe I an going backwards here, given the popularity of utility-first CSS frameworks like TailwindCSS.
This one is not really a CSS feature per se, but it is a way to write CSS,
where semantically correct HTML is automatically styled correctly. To some
extent, it is, however, backed by some CSS Selectors Level 4 which are now
Widely implemented across browsers, such as :has
, :is
, :where
, :not
and
so on.
I used to use BootstrapCSS in my projects because it is complete and easy to use, but I never liked the way it imposed a heavy CSS Structure on my source. For this site, I was looking for something lighter and came across PicoCSS which styles 90% of my site without changing anything to my templates.
I already had a meaningful semantic HTML base structure:
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<header>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
</nav>
</header>
<main>
<article>
<header>
<h1>Page Title</h1>
</header>
<section>
<!-- ... -->
</section>
</article>
</main>
<footer>
<!-- ... -->
</footer>
</body>
</html>
And I really like the way it works: the content is styled based on its semantic markup, and not on a HTML imposed structure.
For example, here is Bootstrap Modal component:
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Modal Title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
Modal Body
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
Here is the Modal component from Tailwind Plus:
<div>
<button class="rounded-md bg-gray-950/5 px-2.5 py-1.5 text-sm font-semibold text-gray-900 hover:bg-gray-950/10">Open dialog</button>
<div class="relative z-10" aria-labelledby="dialog-title" role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-gray-500/75 transition-opacity" aria-hidden="true"></div>
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex size-12 shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:size-10">
<svg class="size-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-base font-semibold text-gray-900" id="dialog-title">Modal Title</h3>
<div class="mt-2">
Modal Body
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
<button type="button" class="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 sm:ml-3 sm:w-auto">Save changes</button>
<button type="button" class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-gray-300 ring-inset hover:bg-gray-50 sm:mt-0 sm:w-auto">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>
Compare those with PicoCSS Modal component:
<dialog open>
<article>
<header>
<button aria-label="Close" rel="prev"></button>
<p>
<strong>Modal Title</strong>
</p>
</header>
Modal Body
<footer>
<button class="secondary">Close</button>
<button>Save changes</button>
</footer>
</article>
</dialog>
It makes a huge difference in simplicity, readability and accessibility (note that the ARIA attributes are rendered useless because the semantic markup already carries that information).
@import
to split CSS files
One last thing I liked to use SASS for was the possibility to split CSS files into smaller ones to make them easier to grasp. For example:
@use 'reset';
@use 'typography';
@use 'layout';
@use 'content';
With the CSS Cascading and Inheritance Level 5 module, CSS has that natively:
@import url('reset.css');
@import url('typography.css');
@import url('layout.css');
@import url('content.css');
From my understanding, the @import
ed CSS files are downloaded in parallel,
which reduces the penalty of having several files to download.
CSS @import
rules even have the benefit of being conditional. For example:
@import url("light.css") only screen and (prefers-color-scheme: light);
@import url('dark.css') only screen and (prefers-color-scheme: dark);
Things I’m looking forward to
Those are some things I’m looking forward to using. I do not use them yet because of browser support or because I did not have a use for them yet. But I’m excited to try them out.
CSS Mixins
CSS Mixins are also a major feature of SASS, and foster a cleaner and more reusable CSS code.
CSS will have them with the CSS Functions and Mixins Module, which is still a draft where mixins are not specified yet.
In the meantime, here is an example from SASS Mixin Guide:
@mixin rtl($property, $ltr-value, $rtl-value) {
#{$property}: $ltr-value;
[dir=rtl] & {
#{$property}: $rtl-value;
}
}
.sidebar {
@include rtl(float, left, right);
}
Though in some cases, it can easily be replaced with CSS variables:
:root {
--sidebar-float: left;
}
[dir=rtl] {
--sidebar-float: right;
}
.sidebar {
float: var(--sidebar-float);
}
CSS Custom Properties
This one is a nice little feature from the CSS Properties and Values API Level 1 module which extends CSS variables nicely.
They allow to define the type, initial value and inheritance rule of a custom variables. For example:
1@property --my-color {
2 syntax: "<color>";
3 inherits: false;
4 initial-value: black;
5}
6
7.primary {
8 --my-color: red;
9}
10
11.secondary {
12 --my-color: 10px;
13}
14
15button {
16 background-color: var(--my-color);
17 color: white;
18}
Here, the definition of --my-color
on line 12 is not valid (it is a length and
not a color). As the property value is not inherited from a parent, the initial
value will be used: a <button class="secondary">
will have a black background.
However, as the property is defined to be a color, linters like Stylelint and ESLint will eventually be able to catch such errors, in addition to catching typos in values or in the property name.
CSS Scopes
This one is maybe the one I am expecting the most.
To style a UI component, it is often necessary to target a specific element of the component, and repeat selectors:
.card { /*...*/ }
.card article { /*...*/ }
.card article header { /*...*/ }
.card article footer { /*...*/ }
.card article footer button { /*...*/ }
With nested CSS modules, this can be simplified to:
.card {
/*...*/
article {
/*...*/
header {
/*...*/
}
footer {
/*...*/
button {
/*...*/
}
}
}
}
It can however have some edge cases and yield unexpected results (see the example on MDN).
Scopes are a new feature from the CSS Cascading and Inheritance Level 6 module. They are a more natural way of defining rules:
@scope (.card){
:scope {
/*...*/
}
article {
/*...*/
header {
/*...*/
}
footer {
/*...*/
button {
/*...*/
}
}
}
}
@scope
power comes from several fact:
- It follows a proximity rules: an element is styled with the nearest scope rules;
- It adds no specificity to the selector, which means that it can be overridden more easily;
- it is more expressive.