Tu fais une page “full height” propre, tu mets height: 100dvh (parce que 100vh est connu pour être bancal sur mobile), et tu poses ton CTA en bas. Sur desktop, nickel. Sur mobile, tu focuses un champ… et là, le clavier s’ouvre et ton CTA disparaît sous le clavier. Ou pire, ton contenu devient un truc impossible à scroller.
Ce n’est pas toi qui es nul. C’est un mélange de viewport CSS, de clavier virtuel, et de comportements différents entre Safari, Chrome et les WebViews. Ce qui compte, c’est d’arrêter de “se battre” avec 100% de hauteur, et de choisir une stratégie qui tient en prod.
Le symptôme typique : “full-height” + bottom bar + formulaire = bug assuré
Le pattern que je vois le plus souvent, c’est une page de login ou de checkout en mode “app” : un header, un bloc de contenu, et une barre d’action collée en bas (CTA, boutons de navigation, validation). Le layout est souvent en flex avec un container fixé à la hauteur du viewport.
Quand le clavier virtuel s’ouvre, l’utilisateur voit encore le champ (parce que le navigateur essaie d’être sympa), mais l’espace réellement visible se réduit. Sauf que ton container, lui, reste parfois à 100dvh (ou 100vh). Résultat : la zone du bas est maintenant sous le clavier. Et si tu as en plus un scroll-lock (overflow hidden sur le body, modal qui capture le scroll, etc.), tu viens de fabriquer une UI qui ne permet littéralement plus de valider le formulaire.
Sponsorisé par Le Scribouillard
Besoin de contenu optimisé SEO ?
Utilisez la meilleure plateforme française de création de contenu assistée par IA ! Et générez des articles pour moins de 1€ !
Pourquoi dvh ne “suit” pas forcément le clavier (et pourquoi c’est logique)
On mélange souvent deux notions : le layout viewport (celui sur lequel le layout CSS est calculé) et le visual viewport (la partie réellement visible à l’écran). Les barres du navigateur (URL, toolbar) et le clavier virtuel jouent principalement avec le visual viewport. Et le clavier est un cas à part : sur beaucoup de plateformes, il est traité comme une surcouche système qui réduit la zone visible, sans forcément déclencher un recalcul “propre” des unités CSS.
Les unités modernes svh/lvh/dvh ont été pensées pour mieux gérer les barres de navigateur qui apparaissent/disparaissent. Mais le clavier, selon le navigateur et le contexte (Safari, Chrome, WKWebView, in-app browser…), peut ne pas impacter la valeur que le moteur utilise pour dvh. Donc oui : tu peux être “à jour” côté CSS et te prendre le bug quand même.
Et c’est là que ça devient piégeux : ce n’est pas un bug reproductible “universel”. Tu peux tester sur ton Chrome Android et te dire que ça va, puis découvrir que ça casse sur iOS Safari, ou dans une WebView d’app (le vrai enfer), ou seulement en mode paysage, ou seulement quand un champ est proche du bas.
Le piège n°1 : utiliser height au lieu de min-height (et verrouiller le scroll)
Le réflexe “height: 100dvh” est souvent la racine du problème, pas parce que 100dvh est “mauvais”, mais parce que height fixe + contenu interactif + clavier = tu crées une boîte rigide au moment où l’UI devient dynamique.
Dans la plupart des écrans avec formulaire, le fix le plus rentable c’est de basculer vers min-height et de laisser le contenu respirer. Ça évite pas tout, mais ça retire déjà beaucoup de situations où tu te retrouves avec une zone qui ne peut plus scroller.
.screen {
min-height: 100dvh;
display: flex;
flex-direction: column;
}
.screen__content {
flex: 1;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.screen__actions {
position: sticky;
bottom: 0;
padding-bottom: env(safe-area-inset-bottom);
background: white;
}Le combo important ici, c’est l’idée suivante : le contenu scrolle dans sa zone, et la barre d’action est sticky (donc attachée au bas de la zone scrollable), pas un position: fixed collé au viewport global. Sticky n’est pas magique, mais c’est souvent moins conflictuel avec le clavier que fixed + container full-height.
Et si tu fais partie des gens qui mettent overflow: hidden sur body “pour éviter le bounce” ou “pour faire app-like”, méfiance. Sur mobile, c’est exactement le genre de détail qui transforme un souci “le CTA passe sous le clavier” en “l’utilisateur est bloqué”.
Le piège n°2 : le bottom CTA en position: fixed (et le faux sentiment de sécurité)
Un CTA fixé en bas du viewport, sur desktop, c’est confortable. Sur mobile, quand le clavier arrive, le navigateur ne te doit rien. Certains vont essayer de “remonter” la page pour que l’input reste visible, mais ton fixed reste fixed, et l’overlay du clavier passe par-dessus. Tu te retrouves avec une barre d’action qui existe mais que l’utilisateur ne peut plus toucher.
Si tu as absolument besoin d’une barre collée en bas (vraie navigation d’app, stepper, etc.), je préfère une approche où la barre fait partie du layout et où c’est la zone centrale qui gère le scroll. Flex + sticky, ou flex + footer normal, ça paraît moins “app”, mais c’est souvent beaucoup plus fiable.
Les modals et le scroll-lock : là où ça devient vraiment sale
Les modals “plein écran” sont un concentré de tous les problèmes : on bloque le scroll du body, on met un container full-height, on centre des éléments, et on met des boutons en bas. Puis on ajoute un champ. Et le clavier arrive.
Le piège classique, c’est le modal en position: fixed qui prend height: 100dvh, avec un contenu interne qui n’a pas de scroll. Résultat : tu peux avoir un champ focusable, mais pas la possibilité de scroller pour accéder au bouton “Valider”. Dans ces cas-là, la solution “propre” n’est pas de bricoler le padding à la main, c’est de rendre la zone de contenu scrollable et d’éviter de figer une hauteur absolue partout.
Sur iOS en particulier, le combo “modal fixed” + “scroll-lock” + “input” a des effets secondaires pénibles (scroll qui saute, focus qui déclenche des repositionnements). Plus tu gardes une architecture simple (un conteneur scrollable unique, pas 4 scroll nested), plus tu réduis la surface de bug.
Quand le CSS ne suffit pas : la solution JS minimaliste avec Visual Viewport
Il y a des cas où tu ne peux pas juste “laisser scroller” : un écran très design-system, une bottom sheet, une barre d’action qui doit rester visible, ou une WebView qui se comporte différemment selon l’app. Là, le fix le plus robuste que je connaisse, c’est de se baser sur l’API VisualViewport pour obtenir la hauteur réellement visible, et de la pousser dans une variable CSS.
L’idée est simple : on ne fait pas du JS pour recoder le layout, on fait du JS pour donner au CSS une info fiable. Et on garde un fallback pour les navigateurs qui ne supportent pas l’API.
function updateVvh() {
const vv = window.visualViewport;
const h = vv ? vv.height : window.innerHeight;
document.documentElement.style.setProperty('--vvh', `${h}px`);
}
updateVvh();
window.addEventListener('resize', updateVvh);
if (window.visualViewport) {
// Sur certains navigateurs, l'ouverture du clavier déclenche surtout des events ici.
window.visualViewport.addEventListener('resize', updateVvh);
window.visualViewport.addEventListener('scroll', updateVvh);
}.screen {
height: var(--vvh, 100dvh);
display: flex;
flex-direction: column;
}
.screen__content {
flex: 1;
overflow: auto;
}Deux remarques terrain.
D’abord, visualViewport.scroll surprend : quand le clavier s’ouvre, le visual viewport peut se “déplacer” (offset) même si la page ne scrolle pas au sens traditionnel. Écouter scroll en plus de resize rend la valeur plus stable sur certains devices.
Ensuite, ne tombe pas dans le piège du “je vais compenser en ajoutant exactement la hauteur du clavier en padding”. Ça devient vite une guerre contre des cas limites (orientation, zoom, barres du navigateur, safe area). En général, tu veux juste que ton container prenne la bonne hauteur, et que le scroll interne fasse le reste.
iOS Safari vs Android Chrome vs WebViews : ce que je garde en tête en QA
Sur Android Chrome, tu as souvent un comportement assez cohérent : le visual viewport se met à jour, et l’API VisualViewport est plutôt fiable. Sur iOS Safari, ça s’est amélioré ces dernières années, mais le clavier reste un moment où des trucs “spéciaux” arrivent, surtout si tu as des éléments fixed et des scroll containers imbriqués.
Et les WebViews, c’est le monde réel : WKWebView côté iOS, WebView Android, in-app browsers (Instagram, Facebook, LinkedIn…). Là, tu peux avoir des écarts sur le déclenchement des events, sur la manière dont le clavier est géré, et même sur la valeur de innerHeight. Si ton produit vit beaucoup dans une WebView, le fix VisualViewport + variable CSS est souvent celui qui te donne le plus de contrôle, sans partir en usine à gaz.
Mon arbitrage en prod : arrêter de “forcer le plein écran” quand ce n’est pas nécessaire
Je vais être direct : la plupart du temps, on met du 100vh/100dvh “par réflexe design”, pas parce que c’est fonctionnellement indispensable. Et sur mobile, ce réflexe coûte cher. Si ton écran a un formulaire, laisse le navigateur faire son boulot : contenu scrollable, footer dans le flux ou sticky, pas de scroll-lock agressif.
Quand tu as un vrai besoin “app-like”, assume une stratégie robuste : soit tu conçois ton layout pour tolérer le clavier (zones scrollables bien définies), soit tu passes par VisualViewport pour rendre la hauteur visible exploitable en CSS. Ce n’est pas du hack honteux, c’est juste s’adapter au fait que le clavier n’est pas un élément de layout CSS standard.
Conclusion : dvh n’est pas une baguette magique, c’est juste un outil de plus
dvh/svh/lvh ont clairement amélioré la vie des devs frontend sur mobile. Mais ils ne règlent pas tout, et surtout pas le moment où un clavier virtuel vient “manger” l’écran. Le bon move, c’est de traiter ça comme un bug de prod UI : tu identifies le pattern (full-height + fixed + input), tu simplifies le scroll, et si besoin tu ajoutes une fine couche JS avec VisualViewport.
Si tu veux pousser le sujet encore plus loin, le prochain niveau c’est de regarder comment ton design system gère les bottom sheets, les modals, et les scroll containers. C’est souvent là que les bugs “clavier” naissent, pas dans l’input lui-même.