Construction d'une popin (insertion d'un conteneur « modal ») alimenté par une requête asynchrone ou par un contenu Hyper Text Language Markup statique caché déjà présent dans la page.
Petit point sémantique : nous allons parler ici de la construction d'une fenêtre pseudo « modale », donc d'une popin et non pas d'un popup.
- Définitions :
- Popin : contenu HTML inséré dans
la page
courante en superposition du contenu existant. Le plus souvent, un calque de fond vient bloquer l'accès à
l'arrière-plan.
C'est le sujet de cet article. - Popup : nouvelle fenêtre système de navigateur. Comme ce n'est pas le sujet abordé ici, voir le fonctionnement sur MDN
User eXperience/User Interface, accessibilité, etc…
-
Les dimensions d'affichage ;
- le chargement et l'affichage du contenu ;
- les terminaux (mobile, tablette, ordinateur de bureau) ;
- les différents navigateurs, particulièrement Safari sous iOs ;
- et bien entendu, l'accessibilité ! Si si…
Dans une popin, il y a du contenu, là, c'est plutôt normal. Mais il peut y avoir aussi une barre de titre, un pied de bloc fixé en bas pour conserver apparent le bouton d'action principale (type « ajouter au panier » ou valider une action d'un formulaire, etc). Et un bouton de fermeture.
Et tout ça doit rester visible dans la fenêtre du navigateur, quelle que soit sa hauteur, et c'est le moment où ça se corse un peu. Il faut également gérer l'accessibilité, notamment la navigation clavier.
La navigation est d'ailleurs un point crucial pour l'accessibilité.
Quand un élément du contexte déclenche l'ouverture d'une modale par-dessus la page, la gestion du focus est primordiale. Une fois la modale affichée, le curseur doit être mis en avant-plan et ne surtout pas rester sur le déclencheur.
Quand la modale est affichée, tous les éléments « focusables » doivent passer dans le flux de la navigation clavier qui ne doit pas passer en arrière-plan. En résumé, ça tourne en boucle à l'intérieur de la popin.
Cerise sur la gateau, au cas où ce serait trop simple, à la fermeture de la modale, le focus doit être remis sur l'élément qui a déclenché l'ouverture. Que ce soit en naviguant au clavier ou avec un dispositif de pointage.
Au final, ça devient vite un casse-tête à faire manger son clavier à un intégrateur web. Sans sel.
Exemples de modales
Ces sources Javascript et CSS proposent de répondre à ces besoins.
Les 5 premières modales chargent un contenu asynchrone. La dernière affiche un contenu déjà présent dans le code de la page.
Ce contenu de popin est déjà présent dans la page.
L'option bounding: false est ajoutée à l'appel de l'ouverture pour empêcher la fermeture de la popin sur le clic à l'extérieur et focus: 'self' met le focus sur la modale au lieu du bouton de fermeture.
Texte fictif
Texte fictif : Pellentesque orci aenean mi enim nec cras augue suspendisse lacus sapien mi ipsum ligula in tortor scelerisque iaculis praesent felis risus magna.
Texte fictif : Tincidunt suspendisse amet felis orci tortor nibh vitae ultrices lectus a semper elit ligula lacus.
Texte fictif : Posuere nulla dui tristique iaculis faucibus sed nulla magna non nec non vitae odio metus tortor semper purus porttitor euismod.
Texte fictif : Justo integer suscipit pulvinar pellentesque in sapien aenean in nisl congue integer magna mi augue massa tempus enim vel dui pellentesque at tortor justo.
Texte fictif : Nibh vitae quam massa sit lorem sapien sem eu congue magna et eu aliquam.
La partie Javascript
Le petit nom de la fonction est maryPoppins. Les classes CSS sont préfixées mary-poppins. Qu'est-ce qu'on rigole…
Les options de modale
-
Chargement de contenu asynchrone
Utilisation du paramètre url qui charge une requête Hypertext Transfer Protocol
-
Chargement synchrone
Utilisation du paramètre markup.
- Une chaîne simple de caractères ;
- Du code Hyper Text Language Markup.
Quelle que soit la méthode, le contenu peut contenir des balises script qui seront exécutées.
Callbacks
Un paramètre onInit permet d'appeler d'autres fonctions. Le fonctionnement est décrit dans le
code source Javascript.
La fonction de la modale
Taille brute : 11,39 kilo-octets (~3,55 kilo-octets gzip)
((() => {
'use strict';
// initialisation de maryPoppinsOpener pour gérer la reprise
// de focus au clic des éléments quand on ne navigue pas au clavier
window.maryPoppinsOpener = null;
// éléments focalisables
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`;
function maryPoppins(settings) {
// Gestion des libellés internationalisés avec fallback
// Vérification de l'existence de i18n
// Utilise `i18n` global ou un objet vide par défaut
const i18n = window.i18n || {};
i18n.poppins_default_header ??= 'Boite modale sur la page';
i18n.poppins_close_modal_header ??= 'Fermer la modale "%s"';
i18n.poppins_close_modal ??= 'Fermer la modale';
i18n.poppins_content_loading ??= 'Chargement…';
const validRoleValues = new Set(['dialog', 'alert', 'alertdialog']);
const validFocusValues = new Set(['self', 'button']);
const defaultOnInit = (modal) => {
if (typeof window.callBackModal === 'function') {
window.callBackModal(modal);
}
};
const defaultSettings = {
// css : ajoute une classe suffixée par "-modal": xxxx-modal
css: null,
full: false, // si true, la modale fait la largeur du body ou 1024 (spécifiée dans la CSS)
// header : ajoute un header fixe dans la modale
header: settings.header === '' ? i18n.poppins_default_header : settings.header ?? null,
// si header : level gère le niveau de titre, par défaut = 2
level: Math.min(6, Math.max(1, Number(settings.level) || 2)),
// ariaLabel donne un nom à la modale, sinon un nom par défaut est ajouté
ariaLabel: null,
// markup si url null : un sélecteur du DOM ou une chaine HTML
markup: null,
// url si markup null : load un contenu fetch
url: null,
role: validRoleValues.has(settings.role) ? settings.role : 'dialog',
focus: validFocusValues.has(settings.focus) ? settings.focus : 'button',
width: settings.width ?? (settings.full ? null : '40rem'),
// bounding false bloque la fermeture si clic en dehors de la modale
bounding: true,
// centered : par défaut centré verticalement dans la page si < au viewport height
// si false : fixée en haut sur desktop, en bas sur mobile
centered: true,
// opener : si spécifié, ex. opener: this sur l'élément déclencheur
opener: null,
// onInit peut être une fonction ou un tableau de fonctions
// exemples :
// onInit: (modal) => Utils.simCa(modal.querySelector('.mary-poppins-content'))
// onInit: [
// (modal) => Utils.simCa(modal.querySelector('.mary-poppins-content')),
// (modal) => toggleBlank(modal.querySelector('.mary-poppins-content'))
// etc.
// ]
// par défaut, utilise const defaultOnInit, sinon les fonctions passées sont ajoutées
onInit: null
};
let opener = settings.opener || window.maryPoppinsOpener || document.activeElement;
settings = {...defaultSettings, ...settings};
if ((!settings.url && !settings.markup) || (settings.url && settings.markup)) {
console.warn('URL ou contenu HTML requis, mais pas les deux à la fois !');
return;
}
document.documentElement.classList.add('backdrop');
let modal = document.createElement(settings.header !== null ? 'section' : 'div');
modal.className = 'mary-poppins';
settings.centered && modal.classList.add('mary-poppins-center');
settings.full && modal.classList.add('mary-poppins-full-width');
settings.css && modal.classList.add(settings.css);
modal.setAttribute('role', settings.role);
modal.setAttribute('aria-modal', 'true');
modal.style.zIndex = (window.getMaxZIndex?.() ?? function () {
const elements = [...document.querySelectorAll(':scope body *')].filter(el => getComputedStyle(el).zIndex !== 'auto');
return Math.max(...elements.map(el => Number(getComputedStyle(el).zIndex) || 0), 0);
}()) + 1;
if (settings.width) {
modal.style.width = settings.width;
}
let modalContent = document.createElement('div');
modalContent.className = 'mary-poppins-content';
modalContent.setAttribute('tabindex', '0');
if (settings.header !== null) {
let header = document.createElement('header');
header.id = 'mary-poppins-header';
header.innerHTML = `<div role="heading" aria-level="${settings.level}" class="h4">${settings.header}</div>`;
modal.setAttribute('aria-labelledby', 'mary-poppins-header');
modal.classList.add('with-header');
modal.prepend(header);
} else {
modal.setAttribute('aria-label', settings.ariaLabel ?? i18n.poppins_default_header);
}
let buttonCloser = document.createElement('button');
buttonCloser.className = 'mary-poppins-btn-close';
buttonCloser.type = 'button';
buttonCloser.innerHTML = `<span class="screen-readers">${settings.header ? i18n.poppins_close_modal_header.replace('%s', settings.header) : i18n.poppins_close_modal}</span>`;
buttonCloser.addEventListener('click', closeModal);
modal.prepend(buttonCloser);
if (settings.url) {
modalContent.textContent = `${i18n.poppins_content_loading}`;
fetch(settings.url)
.then(response => {
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
return response.text();
})
.then(html => {
modalContent.innerHTML = html;
initModal();
})
.catch(() => {
modalContent.innerHTML = '<p class="error">Erreur de chargement</p>';
});
} else if (settings.markup) {
let markupElement;
try {
// Vérifier si c'est un sélecteur valide
markupElement = document.querySelector(settings.markup);
} catch (error) {
// Ce n'est pas un sélecteur valide, donc c'est probablement du HTML brut
markupElement = null;
}
if (markupElement) {
modalContent.innerHTML = markupElement.innerHTML;
} else {
// On vérifie si c'est du HTML brut ou du texte
const isHTML = str => {
const parser = new DOMParser();
const doc = parser.parseFromString(str, "text/html");
// Vérifie s'il y a des éléments HTML
return Array.from(doc.body.childNodes).some(node => node.nodeType === 1);
};
if (isHTML(settings.markup)) {
modalContent.innerHTML = settings.markup;
} else {
modalContent.textContent = settings.markup;
}
}
initModal();
}
modal.appendChild(modalContent);
document.body.appendChild(modal);
let outsideClickListener = null;
function initModal() {
document.addEventListener('keyup', escClose);
requestAnimationFrame(() => {
if (!settings.bounding) {
document.querySelectorAll('body > *:not(.mary-poppins)').forEach(el => {
el.setAttribute('inert', '');
});
document.documentElement.classList.add('backdrop-inert');
} else {
// Stocke le listener pour pouvoir le supprimer après
outsideClickListener = (e) => {
let getBounding = modal.getBoundingClientRect(),
insideY = e.clientY > getBounding.top && e.clientY < getBounding.bottom,
insideX = e.clientX > getBounding.left && e.clientX < getBounding.right;
if (!insideY || !insideX) {
closeModal();
}
};
document.addEventListener('click', outsideClickListener);
}
if (settings.focus === 'self') {
modal.setAttribute('tabindex', '0');
modal.focus();
} else if (settings.focus === 'button') {
buttonCloser.focus();
}
if (settings.onInit) {
if (Array.isArray(settings.onInit)) {
settings.onInit = [defaultOnInit, ...settings.onInit]; // Nouvelle affectation
} else if (typeof settings.onInit === 'function') {
settings.onInit = [defaultOnInit, settings.onInit]; // Transforme en tableau
}
} else {
settings.onInit = [defaultOnInit]; // Défaut seul
}
// Exécution des callbacks
settings.onInit.forEach(fn => typeof fn === 'function' && fn(modal));
// exécution de scripts inline/src dans le contenu chargé
const scripts = modal.querySelectorAll('script');
if (scripts.length) {
scripts.forEach((oldScript) => {
const newScript = document.createElement('script');
if (oldScript.src) {
newScript.src = oldScript.src;
newScript.async = oldScript.async;
} else {
newScript.textContent = oldScript.textContent;
}
if (oldScript.nonce) {
newScript.nonce = oldScript.nonce;
}
oldScript.replaceWith(newScript);
});
}
checkFooterAndCloser();
trapFocus(modal);
});
}
function checkFooterAndCloser() {
if (modalContent.querySelector('footer, .footer')) {
modal.classList.add('with-footer');
}
for (let closer of modalContent.querySelectorAll('.closer')) {
closer.addEventListener('click', closeModal);
}
}
function closeModal() {
modal.remove();
if (!document.querySelector('.mary-poppins')) {
document.documentElement.classList.remove('backdrop', 'backdrop-inert');
document.body.querySelectorAll('body > [inert]').forEach(el => el.removeAttribute('inert'));
}
document.removeEventListener('keyup', escClose);
// Suppression du listener pour éviter les fuites
if (outsideClickListener) {
document.removeEventListener('click', outsideClickListener);
outsideClickListener = null;
}
requestAnimationFrame(() => {
opener.focus();
window.maryPoppinsOpener = null;
});
}
function escClose(e) {
if (e.key === 'Escape') {
closeModal();
}
}
function trapFocus(element) {
const focusableElements = [...element.querySelectorAll(focusableSelectors)];
if (focusableElements.length === 0) return;
const first = focusableElements[0];
const last = focusableElements[focusableElements.length - 1];
function handleKeyDown(e) {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}
element.removeEventListener('keydown', handleKeyDown); // Évite les doublons
element.addEventListener('keydown', handleKeyDown);
}
}
window.maryPoppins = maryPoppins;
}))();
Utilisation
De manière « automatique ». En plaçant directement un attribut data-popin sur les éléments dans le contenu des pages.
Cet attribut doit être au format JavaScript Object Notation for Linked Data. Par exemple, sur un button :
/* le bouton */
<button type="button" aria-label="Ouvrir une popin"
data-popin='{"url": "https://www.domain.tld/page.html", "width": "30rem", "bounding": false, "focus":"button", "header":"Site alternatif"}'>
<svg aria-hidden="true" width="32" height="32">
<use href="#icon-light-bulb"></use>
</svg>
</button>
/* appel générique */
(()=> {
document.addEventListener('DOMContentLoaded', function handler() {
document.querySelectorAll('[data-popin]').forEach(tag => {
tag.addEventListener('click', (e) => {
window.maryPoppinsOpener = tag;
maryPoppins(JSON.parse(tag.getAttribute('data-popin')));
});
});
document.removeEventListener('DOMContentLoaded', handler);
});
})();
Méthode plus traditionnelle, en appelant la fonction de manière classique dans une autre fonction.
Avec du contenu existant.
document.querySelector('#SELECTOR').addEventListener('click', function (e) {
// mise à jour du trigger pour reprise du focus
window.maryPoppinsOpener = this;
maryPoppins({
markup: document.querySelector('#ID_CONTENT')?.innerHTML,
width: '30rem',
focus: 'self'
/* …parameters */
});
});
Avec un contenu asynchrone.
document.querySelector('#SELECTOR').addEventListener('click', function (e) {
maryPoppins({
url: '/path/to/async/content',
// mise à jour du trigger pour reprise du focus avec param opener
opener: this
// …parameters
});
});
Feuille de styles
La feuille de styles fournie utilise des variables personnalisées, avec des fallbacks pour les couleurs.
Vous pouvez les adapter, bien entendu.
Côté responsive, les styles s'assurent que la modale ne déborde jamais, même sur écran réduit quand on spécifie une largeur prévue pour écrans larges.
Taille brute : 9,84 kilo-octets (~2,66 kilo-octets gzip)
/*
* backdrop d'attente de chargement
* nécessite attribut sur body data-loading="loading label"
*/
:root[class*='backdrop'] {
height: 100dvh;
overflow: hidden;
body {
overscroll-behavior: none;
}
/* arrière-plan flou sauf éléments exclus */
&:not(.no-blur) body > *:not(:is(.mary-poppins, [class*="admin"], #snack-bar)) {
-webkit-filter: blur(5px);
filter: blur(5px);
opacity: .1;
}
}
:root.backdrop body {
/* overlay intercepteur de clics (z-index: header + 2) */
&:after {
position: fixed;
content: '';
inset: 0;
z-index: 5;
}
/*
* étiquette de chargement centrée (z-index: after + 1)
* image décorative via --svg-colibri (à remplacer) :
* --svg-colibri: url('data:image/svg+xml,...');
*/
&:before {
position: fixed;
color: black;
background-color: white;
top: 50%;
left: 50%;
border-radius: var(--gutter, 2.5rem);
content: attr(data-loading);
z-index: 6;
transform: translate(-50%, -50%);
background-image: var(--svg-colibri);
background-position: var(--small-gutter, 0.625rem) center;
}
}
@media (prefers-color-scheme: light) {
:root.backdrop body:before {
box-shadow: 0 0 0 0.0625rem darkgray;
}
}
@media (max-width: 47.9375rem) {
:root.backdrop body:before {
line-height: calc(var(--gutter, 2.5rem) * 1.5);
padding: 0 calc(var(--half-gutter, 1.25rem) * 1.5) 0 calc(var(--gutter, 2.5rem) * 1.5);
background-size: var(--gutter, 2.5rem);
}
}
@media (min-width: 48rem) {
:root.backdrop body:before {
line-height: var(--gutter, 2.5rem);
padding: 0 var(--half-gutter, 1.25rem) 0 var(--gutter, 2.5rem);
background-size: var(--half-gutter, 1.25rem);
}
}
/*
* Modal .mary-poppins
*/
.mary-poppins {
--background-color-popin: var(--background-color-secondary, white);
--box-shadow-color: var(--box-shadow);
--mary-poppins-radius: 0.625rem;
background-color: var(--background-color-popin);
border-radius: var(--mary-poppins-radius);
color: inherit;
max-width: calc(100vw - calc(var(--mary-poppins-gutter) / 2));
overscroll-behavior-y: contain;
position: fixed;
top: calc(var(--mary-poppins-gutter) / 4);
left: 50%;
transform: translateX(-50%);
--max-height: calc(100dvh - calc(var(--mary-poppins-gutter) / 2));
--mary-poppins-header-height: calc(var(--mary-poppins-gutter) * 1.2);
--mary-poppins-footer-height: var(--mary-poppins-header-height);
@media (max-width: 63.9375rem) {
--mary-poppins-gutter: 2.75rem;
}
@media (min-width: 64rem) {
--mary-poppins-gutter: 3.125rem;
}
@media (max-width: 30rem) {
min-width: calc(100vw - var(--gutter, 2.5rem));
}
/* viewport mobile sans hover */
@media (any-hover: none) and (max-width: 47.9375rem) {
--max-height: calc(100dvh - var(--mary-poppins-gutter));
/*
--max-height: calc(100dvh - calc(var(--mary-poppins-gutter) * 3));
*/
&:not(.mary-poppins-center) {
top: auto;
bottom: calc(var(--mary-poppins-gutter) / 4);
}
}
&.mary-poppins-center {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/*
* backdrop via :before pour éviter un nœud DOM supplémentaire
* pointer-events géré aussi côté JS (settings.bounding)
*/
&:before {
position: fixed;
inset: calc(50% - 50vh) var(--margin-hero, calc(50% - 50vw)) calc(50% - 50vh) var(--margin-hero, calc(50% - 50vw));
content: '';
z-index: -1;
pointer-events: all;
}
/* désactivation des événements backdrop quand :root.backdrop-inert */
:root.backdrop-inert & {
&:before {
pointer-events: none;
}
}
/* fond partagé header / footer / contenu */
header, .header, footer, .footer, .mary-poppins-content {
background-color: var(--background-color-popin);
}
&.mary-poppins-full-width {
width: var(--size-bodyss, calc(100dvw - 3rem));
}
/* --- zone de contenu scrollable --- */
.mary-poppins-content {
max-height: var(--max-height);
overflow: auto;
padding: calc(var(--mary-poppins-gutter) / 2);
box-shadow: 0 0 0 1px var(--box-shadow-color);
outline-offset: -1px;
}
&:not(.with-header) .mary-poppins-content {
padding-right: calc(var(--mary-poppins-gutter) / 1.2);
padding-left: calc(var(--mary-poppins-gutter) / 1.8);
}
&.with-header .mary-poppins-content {
margin-top: var(--mary-poppins-header-height);
max-height: calc(var(--max-height) - var(--mary-poppins-header-height));
}
&.with-footer .mary-poppins-content {
margin-bottom: var(--mary-poppins-footer-height);
max-height: calc(var(--max-height) - var(--mary-poppins-footer-height));
}
&.with-footer.with-header .mary-poppins-content {
max-height: calc(var(--max-height) - calc(var(--mary-poppins-header-height) + var(--mary-poppins-footer-height)));
}
/* --- header --- */
&.with-header header {
box-shadow: 0 1px 0 0 var(--box-shadow-color), 0 0 0 1px var(--box-shadow-color);
color: var(--heading-color, black);
font-weight: 500;
line-height: var(--mary-poppins-header-height);
padding: 0 var(--mary-poppins-header-height) 0 calc(var(--mary-poppins-gutter) / 2);
position: absolute;
inset: 0 0 calc(100% - var(--mary-poppins-header-height)) 0;
border-radius: var(--mary-poppins-radius) var(--mary-poppins-radius) 0 0;
> * {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: unset !important;
line-height: inherit;
}
}
/* --- footer --- */
footer, .footer {
border-radius: 0 0 var(--mary-poppins-radius) var(--mary-poppins-radius);
box-shadow: 0 -1px 0 0 var(--box-shadow-color), 0 0 0 1px var(--box-shadow-color);
inset: calc(100% - var(--mary-poppins-footer-height)) 0 0 0;
line-height: var(--mary-poppins-footer-height);
margin: 0;
padding: 0 calc(var(--mary-poppins-gutter) / 2);
position: absolute;
}
/* --- border-radius selon présence header / footer --- */
&:not(.with-header):not(.with-footer) .mary-poppins-content {
border-radius: var(--mary-poppins-radius);
}
&.with-footer:not(.with-header) .mary-poppins-content {
border-radius: var(--mary-poppins-radius) var(--mary-poppins-radius) 0 0;
}
&.with-header:not(.with-footer) .mary-poppins-content {
border-radius: 0 0 var(--mary-poppins-radius) var(--mary-poppins-radius);
}
/* --- bouton de fermeture --- */
.mary-poppins-btn-close {
background-color: transparent;
border-radius: 0 var(--mary-poppins-radius) 0 0;
color: inherit;
height: var(--mary-poppins-header-height);
right: 1px;
top: 1px;
position: absolute;
width: var(--mary-poppins-header-height);
z-index: 1;
--outline-offset: -0.125rem;
--outline-color: var(--link-color, black);
/* cercle vide (before, mode contrast) */
--mask-image-before: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="m16,31.5C7.45,31.5.5,24.55.5,16S7.45.5,16,.5s15.5,6.95,15.5,15.5-6.95,15.5-15.5,15.5Zm0-30C8,1.5,1.5,8,1.5,16s6.5,14.5,14.5,14.5,14.5-6.5,14.5-14.5S24,1.5,16,1.5Z"/><path d="m31,16c0,8.28-6.72,15-15,15S1,24.28,1,16,7.72,1,16,1s15,6.72,15,15Z"/></svg>');
/* cercle plein avec croix (after, défaut) */
--mask-image-after: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="m16,2c-7.73,0-14,6.27-14,14s6.27,14,14,14,14-6.27,14-14S23.73,2,16,2Zm4,20.1l-3.15-3.12c-.47-.46-1.22-.46-1.68,0l-3.13,3.15c-.46.47-1.22.47-1.69,0l-.45-.45c-.46-.46-.46-1.21,0-1.68l3.13-3.17c.46-.47.46-1.22-.01-1.68l-3.16-3.12c-.47-.46-.47-1.22,0-1.69l.46-.46c.46-.46,1.21-.47,1.68,0l3.17,3.13c.47.46,1.22.46,1.68-.01l3.11-3.15c.46-.47,1.22-.47,1.69,0l.47.47c.46.46.46,1.21,0,1.68l-3.13,3.16c-.46.47-.46,1.22,0,1.68l3.14,3.11c.47.47.47,1.23,0,1.69l-.47.46c-.46.46-1.21.46-1.67,0Z"/></svg>');
&:after, &:before {
content: '';
-webkit-mask-position: center center;
-webkit-mask-size: 60%;
-webkit-mask-repeat: no-repeat;
mask-position: center center;
mask-size: 60%;
mask-repeat: no-repeat;
position: absolute;
inset: 0;
}
&:after {
-webkit-mask-image: var(--mask-image-after);
mask-image: var(--mask-image-after);
background-color: var(--basic-color, black);
}
}
/* taille réduite du bouton sans header */
&:not(.with-header) .mary-poppins-btn-close {
width: calc(var(--mary-poppins-gutter) / 1.2);
height: calc(var(--mary-poppins-gutter) / 1.2);
}
/* icônes mask réduites quand header présent */
&.with-header .mary-poppins-btn-close {
&:after, &:before {
-webkit-mask-size: 40%;
mask-size: 40%;
}
}
/* sticky interne à la popin */
[class*=stick] {
position: -webkit-sticky;
position: sticky;
top: calc(calc(var(--mary-poppins-gutter) / 2) * -1);
}
.sticky-line {
background-color: inherit;
}
}
/* --- surcharges mode contrast (:root.contrast) --- */
:root.contrast {
.mary-poppins {
--box-shadow-color: var(--canvasText, black);
padding: 1px;
&.with-header header {
inset: 1px 1px calc(100% - var(--mary-poppins-header-height)) 1px;
}
footer, .footer {
inset: calc(100% - var(--mary-poppins-footer-height)) 1px 1px 1px;
}
.mary-poppins-btn-close {
--outline-offset: -0.0625rem;
&:before {
-webkit-mask-image: var(--mask-image-before);
mask-image: var(--mask-image-before);
background-color: var(--basic-color-reverse, white);
}
}
}
}
/* --- opacité des éléments inerts sous backdrop --- */
:root[class*="backdrop"] {
&:not(.contrast) [inert] {
opacity: .35;
}
&.contrast [inert] {
opacity: .06;
}
}
That's all Folks !