Modales
La famille des modales : la primitive et ses préréglages, les hooks CRUD, un pied sur mesure, et ce qu'un portail doit viser.
Exemples de modales avec le hook useModal
Modale simple
Libellés localisés
Aucune des trois modales ci-dessous ne reçoit de prop agree, disagree ni closeTitle. Chaque libellé — les deux boutons et le nom accessible de la croix de fermeture — est lu dans le bundle components.modal. Basculez la langue du labo et rouvrez-les : les libellés suivent.
Mode popover — léger, non bloquant (usePopover)
usePopover, sur demande : la modale passe par l'API Popover native du navigateur au lieu d'un dialog. Elle peut s'ouvrir DÉCLARATIVEMENT (un bouton avec popovertarget, sans JS) ou par useModal. Échap et un clic sur le fond la ferment. Elle NE bloque PAS la page — pour des panneaux légers, pas pour une confirmation bloquante.
Modale popover (déclarative)
Ouverte par un bouton portant popovertarget — zéro JavaScript. Échap ou un clic sur le fond la ferme.
Modale popover (par useModal)
Le même mode popover, ouvert et fermé par le hook useModal.
Plein écran responsive (point de rupture)
Bascule d'une modale (avec suivi d'état)
Modales d’alerte (un seul bouton)
Modales de confirmation (deux boutons)
Modale en plein écran
Responsive et placement
Largeur sur mesure
Options de comportement
Pied sur mesure
footerNode — pied collé en bas, contenu qui défile
Quand se servir de footerNode
Servez-vous de la prop footerNode quand le pied standard agree / disagree est trop rigide — typiquement pour un formulaire avec une ligne d'état, des boutons à soi, ou toute mise en page qui n'entre pas dans la rangée modal-action par défaut.
✅ Ce que ça apporte
- Le pied toujours collé au bas de la modale
- La zone de contenu qui défile seule, en douceur
- L'en-tête qui reste en haut
- Aucun !important à poser
- Aucune tuyauterie modalBoxClassName à écrire
⚠️ Règles de priorité
Quand footerNode est donné, ces props sont IGNORÉES :
agree, disagree, agreeColor, disagreeColor, agreeIcon, disagreeIcon, showAgree, showDisagree, showFooter, footerReverse, footerClassName, footerOptions, onAgree, onCancel
Un console.warn est émis en développement si l’une d’elles est passée à côté.
Avant / après
❌ Avant (recette à la main — 8 lignes, 5 marqueurs !)
<Modal contentClassName ="!overflow-hidden !p-0 flex flex-col flex-1 min-h-0" modalBoxClassName="!overflow-hidden flex flex-col" showFooter={false}> <div className="flex-1 min-h-0 overflow-y-auto ..."> {form fields} </div> <div className="shrink-0 flex border-t bg-base-100 ..."> {status + cancel + save} </div></Modal>✅ Après — une prop, aucune surcharge
<Modal title="Edit profile" footerNode={<FormFooter ... />}> <form className="flex flex-col gap-4"> {form fields} </form></Modal>// modal-box auto: flex flex-col// content auto: flex-1 min-h-0 overflow-y-autoFormulaire dans une modale
Usage du hook useModal
const { modalRef, open, close, toggle, isOpen } = useModal() ;// With callbacksconst { modalRef, open } = useModal({ onOpen: () => console.log('Opened'), onClose: () => console.log('Closed'),}) ;<Button onClick={ open }>Open</Button><Modal ref={ modalRef }>Content</Modal>Modales empilées (imbrication)
On peut ouvrir plusieurs modales l'une sur l'autre : le navigateur gère l'ordre d'empilement.
Tout fermer d’un coup
closeAllOpenDialogs() ferme chaque <dialog> ouvert du document, sans rien demander — ce qu’il faut avant un départ forcé, et seulement là. Les deux modales ci-dessous partent ensemble, depuis un bouton de la seconde.
Quatre préréglages qui demandent avant de lâcher
Une confirmation à retaper, une valeur montrée une seule fois, une question posée au passage, et une charge utile en lecture. Rien ici n’est détruit : les deux lignes sont des entrées d’un tableau.
ConfirmTypedModal
Une seule instance, repointée de ligne en ligne. Tapez un identifiant puis fermez par Échap ou par un clic à côté de la fenêtre, et rouvrez LA MÊME ligne : le champ est vide et le bouton verrouillé.
record-4817Premier enregistrementrecord-9052Second enregistrementSecretRevealModal
La valeur EST le déclencheur : il n’y a pas de drapeau d’ouverture. Rien ne la referme tant qu’elle n’est pas sauvegardée — essayez Échap. Copiez-la, puis attendez deux secondes : l’icône revient au presse-papiers et le bouton reste déverrouillé. Le toast est l’hôte qui réagit à un rappel, pas la bibliothèque.
PromptModal
Son montage EST la demande d’ouverture. Elle rend le texte sans ses espaces, et une chaîne vide quand rien n’a été tapé. Son champ grandit avec ce qu’on tape, par textAreaProps.
JsonViewerModal
Elle prend la valeur, pas une chaîne : la charge utile est mise en forme ici, et le bouton de copie est dans l’en-tête, là où un écran tactile peut l’atteindre. Elle prévient son hôte de la copie plutôt que de toaster — et l’hôte, ici, toaste.
InputModal — ouverture au focus
Cliquez dans le champ, ou servez-vous du bouton, pour ouvrir la modale.
Avec openOnFocus
Click the input field or the button to open
#3b82f6
Sans openOnFocus
Only the button opens the modal
Quand se servir d’openOnFocus ?
✅ Bien pour :
- Les sélecteurs de date
- Les sélecteurs d’heure
- Les sélecteurs de couleur
- Les choix en un clic
- Les champs en lecture seule
❌ À éviter pour :
- Les formulaires complexes
- Les choix en plusieurs étapes
- Quand il faut taper
- Les envois de fichier
- Les champs modifiables
// ✅ Le bon patron, avec un état temporaireconst [value, setValue] = useState(initial) ;const [tempValue, setTempValue] = useState(initial) ;<InputModal value={value} // ce qui est affiché onModalOpen={() => setTempValue(value)} onAgree={() => setValue(tempValue)}> <input value={tempValue} onChange={setTempValue} /></InputModal>Autres options
Masquer le bouton d’action
Use showActionButton={false} to hide the button
onFocus personnalisé
Combine with your own focus handler
FormModal & ModalFooter
Modifiez le nom puis fermez : la question est posée. Enregistrez : le bouton attend, tout est verrouillé, puis la modale se ferme — sauf si le serveur refuse.
Les trois hooks CRUD
useAddModal, useEditModal, useRemoveModal
| Nom | Courriel | Téléphone | Rôle | Actions |
|---|---|---|---|---|
AJ Alice Johnson | alice@example.com | 555-0101 | Developer | |
BS Bob Smith | bob@example.com | 555-0102 | Designer | |
CB Charlie Brown | charlie@example.com | 555-0103 | Manager | |
DP Diana Prince | diana@example.com | 555-0104 | Developer |
useAddModal
useEditModal
useRemoveModal
Confirmation asynchrone
La modale reste ouverte pendant l'action : bouton « En cours… », Annuler et la croix désactivés, Échap et le fond sans effet. Elle se ferme quand l'action réussit, et reste ouverte avec une erreur quand elle échoue.
Montée à la demande, ouverte au montage
Chaque bouton monte un composant qui ouvre sa propre modale depuis un effet de montage, sans second clic. Les quatre cartes ne diffèrent que par la façon dont la modale atteint le portail et dont on lui dit de s'ouvrir.
Enveloppée dans un Portal par l’appelant
Portal autour de Modal, et un showModal() sur le nœud.
Placée dans un portail par la modale elle-même
La prop portal sur Modal, et le même showModal().
Pilotée par useModal
open() depuis le hook, qui possède le nœud et ses écouteurs.
Déclarée avec openOnMount
useModal({ openOnMount : true }) et aucun effet dans le composant.
Le badge dit si le nœud existait quand l’effet a tourné. Une modale qui reste fermée avec « ref attachée » est un défaut différent d’une qui reste fermée avec « ref toujours nulle » : la seconde veut dire que l’effet a tourné avant que le nœud soit dans le document, et que l’appel optionnel l’a avalé.
Feuille montant du bas
Un panneau qui monte du bas de l'écran et se referme en le glissant vers le bas — ce qu'un menu ancré dans un coin ne peut pas être sur un appareil tenu à deux mains. Il ne porte aucun contenu : ce qui s'y pose appartient au produit.
Elle monte.
Glissez la feuille vers le bas, touchez à côté, pressez Échap, ou touchez une entrée : dans tous les cas elle redescend AVANT que le dialogue se ferme. Les entrées y arrivent par le close que la feuille passe à son contenu.
Les deux blocs qui suivent n'enseignent rien : ils existent pour qu'un défaut se voie. Un toast doit passer au-dessus d'une modale, et un panneau flottant doit dégager une page mise en plein écran — deux pièges du top layer qu'aucun z-index ne rattrape, donc deux choses à rouvrir après chaque changement.
Un toast au-dessus d’une modale
Vérifiez qu’un toast passe au-dessus du fond d’une modale. Le cas d’essai : le top layer natif du dialog contre le popover du ToastProvider.
Ces boutons déclenchent un toast D’ABORD, puis ouvrent une modale quelques centaines de millisecondes plus tard. Sans le MutationObserver du fournisseur, la modale volerait le sommet de la pile du top layer et masquerait le toast. Avec lui, le popover du toast est repromu dès que le nouveau dialog s’ouvre.
Essayez toutes les combinaisons (3 × 3 = 9 positions) : haut / milieu / bas × début / centre / fin. Chaque toast doit s’ancrer au coin, au bord ou au centre choisi — indépendamment de l’endroit où se trouve la modale.
Portal et le top layer du plein écran
Ouvrez chaque surface ci-dessous, passez en plein écran et ouvrez-les à nouveau. Les quatre doivent passer au-dessus de la page dans les deux états — un panneau qui s'ouvre à moitié derrière cette carte est un portail qui visait document.body pendant que le top layer était ailleurs.
document.bodyCelle-ci n'a jamais été concernée : un <dialog> ouvert par showModal() entre de lui-même dans le top layer. Elle est là pour qu'une régression se voie à côté des autres plutôt que sur une autre page.
Du remplissage, juste sous le déclencheur : un panneau ouvert doit couvrir ce texte, jamais glisser derrière. Il est haut exprès — laissez le popover ouvert et faites défiler : le panneau doit suivre son déclencheur plutôt que rester où il s'est ouvert.