Ancrez l’annotation à une plage DOM Range, puis positionnez son libellé à l’aide des rectangles des fragments rendus par la plage. Ce guide présente une petite démo dans un navigateur de bureau sur Mac, qui maintient une note attachée à une expression traversant des éléments en ligne et répartie sur plusieurs lignes.
La tâche est précise : attacher une annotation visible à une expression dans la mise en page ordinaire d’une page Web sans reconstruire son texte. Vous allez identifier l’expression, choisir un fragment pour le libellé et refaire les mesures après les changements de mise en page. MDN décrit Range.getClientRects() comme l’API du navigateur permettant de récupérer les rectangles occupés par une plage. Range: getClientRects() method.
Pourquoi un libellé s’éloigne-t-il d’un texte réparti sur plusieurs lignes ?
Une expression a deux représentations pertinentes : sa position dans le document et sa géométrie actuelle à l’écran. WHATWG définit une plage par un point de début et un point de fin, chacun composé d’un nœud et d’un décalage. Ces extrémités peuvent se trouver dans des nœuds de texte différents, ce qui permet à une même plage de traverser un élément de mise en évidence existant. DOM Standard.
Un rectangle englobant unique fait perdre des détails. Imaginez une expression qui commence au milieu d’une ligne et se termine près du début de la suivante. Son rectangle englobant inclut les espaces vides à côté des deux lignes. Un libellé centré sur ce rectangle peut donc sembler détaché des mots. Floating UI signale explicitement ce problème pour les liens sur plusieurs lignes et les sélections de plage. inline.
Les rectangles individuels doivent aussi être interprétés. Le W3C précise qu’une plage peut renvoyer les rectangles de bordure des éléments sélectionnés ainsi que les rectangles du texte, dans l’ordre du contenu. Les éléments imbriqués peuvent donc produire des rectangles qui se chevauchent ; leur nombre ne permet pas de déduire de façon fiable le nombre de lignes. Les limites du texte reposent sur les métriques des polices, et non sur le contour exact des glyphes peints. CSSOM View Module.
Quel fragment doit porter l’annotation ?
Choisissez une règle de placement avant d’écrire le code de positionnement. L’aide à la décision ci-dessous distingue le texte annoté du point où son libellé apparaît.
| Comportement souhaité | Géométrie de référence | Règle de placement |
|---|---|---|
| Une note qui suit une expression | La plage entière | L’attacher sous son dernier fragment utilisable dans une mise en page horizontale simple, de gauche à droite |
| Un libellé près d’une interaction avec le pointeur | Les rectangles des fragments de la plage | Choisir le fragment situé aux coordonnées client de l’interaction |
| Un libellé récapitulatif centré | Le rectangle englobant | Accepter que le libellé puisse se trouver au-dessus d’un espace vide |
| Un arrière-plan qui marque toute l’expression | La plage entière | Utiliser une mise en évidence du texte indépendamment du placement du libellé |
La démo utilise la première règle. Elle fixe délibérément le mode d’écriture et le sens du texte afin que le dernier rectangle soit un repère de fin utile. Considérez cela comme une condition de l’exemple. Le W3C précise que ces rectangles suivent l’ordre du contenu ; il ne garantit pas un ordre universel du coin supérieur gauche au coin inférieur droit. CSSOM View Module.
Pour un placement basé sur le pointeur avec Floating UI, son intergiciel inline() accepte les coordonnées x et y relatives à la fenêtre d’affichage. La documentation limite ce choix basé sur les coordonnées aux rectangles disjoints ; il ne faut donc pas le présenter comme un sélecteur de caractères sans restriction. inline.
Créer une démo complète de texte sur plusieurs lignes sur Mac
Utilisez une version récente de Firefox pour ordinateur et ouvrez un onglet vierge à l’adresse about:blank. Appuyez sur Command + Option + K pour ouvrir la console Web de la page. Mozilla documente ce raccourci et confirme que la console exécute du JavaScript dans le contexte de la page. Web Console.
Exécutez le programme complet ci-dessous dans cette console. Il crée tout le contenu d’exemple, les styles et les commandes ; aucun projet, import ou bibliothèque externe n’est nécessaire. L’expression synthétique traverse un élément de mise en évidence existant. L’annotation est un libellé d’affichage seul ; elle ne comporte aucune commande d’édition.
(() => {
window.fragmentDemo?.destroy();
const host = document.createElement('section');
host.style.cssText = 'margin:48px 24px;padding-bottom:900px';
const p = document.createElement('p');
p.dir = 'ltr';
p.style.cssText = 'font:20px/1.6 Georgia,serif;width:24rem;max-width:80vw;writing-mode:horizontal-tb';
const a = document.createTextNode('Sample A: amber tiles follow the curved path ');
const em = document.createElement('em');
em.textContent = 'past the small blue circle and ';
const c = document.createTextNode('toward the final silver square. End.');
p.append(a, em, c);
const button = document.createElement('button');
button.textContent = 'Change text width';
const status = document.createElement('p');
host.append(button, p, status);
document.body.append(host);
const range = document.createRange();
range.setStart(a, 'Sample A: '.length);
range.setEnd(c, c.data.indexOf(' End.'));
const note = document.createElement('div');
note.textContent = 'Note A: review this phrase';
note.style.cssText = 'position:fixed;z-index:1000;background:#fff2ad;color:#222;padding:6px 10px;font:14px/1.4 system-ui;pointer-events:none';
document.body.append(note);
const style = document.createElement('style');
style.textContent = '::highlight(fragment-demo) {background-color:#dbeafe;}';
document.head.append(style);
const canHighlight = typeof Highlight !== 'undefined' && !!window.CSS?.highlights;
if (canHighlight) CSS.highlights.set('fragment-demo', new Highlight(range));
status.textContent = canHighlight ? 'The blue phrase owns Note A.' : 'Text highlighting unavailable; Note A still uses the range.';
function update() {
if (!host.isConnected) return;
const rects = Array.from(range.getClientRects()).filter(r => r.width > 0 && r.height > 0);
const r = rects.at(-1);
note.hidden = !r || range.collapsed;
if (note.hidden) return;
const maxLeft = Math.max(8, innerWidth - note.offsetWidth - 8);
note.style.left = Math.max(8, Math.min(r.left, maxLeft)) + 'px';
note.style.top = (r.bottom + 8) + 'px';
}
let narrow = false;
button.onclick = () => {
narrow = !narrow;
p.style.width = narrow ? '16rem' : '24rem';
update();
};
const observer = new ResizeObserver(update);
observer.observe(p);
window.addEventListener('resize', update);
window.addEventListener('scroll', update, true);
document.fonts.ready.then(update);
update();
window.fragmentDemo = {destroy() {
observer.disconnect();
window.removeEventListener('resize', update);
window.removeEventListener('scroll', update, true);
if (canHighlight) CSS.highlights.delete('fragment-demo');
host.remove(); note.remove(); style.remove();
}};
})();
Les extrémités excluent le préfixe Sample A: et le suffixe End.. Cliquez sur Change text width pour alterner entre deux largeurs de paragraphe. La même plage reste la cible de l’annotation tandis que le libellé est repositionné sous son dernier fragment utilisable. Faites défiler la page et redimensionnez la fenêtre pour observer le comportement. Supprimez la démo avec window.fragmentDemo.destroy().
Le programme a passé une vérification de syntaxe JavaScript, et les décalages de l’exemple ainsi que le calcul de position des fragments ont passé des vérifications déterministes. Le rendu dans un navigateur n’a pas été exécuté ici ; les étapes ci-dessus permettent de vérifier visuellement le résultat.
Comment la démo préserve-t-elle le flux du texte et sa composition ?
La plage mesure le texte existant. Après avoir construit l’exemple, le programme laisse en place les nœuds de texte de l’expression et l’élément de mise en évidence. Il ajoute le libellé séparément et peut mettre l’expression en évidence à l’aide de l’API CSS Custom Highlight, qui, selon MDN, fonctionne sans modifier la structure DOM sous-jacente. CSS custom highlight API.
Cette distinction est importante pour adapter l’approche. Ajouter du remplissage, des bordures ou des marges en ligne entre des unités de caractères peut créer des limites de composition. Le W3C indique également que les vertical-align non initiaux et les limites d’isolation bidirectionnelle interrompent la composition. Évitez d’insérer des éléments distincts autour de chaque caractère uniquement pour obtenir leurs coordonnées ; mesurez le résultat existant du navigateur. CSS Text Module Level 3, Shaping Across Element Boundaries.
Pour le texte réel d’une application, déduisez les extrémités de la plage des limites de texte voulues. Les décalages du texte DOM comptent des unités de code ; ils ne correspondent pas au nombre de glyphes visibles. Alignez les extrémités sur des caractères complets comme prévu, sans supposer que chaque caractère affiché occupe un seul décalage. DOM Standard.
La mise en évidence est facultative. MDN indique que Highlight est disponible de base depuis 2025 et précise que les navigateurs plus anciens peuvent ne pas le prendre en charge. La vérification de fonctionnalité de la démo maintient le placement de l’annotation lorsque la mise en évidence est indisponible et affiche un message explicatif. Highlight.
Que faut-il mettre à jour lorsque la page se redispose ?
La démo refait les mesures lorsque le paragraphe est redimensionné, lorsque la fenêtre change de taille et lors du défilement. ResizeObserver détecte les changements de dimensions d’un élément, y compris ceux qui ne sont pas liés au redimensionnement de la fenêtre du navigateur. Ici, son rappel ne déplace que le libellé séparé, ce qui évite une boucle de rétroaction qui modifierait sans cesse la taille du paragraphe mesuré. ResizeObserver.
Le chargement des polices nécessite une mise à jour distincte. Le programme effectue les mesures immédiatement, puis les répète lorsque document.fonts.ready est résolu. MDN indique que cette promesse est résolue une fois le chargement des polices du document et les opérations de mise en page associées terminés. Les changements ultérieurs de police ou de contenu déclenchés par l’application doivent demander explicitement une nouvelle mesure. FontFaceSet: ready property.
Conservez un seul système de coordonnées. La démo utilise un libellé en position fixe sur une page vierge et applique directement les coordonnées client. Ajouter les décalages de défilement du document les convertirait en coordonnées du document. MDN explique que les rectangles client sont relatifs à la fenêtre d’affichage et que l’ajout des décalages de défilement sert à obtenir des coordonnées relatives au document. Element: getBoundingClientRect() method.
Ce petit exemple limite le placement horizontal, mais ne gère ni l’évitement des collisions verticales, ni le masquage hors écran, ni les conteneurs arbitrairement transformés. Il ne restaure pas non plus une annotation après un rechargement ou un remplacement complet du texte. Dans une application, conservez un moyen d’identifier le passage visé, reconstruisez sa plage après le rendu, puis refaites les mesures.
Quand utiliser Floating UI ?
Utilisez Floating UI lorsque le libellé doit tenir compte du découpage et bénéficier d’un cycle de mise à jour géré. Son interface documentée de référence virtuelle prend en charge une plage au moyen de deux méthodes : getBoundingClientRect(), qui renvoie le rectangle englobant de la plage, et getClientRects(), qui renvoie ses rectangles de fragments. Fournissez contextElement avec un véritable élément contenant le texte afin que le découpage et la détection des mises à jour disposent d’un contexte d’élément. Virtual Elements.
Transmettez cette référence virtuelle à computePosition() et placez inline() près du début du tableau des intergiciels, avant flip(). Faites correspondre le positionnement CSS de l’élément flottant à la stratégie choisie. La documentation décrit flip() pour changer de côté lorsque l’espace manque et shift() pour déplacer un libellé dans les limites de son découpage. inline, computePosition.
Ne démarrez autoUpdate() que lorsque l’annotation est montée, et appelez la fonction de nettoyage renvoyée lorsque vous supprimez le libellé. Ses mécanismes de mise à jour documentés couvrent le défilement des ancêtres, le redimensionnement et les décalages de mise en page. Déclenchez tout de même une mise à jour après un changement de l’application qui modifie les fragments du texte choisi sans modifier les dimensions du conteneur observé. autoUpdate.
Diagnostiquer une annotation qui semble toujours mal placée
Effectuez ces vérifications dans l’ordre afin qu’un problème de géométrie ne masque pas un problème de ciblage du texte.
- Confirmez l’expression. Inspectez
range.toString()et vérifiez les deux extrémités avant d’ajuster les coordonnées. - Inspectez les fragments. Affichez
Array.from(range.getClientRects())dans la console. Plusieurs entrées qui se chevauchent peuvent provenir de contenu en ligne imbriqué ; n’interprétez pas leur nombre comme un nombre de lignes. - Vérifiez la règle de placement. Un rectangle englobant, le dernier fragment et le fragment sélectionné par le pointeur répondent à des comportements d’annotation différents.
- Vérifiez le système de coordonnées. Pour la démo sur page vierge, gardez le libellé fixe en coordonnées client.
- Répétez après les changements de mise en page. Modifiez la largeur, faites défiler, redimensionnez et laissez les polices finir de charger. Reconstruisez la plage si l’application remplace ses nœuds de texte.
- Vérifiez les hypothèses de mise en page. Pour l’écriture verticale, les directions mixtes, les colonnes ou les conteneurs transformés, définissez et testez une règle de sélection des fragments adaptée avant de réutiliser le raccourci du dernier fragment de la démo.
Une annotation fiable commence par une plage de texte précise et une règle de placement explicite. Laissez le navigateur gérer le retour à la ligne et la composition, puis refaites les mesures de ses fragments rendus lorsque la mise en page concernée change. La note reste ainsi liée à l’expression tandis que les mots s’écoulent naturellement.
Sources
- DOM Standard — WHATWG
- CSSOM View Module — W3C
- CSS Text Module Level 3 — W3C
- Range: getClientRects() method — MDN
- Element: getBoundingClientRect() method — MDN
- CSS custom highlight API — MDN
- Highlight — MDN
- ResizeObserver — MDN
- FontFaceSet: ready property — MDN
- Web Console — Mozilla
- inline — Floating UI
- Virtual Elements — Floating UI
- computePosition — Floating UI
- autoUpdate — Floating UI