bcarlin.net/content/blog/008-embracing-modern-css/index.en.md

543 lines
14 KiB
Markdown
Raw Normal View History

---
# vim: spell spelllang=en
title: 'Embracing Modern CSS'
slug: '8-embracing-modern-css'
date: '2025-07-06T14:29:26+02:00'
draft: false
categories:
- dev
tags:
- CSS
summary: |
Where I write about my discoveries of recent CSS I was unaware of.
description: |
The article explores recent developments in CSS, such as nested rules, CSS
variables, and classless CSS, which allow for cleaner and more efficient
stylesheet writing. It also mentions future features like CSS mixins, custom
CSS properties, and CSS scopes, which promise to further enhance the way user
interfaces are styled.
---
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.
[Plain Vanilla]: https://plainvanillaweb.com/pages/styling.html
## 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,
```scss
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
header {
nav {
ul {
list-style: none;
li {
text-align: center;
}
}
}
}
```
makes more sense to me than
```css
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:
```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.
[SASS]: https://sass-lang.com
[CSS Nesting Module]: https://drafts.csswg.org/css-nesting/
[Baseline Widely Available]: https://webstatus.dev/features/Nesting
[90% of users]: https://caniuse.com/css-nesting
[CSS file]: https://bcarlin.net/static/css/bcarlin.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 :
```scss
$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:
```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);
}
}
```
[CSS Custom Properties for Cascading Variables Module Level 1]: https://www.w3.org/TR/css-variables/
## 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:
```html
<!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]:
```html
<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">&times;</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]:
```HTML
<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]:
```html
<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).
[TailwindCSS]: https://tailwindcss.com
[CSS Selectors Level 4]: https://drafts.csswg.org/selectors-4/
[BootstrapCSS]: https://getbootstrap.com
[PicoCSS]: https://picocss.com
[Bootstrap Modal Component]: https://getbootstrap.com/docs/4.3/components/modal/#live-demo
[Modal component from Tailwind Plus]: https://tailwindcss.com/plus/ui-blocks/application-ui/overlays/modal-dialogs
[PicoCSS Modal component]: https://picocss.com/docs/modal
## `@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:
```scss
@use 'reset';
@use 'typography';
@use 'layout';
@use 'content';
```
With the [CSS Cascading and Inheritance Level 5] module, CSS has that natively:
```css
@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:
```css
@import url("light.css") only screen and (prefers-color-scheme: light);
@import url('dark.css') only screen and (prefers-color-scheme: dark);
```
[CSS Cascading and Inheritance Level 5]: https://drafts.csswg.org/css-cascade-5/
## 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]:
```scss
@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:
```css
:root {
--sidebar-float: left;
}
[dir=rtl] {
--sidebar-float: right;
}
.sidebar {
float: var(--sidebar-float);
}
```
[SASS Mixin Guide]: https://sass-lang.com/documentation/at-rules/mixin/#arguments
[CSS Functions and Mixins Module]: https://www.w3.org/TR/2025/WD-css-mixins-1-20250515/
### 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:
```css {linenos=true}
@property --my-color {
syntax: "<color>";
inherits: false;
initial-value: black;
}
.primary {
--my-color: red;
}
.secondary {
--my-color: 10px;
}
button {
background-color: var(--my-color);
color: white;
}
```
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 Properties and Values API Level 1]: https://drafts.css-houdini.org/css-properties-values-api
[Stylelint]: https://stylelint.io
[ESLint]: https://eslint.org
### 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:
```css
.card { /*...*/ }
.card article { /*...*/ }
.card article header { /*...*/ }
.card article footer { /*...*/ }
.card article footer button { /*...*/ }
```
With nested CSS modules, this can be simplified to:
```css
.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:
```css
@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.
[CSS Cascading and Inheritance Level 6]: https://drafts.csswg.org/css-cascade-6/
[example on MDN]: https://developer.mozilla.org/en-US/docs/Web/CSS/@scope#how_scope_conflicts_are_resolved