Parent : [[INDEX-WEB-UIUX]]
Web UI/UX - Modal Scroll Lock + Keyboard Navigation : body overflow, Escape, Arrow keys
Problème
Un modal sans scroll lock permet de scroller la page en arrière-plan. Sans gestion clavier, l'utilisateur ne peut pas fermer le modal avec Escape ni naviguer dans une galerie avec les flèches.
Solution
Scroll lock via document.body.style.overflow, backdrop click pour fermer, et keydown listener pour Escape/ArrowLeft/ArrowRight.
export function AssetModal({ asset, onClose }: Props) {
const [activeIdx, setActiveIdx] = useState(0)
// Reset index quand l'asset change
useEffect(() => { setActiveIdx(0) }, [asset.id])
// ── BODY SCROLL LOCK ──
useEffect(() => {
const prev = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => { document.body.style.overflow = prev }
}, [])
const prev = useCallback(() => setActiveIdx((i) => (i - 1 + images.length) % images.length), [images.length])
const next = useCallback(() => setActiveIdx((i) => (i + 1) % images.length), [images.length])
// ── KEYBOARD NAVIGATION ──
useEffect(() => {
const h = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
if (e.key === 'ArrowLeft') prev()
if (e.key === 'ArrowRight') next()
}
document.addEventListener('keydown', h)
return () => document.removeEventListener('keydown', h)
}, [onClose, prev, next])
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
style={{ background: 'rgba(0,0,0,0.75)', backdropFilter: 'blur(4px)' }}
onClick={onClose} // Backdrop click
>
<div
className="w-full max-w-2xl mx-4 flex flex-col max-h-[90vh] overflow-y-auto rounded-[6px]"
style={{ background: 'var(--surface)', border: '1px solid var(--border)' }}
onClick={(e) => e.stopPropagation()} // Empêche la fermeture au clic sur le contenu
>
{/* Header */}
<div className="flex items-start justify-between gap-4 p-5 border-b" style={{ borderColor: 'var(--border)' }}>
<h2>{asset.title}</h2>
<button onClick={onClose}>✕</button>
</div>
{/* Gallery */}
<div className="relative aspect-video w-full overflow-hidden">
<img src={activeImage} alt={asset.title} />
{images.length > 1 && (
<>
<button onClick={prev} className="absolute left-3 top-1/2 -translate-y-1/2">‹</button>
<button onClick={next} className="absolute right-3 top-1/2 -translate-y-1/2">›</button>
<div className="absolute bottom-3 left-1/2 -translate-x-1/2">
{activeIdx + 1} / {images.length}
</div>
</>
)}
</div>
</div>
</div>
)
}
Points clés
- Sauvegarde de
document.body.style.overflow: restauration propre à la fermeture e.stopPropagation()sur le contenu du modal : évite la fermeture accidentelleEscapepour fermer : standard UX attendu par tous les utilisateursArrowLeft/ArrowRightpour la galerie : navigation rapide sans sourismax-h-[90vh] overflow-y-auto: le contenu scrollable sans déborder l'écranz-50: au-dessus de tout (header z-40)
Pourquoi
- Scroll lock empêche la perte de contexte (l'utilisateur revient au même endroit)
- Keyboard navigation = accessibilité + power users
- Backdrop click = fermeture intuitive
Quand l'utiliser
- Galeries, lightboxes, confirmations, formulaires
- Tout contenu qui demande le focus exclusif
Quand NE PAS l'utiliser
- Drawer latéral (pas besoin de backdrop blur)
- Toast/notification (pas de scroll lock nécessaire)
Références
- ProjectAlpha/AssetModal.tsx L42-100