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 :

Les notes de marge affichées dans Lynx apparaissent après le paragraphe qui
les appelle.
Rendu dans Lynx, un navigateur en mode texte

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 :

Les notes de marge affichées sur une fenêtre étroite apparaissent avec une
typographie distincte après le paragraphe qui les
appelle.
Rendu sous le paragraphe sur une fenêtre étroite

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 :

Les notes de marge affichées sur une fenêtre large apparaissent dans la marge.
Il y en a deux. La première est alignée verticalement avec l'appel de note
correspondant, tandis que la seconde est affichée plus bas, car elle entrerait
sinon en collision avec la première.
Rendu dans la marge sur une fenêtre large

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 :

Schéma de trois notes de marge ancrées à leurs appels de note. La première est
alignée avec le haut de son propre appel de note, car aucune note ne la précède.
La deuxième chevaucherait la première : elle prend donc le bas de la première
note comme ancre et se place une ligne en dessous. La troisième arrive assez bas
dans la page pour s'aligner de nouveau avec son propre appel de
note.
Les trois cas pour la position verticale d'une note
  1. 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) + 1rlh vaut 0 et max() renvoie anchor(top).
  2. 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() renvoie anchor(--lf-sidenote bottom) + 1rlh.
  3. Sinon, max() renvoie anchor(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 ! 🎨