Infobulles sans bibliothèques : positionnement, flèche et modèles compatibles ARIA

Cet article vous a aidé ?

Les infobulles, c’est l’équivalent UI du « juste une toute petite modification ». On ajoute une icône point d’interrogation, on colle du texte au survol, on publie, puis la production nous apprend ce qu’on a oublié : conteneurs défilants, overflow qui coupe, tapotements mobiles, enfer des z-index, et utilisateurs clavier qui ne peuvent pas survoler.

Voici l’approche pragmatique : construire des infobulles sans bibliothèque, mais avec le sérieux d’une bibliothèque. Si votre infobulle survit à un en-tête sticky, à un parent transformé, à une modale, à un zoom 200 % et à un lecteur d’écran, elle survivra à votre prochaine refonte.

Ce qu’est une infobulle (et ce qu’elle n’est pas)

Une infobulle est une information supplémentaire liée à un contrôle ou à un élément inline. Elle doit clarifier, pas porter une signification critique. Si masquer l’infobulle rend l’interface inutilisable, vous n’avez pas construit une infobulle — vous avez construit une étiquette de formulaire cassée.

Les infobulles ne sont pas non plus des popovers, des dialogues ou des notifications toast :

  • Infobulle : petite, éphémère, ancrée à une cible, généralement sans contenu interactif.
  • Popover : ancré comme une infobulle mais pouvant contenir de l’UI interactive (liens, boutons, champs). Nécessite une gestion clavier plus riche.
  • Dialogue : modal ou non modal, prend le focus, a souvent un backdrop et doit être fermable au clavier.

Décidez cela tôt, car c’est déterminant : rôles ARIA, comportement du focus, fermeture et si vous pouvez utiliser des déclencheurs uniquement au survol.

Opinion : Si le contenu contient un lien, une case à cocher ou un bouton « Copier », arrêtez d’appeler ça une infobulle. Vous êtes en train de créer un popover ; traitez-le comme tel.

Faits et historique à connaître

  • Fait 1 : L’attribut HTML title était la « infobulle gratuite » d’origine, mais il est incohérent selon les navigateurs, difficile à styliser et peu fiable pour l’accessibilité.
  • Fait 2 : Les infobulles sont l’un des premiers motifs UI hérités des GUI de bureau (pensez aux bulles d’aide Windows classiques), puis transplantées maladroitement sur le web.
  • Fait 3 : Les premières infobulles web utilisaient souvent onmouseover et document.write. Nous avons survécu à cette époque. À peine.
  • Fait 4 : Le « problème du z-index » est plus ancien que beaucoup de frameworks front-end ; les contextes d’empilement ont causé des soucis longtemps avant que « composant » ne devienne un intitulé de poste.
  • Fait 5 : Les recommandations ARIA pour les infobulles ont mûri plus tard que celles pour les dialogues et les boutons, ce qui explique les motifs discordants que l’on voit encore en production.
  • Fait 6 : Les interfaces tactiles ont contraint les infobulles à évoluer : le survol n’existe pas sur la plupart des téléphones, donc votre « astuce au survol » devient un choix de design.
  • Fait 7 : La tendance vers le pattern « portal vers body » vient de vrais échecs de layout : overflow qui coupe, transforms et conteneurs de scroll imbriqués rendaient les infobulles inline fragiles.
  • Fait 8 : Les navigateurs modernes ont introduit des primitives utiles (comme ResizeObserver), mais ils ont aussi apporté de nouvelles mines (comme contain et des transforms omniprésents).

Exigences non négociables pour des infobulles en production

Avant d’écrire du code, notez ce que signifie « fonctionner ». Voici le niveau que j’applique en production :

1) Doit être découvrable sans survol

Les utilisateurs clavier doivent déclencher l’infobulle au focus. Cela signifie généralement que le déclencheur est focusable (<button>, <a>, ou tabindex="0" en dernier recours).

2) Ne doit pas voler le focus

Les infobulles sont non interactives. Le focus reste sur le déclencheur. Si l’infobulle nécessite une interaction, faites-en un popover avec un plan de gestion du focus.

3) Ne doit pas provoquer de décalages de mise en page

Pas de déplacement de contenu. Les infobulles doivent être des overlays positionnés. Le CLS n’est pas réservé aux pages marketing ; les tableaux de bord internes sont aussi jugés — par des humains en colère.

4) Doit survivre au scroll, au zoom et au redimensionnement

Pas « à peu près ». Pas « la plupart du temps ». Les utilisateurs défilent en survolant, les trackpads existent et le zoom est courant en entreprise.

5) Ne doit pas être coupée

Les conteneurs avec overflow:hidden, les ancêtres transformés et les volets défilants sont l’endroit où les infobulles naïves meurent.

6) Ne doit pas bloquer l’élément que vous essayez d’utiliser

Les infobulles qui couvrent leur déclencheur créent des boucles de flicker et de la rage utilisateur. Il faut un offset sensible, une gestion des pointer-events et une fermeture retardée.

7) Doit être testable

Si votre infobulle ne peut pas être sélectionnée de manière fiable dans les tests, elle sera « corrigée » par quelqu’un avec un marteau. Fournissez des attributs stables (comme data-tooltip-id) et un comportement déterministe.

Blague #1 : Une infobulle qui bloque le bouton, c’est comme un panneau de sécurité soudé sur la sortie de secours. Techniquement présent, pratiquement cruel.

Architecture : inline vs portal, et pourquoi « position: absolute » n’est pas suffisant

Il y a deux architectures sensées quand vous refusez les bibliothèques :

A) Infobulle inline (même sous-arbre DOM)

Vous rendez l’infobulle proche du déclencheur et la positionnez avec du CSS (position: absolute) à l’intérieur d’un conteneur positionné relativement.

Avantages : simple, moins d’opérations DOM, CSS prévisible si le conteneur est stable.

Inconvénients : coupée par overflow: hidden/auto ; bizarreries avec les contextes d’empilement ; les transforms imbriqués peuvent faire des choses surprenantes.

B) Infobulle portalisée (rendue sous document.body)

Vous rendez l’infobulle dans une couche d’overlay de haut niveau (souvent un <div id="overlays"> dédié) et la positionnez avec des coordonnées du viewport. C’est ce que font les bibliothèques matures parce que les navigateurs et le layout ne sont pas sentimentaux.

Avantages : évite la coupe par l’overflow local ; gestion du z-index plus simple ; placement cohérent dans les modales et tiroirs.

Inconvénients : vous devez calculer les coordonnées ; suivre le scroll/redimensionnement ; gérer les containing blocks et le visual viewport sur mobile.

Décision : Si vous avez des panneaux défilants ou des modales (et vous en avez), par défaut utilisez un portal. Les infobulles inline conviennent aux pages statiques, docs et petits outils internes où le risque de coupe est faible.

Contextes d’empilement : pourquoi votre z-index « ne fait rien »

Les infobulles échouent silencieusement quand elles apparaissent derrière quelque chose. Coupables typiques :

  • Un parent avec transform, filter, opacity < 1, contain ou isolation créant un nouveau contexte d’empilement.
  • Éléments positionnés avec leurs propres couches z-index.
  • En-têtes fixed et éléments sticky avec un z-index élevé.

Les infobulles portalisées évitent la plupart de ces problèmes en vivant dans une couche supérieure connue. Mais même là, votre couche d’overlay doit elle-même être au-dessus des autres éléments UI.

Algorithme de positionnement : mesurer, placer, retourner, décaler, contraindre

Voici le modèle mental fiable : chaque placement d’infobulle est une négociation entre le rectangle du déclencheur, la taille de l’infobulle, le viewport et votre côté préféré.

L’algorithme viable minimum

  1. Mesurer le déclencheur : targetRect = target.getBoundingClientRect().
  2. Mesurer l’infobulle : la rendre hors écran ou cachée, puis lire tooltipRect.
  3. Placer sur le côté préféré avec un offset (par ex. 8px).
  4. Retourner (flip) si elle déborde (par ex. top ne rentre pas, placer bottom).
  5. Décaler (shift) le long de l’axe secondaire pour la garder dans le viewport (par ex. pousser à gauche/droite).
  6. Contraindre (clamp) les coordonnées finales pour qu’elle reste visible, avec un peu de padding depuis les bords.

C’est tout. Le reste, ce sont des corrections de bugs déguisées.

Considérations sur le viewport : layout viewport vs visual viewport

Les navigateurs mobiles compliquent la notion de « viewport ». Quand le clavier à l’écran est ouvert ou que la page est zoomée, le visual viewport peut différer du layout viewport. Si vous ne vous basez que sur innerWidth/innerHeight, vos infobulles peuvent flotter dans le mauvais univers.

Si vous souhaitez être sérieux : utilisez window.visualViewport quand disponible (et prévoyez un fallback). Tenez compte de visualViewport.offsetLeft/offsetTop lors du calcul des coordonnées.

Conteneurs de scroll : le bug classique

getBoundingClientRect() renvoie des coordonnées relatives au viewport, ce qui est bien. Mais si vous rendez l’infobulle à l’intérieur d’un conteneur scrollé (architecture inline), vous devrez traduire depuis l’espace de coordonnées du viewport vers l’espace du conteneur. C’est là que commencent beaucoup d’histoires « ça marche chez moi ».

Une fonction de positionnement propre et sans dépendance (pseudo-code)

Pas une bibliothèque complète. Juste assez de structure pour ne pas regretter vos choix :

  • Entrées : rect du target, taille de l’infobulle, préférence de placement, taille du viewport, padding, offset.
  • Sortie : x, y, placement utilisé, offset de la flèche.

Implémentez-la comme une fonction pure. Puis testez-la unitaires avec des rectangles. Vous ne pouvez pas tester l’intégration pour vous sortir de la géométrie.

Gérer les transforms et le positionnement fixed

Si vous portalisez vers body, position: fixed sur l’infobulle est souvent le plus simple : vos x/y sont des coordonnées du viewport. Si vous utilisez position: absolute, vous devrez ajouter les offsets de scroll (window.scrollX, window.scrollY) et gérer le scrolling du document séparément. Le fixed gagne en simplicité.

Le placement de la flèche dépend de la position finale après décalage

La flèche n’est pas une simple décoration ; c’est un indice directionnel. Si vous décalez l’infobulle à gauche pour éviter une coupe, la flèche doit aussi se déplacer. Sinon elle pointe dans le vide, ce qui est subtil mais nuit à l’UX.

Flèches : triangles, carrés pivotés, SVG, et « ne pas tromper l’utilisateur »

Les flèches paraissent faciles jusqu’à la mise en production. Voici les implémentations courantes :

Option 1 : triangle CSS par bordure

Le classique : un élément de taille zéro avec des bordures, où une bordure a une couleur et les autres sont transparentes.

Avantages : fonctionne partout, peu de DOM.

Inconvénients : difficile d’ajouter des ombres proprement ; l’anti-aliasing peut donner un rendu irrégulier ; mise à l’échelle et theming pénibles.

Option 2 : carré pivoté (« losange ») avec transform: rotate(45deg)

C’est mon défaut : un petit carré pivoté, positionné de sorte qu’il chevauche à moitié la boîte de l’infobulle.

Avantages : les ombres fonctionnent ; coins arrondis possibles ; theming plus simple.

Inconvénients : nécessite un chevauchement soigné pour éviter les jointures ; les transforms peuvent créer des contextes d’empilement (oui, encore).

Option 3 : SVG inline

Avantages : net, facile à ombrer/filtrer, alignement précis, peut correspondre aux design systems.

Inconvénients : un peu plus de balisage ; si vous êtes négligent, vous finirez avec des problèmes de pointer-events.

Comment garder la flèche honnête

Calculez un offset de flèche le long de l’axe secondaire de l’infobulle :

  • Si l’infobulle est placée au-dessus/en dessous de la cible : la flèche se déplace à gauche/droite à l’intérieur de l’infobulle.
  • Si elle est placée à gauche/droite : la flèche se déplace haut/bas.

Contraignez aussi cet offset, pour que la flèche ne finisse pas sur le coin arrondi de l’infobulle. Coins arrondis + flèche sur le rayon = glitch visuel.

Modèles compatibles ARIA : clavier, focus et lecteurs d’écran

L’accessibilité n’est pas une leçon morale ; c’est une stratégie de prévention des pannes. Le moment où quelqu’un ne comprend pas une icône d’erreur parce que l’infobulle ne s’annonce pas, vous recevez des tickets, des escalades et un « fix rapide » qui casse autre chose.

Utilisez le bon rôle et la bonne relation

Pour une vraie infobulle (contenu non interactif), utilisez :

  • role="tooltip" sur l’élément infobulle.
  • aria-describedby="tooltip-id" sur l’élément déclencheur.

Cela indique aux technologies d’assistance : « cette chose décrit cette autre chose ». Simple. Ne faites pas de zèle.

Quand utiliser aria-label vs aria-describedby

  • Utilisez aria-label quand le déclencheur n’a pas d’étiquette visible (par ex. bouton icône seule). Cette étiquette doit être courte et stable.
  • Utilisez aria-describedby pour du texte supplémentaire, des explications d’erreur, des indices clavier ou des clarifications que vous ne voulez pas dans le nom principal.

N’utilisez pas les infobulles pour combler des étiquettes manquantes. Les lecteurs d’écran ne devraient pas devoir « découvrir » le nom d’un contrôle.

Afficher au focus, masquer au blur (avec délai)

Règles de base :

  • Au focus : ouvrir l’infobulle.
  • Au blur : fermer l’infobulle.
  • Sur Escape : fermer l’infobulle (même si c’est « juste » une infobulle).

Ajoutez un petit délai de fermeture (100–200ms) pour éviter le flicker lorsque le pointeur passe entre le déclencheur et l’infobulle. Et oui, même pour des infobulles non interactives, les utilisateurs font cela. Les humains sont des chaos engineers.

N’emmenez pas les lecteurs d’écran dans un monde uniquement basé sur le survol

Le survol n’est pas une interaction universelle. Beaucoup d’utilisateurs n’utilisent pas de souris. De nombreux appareils n’ont pas de survol. Fournissez des déclencheurs focusables et assurez-vous que le contenu de l’infobulle est annoncé via la relation described-by.

Gardez le contenu des infobulles court et ennuyeux

Les infobulles doivent faire une ou deux phrases. Si vous avez besoin d’un paragraphe, vous écrivez de la documentation. Si vous avez besoin d’un tableau, vous construisez un popover. Si vous avez besoin d’un formulaire, vous construisez un dialogue. Les mots ont un sens ; votre UI aussi.

Idée paraphrasée (attribuée) : « L’espoir n’est pas une stratégie », souvent citée dans les cercles ops par des leaders orientés fiabilité comme Gene Kranz. Traitez l’accessibilité de la même manière : planifiez-la.

Modèle d’événements : survol, focus, tactile et fermeture

Les infobulles échouent dans les interstices entre les méthodes d’entrée. Votre travail est de rendre ces interstices ennuyeux.

Événements pointeur : utilisez-les, mais ne leur faites pas une confiance aveugle

pointerenter/pointerleave peuvent remplacer la logique séparée souris/tactile, mais vous devez encore décider quoi faire avec les pointeurs grossiers (tactile). Une infobulle qui s’ouvre au tap peut être acceptable si :

  • Le tap ouvre l’infobulle.
  • Un deuxième tap (ou un tap extérieur) la ferme.
  • L’infobulle ne bloque pas l’action suivante souhaitée.

Règles de fermeture qui n’énervent pas

  • Souris : fermer sur pointerleave avec un petit délai ; garder ouverte si le pointeur entre dans l’infobulle (optionnel).
  • Clavier : fermer au blur ou sur Escape.
  • Tactile : fermer au tap extérieur ; envisager la fermeture au scroll.

Jitter et flicker : le problème « j’ai bougé ma souris d’un pixel »

La boucle de flicker arrive souvent parce que l’infobulle chevauche le déclencheur. Quand elle apparaît, le déclencheur n’est plus survolé, donc elle disparaît, donc le déclencheur est de nouveau survolé, etc.

Corrections :

  • Décalez l’infobulle pour qu’elle ne chevauche pas.
  • Appliquez pointer-events: none sur l’infobulle si elle n’a pas besoin de persistance au survol.
  • Ou implémentez une infobulle hoverable avec une « frontière interactive » et des timers.

Performance et fiabilité : jitter, reflow et gestion du scroll

Les infobulles sont petites, mais elles peuvent déclencher des travaux coûteux au pire moment : scroll, resize et mouvement du pointeur. En d’autres termes : en continu.

Ne repositionnez pas à chaque mousemove

Positionnez à l’ouverture, puis repositionnez sur :

  • scroll (throttlé)
  • resize (throttlé)
  • redimensionnement de la cible (ResizeObserver)
  • changement de contenu de l’infobulle (ResizeObserver, ou re-mesure après render)

Si vous repositionnez au mousemove, vous créerez du micro-jank sur des machines d’entrée de gamme et sur des pages avec du layout lourd.

Throttlez avec requestAnimationFrame

Pour le scroll/resize, collectez les événements et effectuez une seule reposition par frame d’animation. Cela vous aligne sur la boucle de rendu du navigateur et évite des calculs de layout redondants.

Minimisez les reflows forcés

getBoundingClientRect() peut forcer le layout si vous lisez après avoir écrit des styles affectant le layout. Regroupez d’abord les lectures, puis les écritures. Une des façons les plus simples : calculez toute la géométrie, puis appliquez style.transform = translate3d(x,y,0) en une seule opération.

Privilégiez les transforms plutôt que top/left

Utilisez transform: translate3d() pour le positionnement, surtout quand l’infobulle anime. C’est généralement plus fluide et évite le layout. « Généralement » parce que le web est une démocratie d’exceptions.

Blague #2 : Si vous mesurez le layout dans une boucle serrée, Chrome vous montrera volontiers à quoi ressemble « eventual consistency » — en rendant finalement votre infobulle ailleurs.

Playbook de diagnostic rapide

Quand une infobulle est « cassée », les ingénieurs argumentent souvent sur du CSS pendant une heure. Ne le faites pas. Suivez le playbook. Trouvez le goulot rapidement.

Premier : identifiez la classe de panne

  • Invisible : ne se rend pas, opacity 0, display none, ou derrière une autre couche.
  • Mal positionnée : coordonnées erronées, mauvais espace de coordonnées, mauvais offsets de scroll.
  • Coupée : overflow hidden/auto, ou container qui coupe à cause du contexte d’empilement/containment.
  • Flickering : boucle d’événements (hover) ou thrash de reposition (scroll/resize).
  • Non annoncée : le lecteur d’écran ne la lit pas (câblage ARIA ou comportement de focus).

Second : vérifiez les hypothèses sur l’espace de coordonnées

  1. Utilisez-vous un portal ? Si oui, préférez position: fixed et des coordonnées du viewport.
  2. Si non portalisé : quel est l’offset parent ? Y a-t-il un ancêtre transformé ?
  3. Mélangez-vous pageX/pageY avec des rects basés sur le viewport ?

Troisième : confirmez l’empilement et la coupe

  1. L’infobulle est-elle dans un élément avec overflow: hidden ou overflow: auto ?
  2. Un ancêtre établit-il un contexte d’empilement (transform/opacity/filter/contain) ?
  3. La z-index de votre couche d’overlay est-elle réellement au-dessus de l’en-tête/la modale ?

Quatrième : confirmez le câblage a11y

  1. Le déclencheur a-t-il aria-describedby pointant vers un ID existant ?
  2. L’infobulle a-t-elle role="tooltip" ?
  3. L’infobulle s’ouvre-t-elle au focus et se ferme-t-elle au blur/Escape ?

Erreurs courantes : symptômes → cause racine → correction

1) L’infobulle apparaît en (0,0) ou en haut à gauche de la page

  • Symptôme : infobulle épinglée au coin, indépendamment du déclencheur.
  • Cause racine : mesure faite avant que l’élément soit dans le DOM, ou getBoundingClientRect() appelé sur une cible nulle/cachée ; parfois une référence obsolète.
  • Correction : rendre l’infobulle cachée (visibility: hidden) d’abord, puis mesurer au frame suivant ; protéger contre les refs manquantes ; logger les rects.

2) L’infobulle est correcte jusqu’à ce que vous fassiez défiler un conteneur

  • Symptôme : l’infobulle s’éloigne de la cible pendant le scroll.
  • Cause racine : vous n’écoutez que le scroll de la fenêtre, pas l’ancêtre scrollable ; ou vous utilisez un positionnement absolute avec de mauvais offsets de scroll.
  • Correction : portal + position fixed ; ou détecter et écouter le conteneur de scroll le plus proche ; repositionner sur les événements de scroll via rAF.

3) L’infobulle est coupée à l’intérieur d’une carte/modale

  • Symptôme : infobulle visiblement tronquée à la bordure du conteneur.
  • Cause racine : l’infobulle vit à l’intérieur d’un élément avec overflow: hidden/auto.
  • Correction : portal vers une couche d’overlay de haut niveau ; ou relaxer l’overflow (rarement acceptable) ; ou utiliser position: fixed avec empilement correct.

4) L’infobulle apparaît derrière l’en-tête

  • Symptôme : l’infobulle existe mais n’est pas visible au-dessus des éléments sticky.
  • Cause racine : couche d’overlay sous un contexte d’empilement avec z-index plus élevé ; ou l’infobulle est dans un contexte d’empilement inférieur dû à un ancêtre.
  • Correction : racine d’overlay centrale avec z-index élevé connu ; éviter les transforms sur les ancêtres d’overlay ; auditer les contextes d’empilement.

5) Flicker au survol

  • Symptôme : infobulle s’affiche/masque rapidement quand on déplace le pointeur près du déclencheur.
  • Cause racine : l’infobulle chevauche la cible et vole le hover ; fermeture immédiate au leave sans délai.
  • Correction : ajouter un offset ; appliquer pointer-events: none pour une infobulle non interactive ; ajouter un délai de fermeture ; envisager un « pont de survol » (padding invisible) si nécessaire.

6) Le lecteur d’écran n’annonce pas l’infobulle

  • Symptôme : infobulle visible visuellement, mais non lue lors du focus sur le déclencheur.
  • Cause racine : aria-describedby manquant ; ID d’infobulle non correspondent ; contenu de l’infobulle non présent dans le DOM jusqu’à après l’événement de focus et le lecteur n’effectue pas de nouvelle annonce.
  • Correction : garder l’élément infobulle dans le DOM (caché) pour que described-by pointe vers du contenu réel ; vérifier le câblage des ID ; ouvrir systématiquement au focus.

7) L’infobulle provoque du jank au scroll

  • Symptôme : le défilement devient saccadé quand l’infobulle est ouverte.
  • Cause racine : repositionnement à chaque événement de scroll avec layout forcé ; ombre ou filter coûteux ; trop d’observers.
  • Correction : throttler via rAF ; réduire le CSS coûteux ; éviter les effets filter ; repositionner uniquement quand l’infobulle est ouverte.

Tâches pratiques (avec commandes) : déboguer comme un SRE

Vous pouvez déboguer l’UI dans le navigateur, bien sûr. Mais quand le bug est « seulement en prod » et « seulement pour certains utilisateurs », il faut de l’observabilité système : quelle build, quelles en-têtes, quel CSP, quels assets, quelles erreurs. Ces tâches sont la mémoire musculaire ennuyeuse qui évite les suppositions héroïques.

Task 1: Confirm which HTML shipped (and if the tooltip markup exists)

cr0x@server:~$ curl -sS -D- https://app.example.internal/settings | sed -n '1,40p'
HTTP/2 200
date: Mon, 29 Dec 2025 10:11:12 GMT
content-type: text/html; charset=utf-8
content-security-policy: default-src 'self'; script-src 'self'
etag: W/"a1b2c3"
...

Ce que signifie la sortie : vous vérifiez le statut, les en-têtes (en particulier le CSP) et si vous recevez bien du HTML. Le CSP compte parce que les stratégies d’infobulle reposent souvent sur des styles ou scripts inline.

Décision : si le CSP bloque les styles/scripts inline et que votre infobulle les utilise, vous devez soit refactoriser vers des assets externes, soit mettre à jour le CSP en toute sécurité.

Task 2: Verify the tooltip JS bundle is present and cacheable

cr0x@server:~$ curl -sS -I https://app.example.internal/assets/tooltip.js
HTTP/2 200
content-type: application/javascript
cache-control: public, max-age=31536000, immutable
etag: "9f8e7d6c"

Ce que signifie la sortie : le bundle existe et est cacheable. Si vous voyez 404 ou des durées de cache courtes, vous avez trouvé un problème de déploiement ou d’invalidation de cache.

Décision : si le cache est mal configuré, corrigez le fingerprinting des assets ou la config CDN avant de toucher au code d’infobulle.

Task 3: Check whether errors spike when tooltips should render

cr0x@server:~$ kubectl -n web logs deploy/frontend --since=15m | grep -E "TypeError|ReferenceError|CSP|tooltip" | tail -n 20
TypeError: Cannot read properties of null (reading 'getBoundingClientRect')
CSP: Refused to apply inline style because it violates the following Content Security Policy directive...

Ce que signifie la sortie : une référence nulle suggère que l’élément déclencheur n’existe pas lors de la mesure (race), ou que le sélecteur est incorrect en prod. L’erreur CSP indique que des styles sont bloqués.

Décision : si vous voyez des violations CSP, stoppez. Corrigez la politique ou l’implémentation. Si vous voyez des rects nuls, ajoutez des garde-fous et différez la mesure.

Task 4: Confirm the rollout version (because “only prod” is often “only that canary”)

cr0x@server:~$ kubectl -n web describe deploy/frontend | sed -n '1,120p'
Name:                   frontend
Namespace:              web
Labels:                 app=frontend
Annotations:            deployment.kubernetes.io/revision: 42
Containers:
  frontend:
    Image:              registry.internal/frontend:sha-8c1d2a7
    Ports:              8080/TCP

Ce que signifie la sortie : vous vérifiez l’image en cours et la révision. Les bugs d’infobulle corrèlent souvent avec une release.

Décision : si le bug a commencé avec une révision, procédez à un bisect par rollback ou comparez les changements autour du code d’infobulle et des couches CSS.

Task 5: Check if a reverse proxy is stripping needed headers

cr0x@server:~$ curl -sS -I https://app.example.internal/ | grep -i -E "content-security-policy|x-frame-options|referrer-policy"
content-security-policy: default-src 'self'; script-src 'self'
referrer-policy: strict-origin-when-cross-origin

Ce que signifie la sortie : les en-têtes de sécurité peuvent affecter les stratégies d’infobulle (par ex. si vous comptiez sur des styles inline). Les politiques d’encadrement importent si votre app est embarquée.

Décision : si les en-têtes diffèrent entre environnements, alignez-les ; sinon vous continuerez à livrer des infobulles qui « fonctionnent en staging » seulement.

Task 6: Find which client-side route/version users are hitting via access logs

cr0x@server:~$ sudo tail -n 200 /var/log/nginx/access.log | grep -E "GET /assets/|GET /settings" | tail -n 20
10.1.2.3 - - "GET /settings HTTP/2.0" 200 42103 "-" "Mozilla/5.0 ..."
10.1.2.3 - - "GET /assets/tooltip.js HTTP/2.0" 200 18342 "-" "Mozilla/5.0 ..."

Ce que signifie la sortie : vous pouvez voir si l’asset d’infobulle est demandé et si c’est 200 ou 304. L’absence de requête signifie souvent que le HTML ne le référence pas ou qu’un client le bloque.

Décision : si vous ne voyez pas de requêtes d’asset, confirmez le bundling et l’inclusion dans le HTML. Si vous voyez 403/404, corrigez le routage/CDN.

Task 7: Verify gzip/brotli isn’t corrupting the JS payload (rare, but real)

cr0x@server:~$ curl -sS -H 'Accept-Encoding: gzip' --compressed https://app.example.internal/assets/tooltip.js | head -n 5
(function(){'use strict';
var Tooltip=function(){...}

Ce que signifie la sortie : vous confirmez que la réponse compressée se décompresse en texte JS valide.

Décision : si vous obtenez des données binaires ou un contenu tronqué, inspectez la configuration de compression du proxy.

Task 8: Measure client errors in real time via server logs (CSP reports)

cr0x@server:~$ kubectl -n web logs deploy/csp-report-collector --since=30m | tail -n 20
{"blocked-uri":"inline","violated-directive":"style-src-elem","document-uri":"https://app.example.internal/settings"}

Ce que signifie la sortie : des utilisateurs déclenchent des violations CSP qui peuvent empêcher le stylage ou la visibilité des infobulles.

Décision : retirez les styles/scripts inline de l’implémentation d’infobulle ou mettez à jour le CSP avec des nonces/hashes (avec précaution).

Task 9: Confirm the overlay root exists in the DOM template (SSR/templating failure mode)

cr0x@server:~$ curl -sS https://app.example.internal/ | grep -n 'id="overlays"' | head -n 5
118:    

Ce que signifie la sortie : si votre portal attend une racine d’overlay et qu’elle manque, les infobulles échoueront silencieusement ou s’attacheront mal au body.

Décision : si elle manque, corrigez le template de base et ajoutez un fallback runtime qui crée le nœud.

Task 10: Detect whether a CSS change introduced overflow: hidden on a key ancestor

cr0x@server:~$ git show HEAD~1:ui/styles/components/card.css | sed -n '1,120p'
.card {
  position: relative;
  overflow: hidden;
  border-radius: 12px;
}

Ce que signifie la sortie : un changement récent a ajouté du clipping d’overflow. C’est un des trois principaux tueurs d’infobulles.

Décision : soit portaler l’infobulle hors de la carte, soit retirer l’overflow hidden si ce n’est pas nécessaire (généralement nécessaire pour les coins arrondis).

Task 11: Reproduce the clipping locally with a deterministic environment

cr0x@server:~$ docker run --rm -p 8080:8080 registry.internal/frontend:sha-8c1d2a7
Listening on http://0.0.0.0:8080

Ce que signifie la sortie : vous lancez la même image qu’en prod. Plus d’excuses « ça marche sur mon laptop ».

Décision : si ça se reproduit, vous pouvez déboguer dans le navigateur en toute confiance. Sinon, la différence vient de la config, des en-têtes ou des couches upstream.

Task 12: Check whether user agents differ (touch vs hover behavior)

cr0x@server:~$ sudo awk -F"'" '{print $6}' /var/log/nginx/access.log | tail -n 200 | sort | uniq -c | sort -nr | head
  83 Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 ...
  52 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...

Ce que signifie la sortie : un pic de trafic iPhone peut révéler des échecs d’infobulle causés par des hypothèses de survol.

Décision : si les appareils tactiles dominent les rapports de bug, assurez-vous d’un comportement adapté au tap et ne comptez pas sur le survol.

Task 13: Identify whether the tooltip code is causing CPU spikes (server-side correlation)

cr0x@server:~$ kubectl -n web top pods | head
NAME                           CPU(cores)   MEMORY(bytes)
frontend-6c7b9c9f8d-2kq9m      120m         310Mi
frontend-6c7b9c9f8d-l8z2p      115m         305Mi

Ce que signifie la sortie : cela ne mesure pas le CPU client, mais peut révéler des corrélations (par ex. SSR ou génération HTML excessive).

Décision : si le CPU serveur a augmenté avec le déploiement de l’infobulle, examinez les boucles SSR, le logging ou les changements de template liés au rendu des infobulles.

Task 14: Confirm that source maps aren’t missing in production (debuggability task)

cr0x@server:~$ ls -lh /srv/app/assets | grep -E 'tooltip\.js(\.map)?' 
-rw-r--r-- 1 www-data www-data  18K Dec 29 09:58 tooltip.js
-rw-r--r-- 1 www-data www-data  61K Dec 29 09:58 tooltip.js.map

Ce que signifie la sortie : vous disposez des sourcemaps (même si restreintes). Déboguer la géométrie d’une infobulle sans sourcemaps, c’est de l’automutilation.

Décision : si les maps manquent, décidez si vous les déployez de manière sécurisée (restreinte) ou améliorez le logging serveur autour des décisions de placement.

Trois mini-histoires d’entreprise tirées des mines à infobulles

Mini-histoire 1 : L’incident causé par une mauvaise hypothèse

Une équipe a construit un composant d’infobulle propre pour un tableau de facturation. Ça fonctionnait en dev, en staging et dans une démo où tout le monde utilisait poliment une souris comme en 2008. L’hypothèse : « les infobulles sont de l’UI au survol. » Ils l’ont déployée.

En moins d’un jour, des tickets support sont arrivés : des utilisateurs ne comprenaient pas pourquoi leurs factures échouaient. L’icône d’erreur avait la seule explication, et cette explication vivait exclusivement dans l’infobulle. Beaucoup d’utilisateurs naviguaient au clavier à cause du RSI, et certains utilisaient un fort zoom où la précision du survol devient difficile.

La première « correction » fut d’ouvrir l’infobulle au clic. Cela l’a rendue relativement accessible, mais ça bloquait aussi l’icône d’erreur et les liens adjacents, créant une nouvelle classe de bug : les clics destinés à l’icône basculaient l’infobulle, tandis que les lecteurs d’écran avaient toujours des annonces incohérentes parce que le DOM de l’infobulle n’était créé qu’après interaction.

La réparation finale fut ennuyeuse et efficace : étiquettes d’erreur appropriées en ligne, infobulles dégradées en courtes clarifications, et aria-describedby câblé vers un élément d’infobulle toujours présent qui s’ouvre au focus. L’incident n’était pas sur la géométrie. C’était sur la sémantique. Le titre du postmortem aurait pu être : « Nous avons utilisé une infobulle comme étiquette. »

Mini-histoire 2 : L’optimisation qui a échoué

Une autre organisation a décidé que les infobulles étaient « coûteuses » parce que leur ouverture provoquait une mesure de layout. Quelqu’un a profile une machine lente et conclu : « Nous devrions pré-calculer les positions pour toutes les infobulles au chargement. » Ça semblait efficace. C’était aussi très faux, à la web.

Ils ont construit une passe préliminaire : itérer sur chaque déclencheur d’infobulle, mesurer les rects et stocker les coordonnées. Puis, au survol, rendre l’infobulle aux x/y stockés. La page avait des centaines de lignes, chacune avec plusieurs icônes. Le chargement s’est alourdi, mais restait acceptable en labo.

En production, les utilisateurs filtrèrent les tableaux, redimensionnèrent des colonnes et ouvrirent des panneaux latéraux. Les coordonnées stockées devinrent obsolètes. Les infobulles dérivaient, retournaient incorrectement et apparaissaient parfois hors écran. Pire : la passe préliminaire elle-même provoquait des reflows forcés, rendant la page lente avant même que l’utilisateur n’interagisse.

La correction fut l’inverse : calculer les positions seulement quand une infobulle est ouverte, et recalculer seulement sur les changements pertinents (scroll/resize/observers) throttlés aux frames d’animation. « Optimiser » fut remplacé par « faire moins, plus tard, correctement ». 

Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Une entreprise soucieuse de la sécurité a introduit un Content Security Policy plus strict. C’était une bonne décision. Cela a aussi cassé un nombre surprenant d’éléments UI qui dépendaient silencieusement de styles inline.

L’implémentation d’infobulle en faisait partie. Elle utilisait un truc rapide : set style="top: ...px; left: ...px" au runtime. Le CSP avait style-src 'self' sans autoriser les inline, donc les infobulles rendaient à leur position CSS par défaut — tout en haut à gauche de la couche d’overlay.

La plupart des équipes ont demandé des exceptions CSP. Une équipe ne l’a pas fait. Ils avaient un item dans la checklist pré-déploiement : « Exécuter CSP report-only en staging et revoir les violations. » Ils avaient construit un petit endpoint collecteur et l’examinaient réellement, parce qu’ils en avaient marre des vendredis surprises.

Ils ont migré le positionnement des infobulles vers des variables CSS mises à jour via une règle de feuille de style plutôt que des attributs inline, et ils ont veillé à ce que le CSS requis soit dans un asset versionné. Quand la politique stricte est passée en production, leurs infobulles ont continué de fonctionner. Le héros fut une checklist et une file de rapports. Glamour ? Non. Efficace ? Extrêmement.

Listes de contrôle / plan étape par étape

Étape par étape : construire une infobulle qui survit à la réalité

  1. Décidez la sémantique : infobulle vs popover. Si interactif, c’est un popover — ne faites pas semblant.
  2. Choisissez l’architecture : portal vers une racine d’overlay de haut niveau par défaut.
  3. Créez un câblage stable : générez un ID d’infobulle et mettez aria-describedby sur le déclencheur.
  4. Gardez le DOM de l’infobulle présent : cachez via visibility et opacity ; ne créez/pas détruisez à chaque survol si vous avez besoin d’un comportement SR cohérent.
  5. Positionnez avec fixed + transform : calculez x/y du viewport ; appliquez avec translate3d.
  6. Implémentez flip + shift : ne laissez pas les infobulles sortir de l’écran. Contraignez avec padding.
  7. La flèche suit le placement final : calculez l’offset de flèche après le shift ; contraignez loin des coins arrondis.
  8. Événements : ouvrir au hover et focus ; fermer au leave/blur/Escape ; fermer au pointerdown extérieur pour le tactile.
  9. Throttlez les mises à jour : rAF pour scroll/resize ; observers uniquement quand ouvert.
  10. Tests : ajoutez des sélecteurs déterministes et testez unitairement la fonction de géométrie avec des rectangles fixture.
  11. Robustesse : gérez la racine d’overlay manquante ; protégez les refs nulles ; traitez le CSP comme une contrainte de conception.

Checklist : portes d’entrée readiness production

  • Fonctionne en navigation clavier uniquement (ouverture au focus, Escape ferme).
  • Annoncé par les lecteurs d’écran (role + described-by + DOM stable).
  • Survit aux conteneurs de scroll imbriqués (repositionnement au scroll).
  • Non coupé par l’overflow (portal).
  • Pas de boucles de flicker (offset + stratégie pointer-events + délai).
  • Ne cause pas de jank de scroll notable (mesures throttlées, pas de reposition sur mousemove).
  • Stratégie de couche z-index définie (racine d’overlay au-dessus des headers/modales).
  • Compatible CSP (pas de styles inline si la politique l’interdit).

FAQ

1) Puis-je simplement utiliser l’attribut HTML title ?
Vous pouvez, mais vous perdrez le contrôle du style, obtiendrez des timings incohérents selon les navigateurs et souvent une accessibilité faible. À utiliser seulement en dernier recours.
2) Le contenu de l’infobulle doit-il être dans le DOM quand il est caché ?
Généralement oui. Cela rend aria-describedby stable et améliore la cohérence des lecteurs d’écran. Cachez avec visibility: hidden et opacity: 0, pas display: none, si vous devez qu’il soit référencé.
3) Quel rôle ARIA dois-je utiliser ?
role="tooltip" sur l’élément infobulle. Puis attachez-le au déclencheur avec aria-describedby. Gardez le contenu non interactif.
4) Les infobulles doivent-elles s’ouvrir au clic ?
Sur les appareils tactiles, le clic/tap peut être un déclencheur raisonnable. Sur desktop, préférez hover + focus. Si vous ouvrez au clic, définissez la fermeture et assurez-vous que cela ne bloque pas l’action suivante.
5) Pourquoi mon z-index d’infobulle ne fonctionne-t-il pas ?
Parce que le z-index ne concurrence que dans le même contexte d’empilement. Les transforms, l’opacité et certaines propriétés CSS créent de nouveaux contextes. Portalisez vers une racine d’overlay connue et gérez le z-index centralement.
6) Comment éviter la coupe dans les conteneurs overflow ?
Portalisez l’infobulle hors du conteneur (typiquement vers document.body ou une racine d’overlay) et positionnez avec des coordonnées du viewport en utilisant position: fixed.
7) À quelle fréquence dois-je recalculer la position d’une infobulle ?
À l’ouverture, puis quand quelque chose de pertinent change : scroll, resize, changement de taille du target ou du contenu de l’infobulle. Throttlez à une mise à jour par frame d’animation.
8) Puis-je animer les infobulles en toute sécurité ?
Oui : animez l’opacité et le transform (petit translate). Évitez d’animer top/left. Gardez les animations courtes et respectez la préférence reduced-motion si votre produit l’exige.
9) Une infobulle doit-elle être hoverable ?
Si elle n’est pas interactive, elle n’a pas besoin d’être hoverable. Utilisez pointer-events: none et évitez les boucles de flicker. Si vous avez besoin de persistance pour lire un texte plus long, ajoutez un délai de fermeture et autorisez le pointeur à entrer dans l’infobulle.
10) Quelle est la stratégie de placement robuste la plus simple ?
Côté préféré + flip si ça ne rentre pas + shift pour rester dans le viewport + clamp avec padding. Ne sautez pas le shift/clamp sauf si votre UI ne défile jamais et ne redimensionne jamais — ce qui est un conte de fées.

Conclusion : prochaines étapes fiables

Si vous voulez des infobulles sans bibliothèques, l’astuce n’est pas d’écrire du code brillant. C’est d’écrire du code qui suppose que votre layout sera hostile, que vos utilisateurs seront divers et que votre CSS sera « amélioré » par quelqu’un qui ne connaît pas votre composant.

Prochaines étapes qui rapportent vite :

  • Choisissez portal + position fixed comme architecture par défaut.
  • Implémentez la géométrie comme une fonction pure et testez-la unitairement avec des rectangles.
  • Câblez aria-describedby + role="tooltip" et ouvrez au focus, pas seulement au survol.
  • Ajoutez un repositionnement throttlé rAF pour scroll/resize et arrêtez de mesurer au mousemove.
  • Exécutez le playbook de diagnostic sur une page avec modales, en-têtes sticky et panneaux de scroll imbriqués — car c’est là que se cache la vérité.

Mentalité opérationnelle : Traitez les infobulles comme toute autre fonctionnalité de production. Elles ont des dépendances (CSP, couches z-index, conteneurs de scroll) et des modes de défaillance. Rendez-les observables et ennuyeuses.

← Précédent
Le démon Docker ne démarre pas : lisez d’abord ce journal (puis réparez-le)
Suivant →
Ubuntu 24.04 : Cron s’exécute manuellement mais pas selon le planning — pièges PATH/environnement et corrections

Laisser un commentaire