Notes de marge avec le positionnement par ancre CSS
Vincent Bernat
Je fais un usage intensif des notes de marge1 : elles gardent le contenu optionnel à côté du texte au lieu de renvoyer le lecteur en bas de page. Tufte CSS les affiche sans JavaScript, mais n’accepte qu’un seul paragraphe. Le positionnement par ancre CSS, désormais pris en charge par les navigateurs récents2, est une alternative élégante. Les notes peuvent contenir plusieurs blocs, toujours sans JavaScript, et se positionnent sous le paragraphe qui les appelle sur les fenêtres étroites et les navigateurs plus anciens.
En 2023, Eric Meyer a présenté cette technique dans « Nuclear Anchored Sidenotes ». Le principal avantage par rapport aux autres solutions est que les notes peuvent se trouver n’importe où dans le document HTML. Il est possible de les placer après le paragraphe qui les appelle, comme des éléments de bloc ordinaires, afin que les navigateurs en mode texte, les lecteurs d’écran, les agrégateurs de flux et le mode lecture les affichent correctement :

Lorsque la fenêtre est trop étroite ou que le navigateur ne prend pas en charge le positionnement par ancre, il est possible de leur appliquer un style qui permet au lecteur de les sauter ou d’y jeter un œil sans perdre sa place dans le texte :

Dès que la fenêtre est assez large, elles apparaissent dans la marge, à la même hauteur que l’appel de note correspondant, sauf si elles entrent en collision avec une note précédente, comme dans l’exemple ci-dessous3 :

Le principe de l’ancrage CSS est de positionner un élément par rapport à un autre élément, l’ancre. Pour les notes de marge, l’ancre est l’appel de note. J’utilise le balisage suivant, avec un attribut de données pour indiquer le nom de l’ancre :
<sup id="fnref:YYY" data-anchor="--lf-sn-YYY">
<a href="#sidenote-YYY">1</a>
</sup>
La note correspondante est un élément <aside> portant le même attribut de
données pour le nom de l’ancre. Elle est placée après le paragraphe contenant
l’appel de note :
<aside role="note" id="sidenote-YYY" data-anchor="--lf-sn-YYY">
<sup>1</sup>
<p>Un premier paragraphe.</p>
<p>Un second paragraphe.</p>
</aside>
Sur une fenêtre étroite ou lorsque le navigateur est trop ancien pour l’ancrage CSS, la note reçoit une couleur atténuée et reste sous son paragraphe :
aside[role="note"] {
margin-block: 1rlh;
color: #444;
}
Sur une fenêtre plus large et lorsque le navigateur est assez récent, la note est déplacée dans la marge de droite :
@supports (anchor-name: attr(data-anchor type(<custom-ident>))) {
@media (min-width: 72rem) {
main {
position: relative;
sup[data-anchor] {
anchor-name: attr(data-anchor type(<custom-ident>));
/* → anchor-name: --lf-sn-YYY */
}
aside[role="note"][data-anchor] {
anchor-name: --lf-sidenote;
position: absolute;
position-anchor: attr(data-anchor type(<custom-ident>));
/* → position-anchor: --lf-sn-YYY */
top: max(anchor(top), anchor(--lf-sidenote bottom, -1rlh) + 1rlh);
left: 100%;
margin: 0 2rem;
width: 18rem;
color: inherit;
}
}
}
}
attr() extrait le nom de l’ancre de l’appel de note depuis l’attribut
data-anchor. Elle renvoie une chaîne de caractères, sauf si une unité CSS ou
un type est précisé, comme ici : le navigateur analyse l’attribut de données
comme un identifiant personnalisé, que anchor-name valide
comme un identifiant à tirets, c’est-à-dire un identifiant
personnalisé commençant par deux tirets4.
La note elle-même est positionnée de manière absolue au-delà du bord droit du
bloc principal. Elle choisit l’appel de note correspondant comme ancre grâce à
position-anchor, dont la valeur provient de l’attribut data-anchor. Chaque
note est elle-même une ancre nommée --lf-sidenote. Elle sert à empêcher la
note suivante d’entrer en collision avec celle-ci.
La fonction CSS anchor() permet de positionner le bord
supérieur de la note par rapport à son ancre : anchor(top) aligne le bord
supérieur de la note avec le bord supérieur de l’appel de note. Elle accepte
aussi une autre ancre en paramètre : anchor(--lf-sidenote bottom) alignerait
le bord supérieur de la note avec le bord inférieur de l’ancre précédente la
plus proche nommée --lf-sidenote, c’est-à-dire la note
précédente5. Comme attr(), anchor() accepte une valeur de repli
en second paramètre et l’utilise lorsque l’ancre nommée n’existe pas.
La propriété top gère trois cas, illustrés dans le schéma suivant :
- Le bord supérieur de la première note s’aligne avec le bord supérieur de son
appel de note : en l’absence de note précédente,
anchor(--lf-sidenote bottom, -1rlh) + 1rlhvaut 0 etmax()renvoieanchor(top). - Lorsque l’appel de note d’une note ultérieure se trouve au-dessus du bas de
la note précédente, augmenté d’un espace vertical, la note passe sous
la précédente pour éviter une collision.
max()renvoieanchor(--lf-sidenote bottom) + 1rlh. - Sinon,
max()renvoieanchor(top)et le bord supérieur de la note s’aligne avec le bord supérieur de l’appel de note.
Jetez un œil à la feuille de style complète, qui adapte aussi l’appel de note à l’emplacement de la note : une flèche « ↓ » lorsque la note se trouve sous le paragraphe, une flèche « → » lorsqu’elle part dans la marge. « Sidenotes In Web Design », de Gwern, recense d’autres implémentations et leurs compromis.
Certains blogueurs se fixent pour objectif d’écrire un billet en 30
minutes. Je comptais publier trois articles liés au web ce week-end. À la place,
j’ai passé un temps fou sur n’importe quoi : une quinzaine de changements sur le
système de construction du site, une contribution pour mettre à jour la
coloration CSS des sélecteurs imbriqués dans Pygments et une
petite correction de l’article de MDN sur la fonction CSS anchor().
L’illustration SVG a demandé un peu moins d’une heure et l’article lui-même
quelques heures. La fonction attr() est arrivée après que je me suis demandé :
« n’y-a-t-il pas plus élégant que de placer le nom de l’ancre dans un style en
ligne ? ». Mais bon, je trouve que ça en vaut malgré tout la peine ! 🎨