L'attribut universel popover permet de désigner un élément qui sera affiché par-dessus le contenu actuel.
Dans son utilisation la plus simple, ça donne quelque chose comme ça.
<button popovertarget="popover" type="button">Afficher "popover"</button>
<div popover id="popover">Salut ! Je suis un popover</div>
Il suffit de cliquer n'importe où ou de faire Touche clavier Échappement (Esc) Esc pour refermer.
Simple, mais très laid !
Par contre, on peut aussi se servir de ses propriétés pour en faire un petit tooltip sympathique, sans écrire 12 000 lignes de code.
Et automatiser (oui, c'est une manie…) la détection et l'affichage. Exemple :
On commence par poser un attribut data-popover sur n'importe quel élément du DOM. L'attribut contiendra le texte à afficher.
<span data-popover="Oh ! La belle bulle dites-donc !">Je vais ouvrir une belle bulle</span>
Je vais ouvrir une belle bulle.
Et c'est tout.
L'API propose 3 méthodes :
showPopover()hidePopover()togglePopover()
Dans le cas du tooltip, on ne va utiliser que les deux premières.
Le script va récupérer tous les éléments qui possèdent l'attribut data-popover et utiliser ces méthodes pour afficher et masquer l'infobulle.
A noter que le script pose un attribut tabindex si l'élément n'est pas interactif nativement.
Le gâteau sous la cerise est qu'il n'y pas besoin de gérer la fermeture avec Touche clavier Échappement (Esc) Esc puisque c'est intrinsèque à l'objet popover.
Cerise sur la cerise, il n'y a pas non plus besoin de gérer le z-index. C'est Toto Matique !
Le script
Le fonctionnement est basé sur la présence d'un attribut data-popover="contenu à afficher" sur n'importe quel élément du DOM.
Un attribut data-popover-css est disponible pour ajouter des classes CSS existantes. Par exemple : data-popover-css="font-size-larger font-weight-300".
On peut laisser l'attribut data-popover vide. Dans ce cas, si l'élément possède un attribut aria-label, c'est ce même contenu qui sera utilisé pour l'affichage, tout en gardant la conformité pour l'accessibilité.
Enfin, si data-popover est vide et qu'il n'y a pas de aria-label, c'est le contenu texte de l'élément qui sera utilisé.
Et en toute fin, si on n'a rien de tout ça… et bien il ne se passera rien.
Taille brute : 10,75 kilo-octets (~3,67 kilo-octets gzip)
(() => {
'use strict';
// Vérification que l'API Popover est disponible dans le navigateur
if (HTMLDialogElement.prototype.showPopover) {
/**
* Utilitaire pour définir plusieurs attributs sur un élément
* @param {Element} el - L'élément DOM cible
* @param {Object} attrs - Objet contenant les paires clé/valeur des attributs
*/
const setAttributes = (el, attrs) =>
Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
/**
* Liste des sélecteurs d'éléments considérés comme focusables
* Utilisé pour déterminer si un élément peut naturellement recevoir le focus
*/
const selectors = window.focusableSelectors ?? `a[href], area[href], summary, select:not([disabled]), textarea:not([disabled]),`
+ `button:not([disabled]), input:not([type="hidden"]):not([disabled]), [role="button"], [tabindex]:not([tabindex="-1"]),`
+ `[contenteditable], object, iframe, embed, audio[controls], video[controls], details:not([disabled]),`
+ `label[for], fieldset:not([disabled]), progress, [role="menuitem"], [role="checkbox"], [role="option"], dialog`;
/**
* Création du tooltip unique réutilisé pour tous les popovers
* Un seul tooltip dans le DOM pour optimiser les performances
*/
const tooltip = document.createElement('div');
setAttributes(tooltip, {
'popover': '', // Active l'API Popover native
'role': 'tooltip', // Rôle ARIA pour l'accessibilité
'aria-live': 'polite', // Annonce les changements aux lecteurs d'écran
'id': 'custom-popover-tooltip' // ID unique pour les références aria-describedby
});
document.body.appendChild(tooltip);
/**
* Variable pour stocker les classes CSS temporaires ajoutées au tooltip
* Réinitialisée à chaque fermeture pour éviter les conflits de style
*/
let currentCssClasses = [];
/**
* Variable pour stocker le timer du délai d'apparition
*/
let showTimer = null;
/**
* Affiche le tooltip pour un élément donné avec un délai de 500ms
* @param {Element} el - L'élément déclencheur du tooltip
*/
function showTooltip(el) {
// Annuler tout timer précédent pour éviter les conflits
if (showTimer) {
clearTimeout(showTimer);
}
// Délai avant d'afficher
showTimer = setTimeout(() => {
// Récupération du texte à afficher par ordre de priorité
let text = el.getAttribute('data-popover');
// Si pas de contenu dans data-popover, on essaie aria-label
if (el.getAttribute('aria-label') && !text) {
text = el.getAttribute('aria-label');
}
// En dernier recours, on utilise le contenu textuel de l'élément
if (!text && el.textContent.trim()) {
text = el.textContent.trim();
}
// Si aucun texte n'est disponible, on abandonne
if (!text) return;
/**
* Gestion spécifique pour les liens s'ouvrant dans une nouvelle fenêtre
* Ajoute automatiquement une mention "(Nouvelle fenêtre)" pour l'accessibilité
*/
const isLink = el.tagName && el.tagName.toLowerCase() === 'a';
if (isLink && el.getAttribute('target') === '_blank') {
// Utilise la traduction i18n si disponible, sinon fallback en français
const suffix =
(typeof i18n === 'object' && i18n['new_window'])
? i18n['new_window']
: 'Nouvelle fenêtre';
// Évite d'ajouter plusieurs fois le suffixe si le code est ré-exécuté
if (!text.includes(suffix)) {
text += `<br>${suffix}`;
}
}
/**
* Supprime l'attribut title natif pour éviter la double-affichage
* Le tooltip personnalisé remplace le comportement par défaut du navigateur
*/
el.getAttribute('title') && el.removeAttribute('title');
/**
* Gestion des classes CSS personnalisées via data-popover-css
* Permet de styliser le tooltip différemment selon le contexte
*/
const cssClasses = el.getAttribute('data-popover-css');
if (cssClasses) {
// Découpe la chaîne par espaces et filtre les valeurs vides
currentCssClasses = cssClasses.split(' ').filter(cls => cls.trim());
// Ajoute toutes les classes au tooltip en une seule opération
tooltip.classList.add(...currentCssClasses);
}
// Injection du contenu HTML dans le tooltip
tooltip.innerHTML = text;
// Association ARIA pour l'accessibilité (lecteurs d'écran)
el.setAttribute('aria-describedby', 'custom-popover-tooltip');
// Affichage du tooltip via l'API Popover
tooltip.showPopover();
/**
* Positionnement dynamique du tooltip après son affichage
* requestAnimationFrame assure que le rendu est terminé avant de calculer les positions
*/
requestAnimationFrame(() => {
// Récupération des dimensions et positions de l'élément déclencheur
const rect = el.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
// Calculer gap en convertissant la valeur en pixels.
// Utilise 24px comme fallback si la variable est invalide ou absente.
const gap = (parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--gutter')) * parseFloat(getComputedStyle(document.documentElement).fontSize)) / 2 || 24;
/**
* Calcul de la position horizontale (centrage par rapport à l'élément)
*/
let left = rect.left + window.scrollX + (rect.width / 2) - (tooltipRect.width / 2);
/**
* Gestion du débordement à droite
*/
if (left + tooltipRect.width > window.innerWidth - gap) {
left = window.innerWidth - tooltipRect.width - gap;
}
/**
* Gestion du débordement à gauche
*/
if (left < gap) {
left = gap;
}
/**
* Calcul de la position verticale
* Par défaut, le tooltip apparaît sous l'élément avec un espacement de 8px
*/
let top = rect.bottom + window.scrollY + 8;
/**
* Si le tooltip dépasse en bas, on le place au-dessus de l'élément
* Assure que le tooltip reste toujours visible dans le viewport
*/
if (top + tooltipRect.height + 16 > window.innerHeight + window.scrollY) {
top = rect.top + window.scrollY - tooltipRect.height - 8;
}
// Application des positions calculées
tooltip.style.top = `${top}px`;
tooltip.style.left = `${left}px`;
// le fondu est géré par la CSS et @starting-style et :popover-open
});
}, 500);
}
/**
* Masque le tooltip et nettoie les attributs/classes ajoutés
* @param {Element} el - L'élément qui avait déclenché le tooltip
*/
function hideTooltip(el) {
if (showTimer) {
clearTimeout(showTimer);
showTimer = null;
}
// Suppression de l'association ARIA
el.removeAttribute('aria-describedby');
/**
* Nettoyage des classes CSS personnalisées
* Important pour éviter que les styles ne persistent pour le prochain tooltip
*/
if (currentCssClasses.length > 0) {
tooltip.classList.remove(...currentCssClasses);
currentCssClasses = []; // Réinitialisation du tableau
}
// Attendre la fin de la transition avant de masquer
setTimeout(() => {
tooltip.hidePopover();
}, 300); // 100ms de plus que la transition CSS
}
/**
* Fonction principale pour initialiser les tooltips sur une zone du DOM
* Peut être appelée manuellement pour des éléments ajoutés dynamiquement
*
* @param {Element} root - Élément racine où chercher les data-popover (par défaut document.body)
*/
function attachPopovers(root = document.body) {
// Vérification de sécurité si l'élément root n'existe pas
if (!root) return;
// Sélection de tous les éléments ayant l'attribut data-popover
const elements = root.querySelectorAll('[data-popover]');
// Détection du support tactile pour adapter le comportement
const hasTouchEvents = "ontouchstart" in window;
elements.forEach(el => {
/**
* Protection contre la double-initialisation
* Si l'élément a déjà ses event listeners, on passe au suivant
*/
if (el._popoverInitialized) return;
el._popoverInitialized = true;
/**
* Gestion de l'accessibilité clavier
* Si l'élément n'est pas naturellement focusable, on le rend focusable
*/
if (!el.matches(selectors)) {
el.setAttribute('tabindex', '0'); // Rend l'élément focusable au clavier
// Ajout du support de la touche Entrée pour afficher le tooltip
el.addEventListener('keydown', event => {
if (event.key === 'Enter') showTooltip(el);
});
}
/**
* Gestion du survol souris (desktop uniquement)
* Sur tactile, on évite mouseover/mouseout qui peuvent causer des bugs
*/
if (!hasTouchEvents) {
el.addEventListener('mouseover', () => showTooltip(el));
el.addEventListener('mouseout', () => hideTooltip(el));
}
/**
* Gestion du focus/blur (important pour l'accessibilité)
* Fonctionne avec la navigation au clavier (Tab) et les lecteurs d'écran
*/
el.addEventListener('focus', () => showTooltip(el));
el.addEventListener('blur', () => hideTooltip(el));
});
}
/**
* Initialisation automatique au chargement du DOM
* Attache les tooltips à tous les éléments data-popover présents initialement
*/
document.addEventListener('DOMContentLoaded', () => {
attachPopovers();
});
/**
* Exposition de la fonction pour un usage manuel
* Permet d'appeler document.attachPopovers(container) pour du contenu dynamique
*
* Exemple d'usage :
* const newContent = document.createElement('div');
* newContent.innerHTML = '<span data-popover="Info">Hover me</span>';
* document.body.appendChild(newContent);
* document.attachPopovers(newContent);
*/
document.attachPopovers = attachPopovers;
}
})();
Style minimal
Style minimal à adapter à votre interface.
Taille brute : 887,00 octets (~407,00 octets gzip)
#custom-popover-tooltip {
padding: var(--tiny-gutter) var(--small-gutter);
border-radius: 0.3125rem;
line-height: 1.3;
border: none;
position: absolute;
opacity: 0;
font-size: 0.875rem;
font-weight: 450;
transition: opacity 200ms ease-in-out 20ms, overlay 100ms allow-discrete;
&:popover-open {
opacity: 1;
}
@media (prefers-color-scheme: light) {
box-shadow: 0 0.4375rem 0.3125rem -0.3125rem rgba(0, 0, 0, 0.5), 0 0 5px -3px black inset, 0 0 0 1px rgb(255,255,255) inset;
background-color: rgb(30,30,30);
color: white !important;
}
@media (prefers-color-scheme: dark) {
box-shadow: 0 0.4375rem 0.3125rem -0.3125rem rgba(0, 0, 0, 0.5), 0 0 5px -3px black inset, 0 0 0 1px rgb(204,204,204) inset;
background-color: rgb(245,245,245);
color: rgb(2,0,1) !important;
}
&::backdrop {
background-color: transparent;
}
}
@starting-style {
#custom-popover-tooltip:popover-open {
opacity: 0;
}
}
Télécharger kortic-popover.css
Ce code est une base, mais l'API est encore plus complète.
That's all, Folks !