100vh mobile fixen: dvh, svh oder lvh in Tailwind

Du gibst einem Chat-Screen h-screen, setzt das Eingabefeld ans Ende einer Flex-Spalte, und auf dem iPhone ist es weg. Es liegt unter der Toolbar von Safari, und die ganze Seite scrollt um genau deren Höhe. Das ist der 100vh mobile Bug, und die Browser verhalten sich mit Absicht so. Dieses Tutorial erklärt, was 100vh auf einem Smartphone misst, wann du in Tailwind CSS dvh, svh oder lvh nimmst und wie du eine App Shell in voller Höhe, Sheets und eine tastaturbewusste Leiste baust, die stehen bleiben, wenn sich die Adressleiste bewegt. Der Code nutzt den Next.js App Router und Tailwind v4, dazu die Komponenten von Wingo UI, die denselben Regeln folgen.
Warum ist 100vh auf dem Smartphone höher als der Bildschirm?
Weil vh mit ausgeblendeten Browserleisten gemessen wird. Ein mobiler Browser kennt drei Viewport-Höhen: small, mit ausgeklappter Adressleiste und Toolbar; large, mit eingeklappten Leisten; und dynamic, also die, die gerade gilt. Die Längenreferenz von MDN (opens in a new tab) sagt es klar: Alle Standard-Viewport-Einheiten wie vh und vw entsprechen derzeit ihren Large-Gegenstücken wie lvh und lvw.
Jede Seite lädt mit sichtbaren Leisten. 100vh ist also von Anfang an um deren Höhe größer als der sichtbare Bereich. Tailwinds h-screen kompiliert genau zu diesem Wert, 100vh, in Tailwind 4.3 genauso wie in v3.
Das ist das klassische „100vh Safari Problem“. Seit iOS 15 sitzt die Adressleiste von Safari standardmäßig unten. Der Streifen, den 100vh verdeckt, liegt also genau dort, wo dein wichtigster Button sitzt. Chrome auf Android folgt bewusst demselben Modell: Als Chrome 56 vh geändert hat, schrieb das Chrome-Team (opens in a new tab), das neue Modell solle dem Verhalten von Safari entsprechen.
Auf dem Desktop und in einer Web-App, die auf dem Homescreen installiert ist, kommen und gehen keine Leisten. Dort sind die drei Höhen gleich, und der Bug tritt nie auf.
Was ist der Unterschied zwischen dvh, svh und lvh?
svh ist die Höhe mit sichtbaren Leisten, lvh die Höhe mit ausgeblendeten Leisten und dvh die, die gerade zutrifft. Alle drei sind Prozentwerte, 100dvh ist also die volle dynamische Höhe:
Die dynamische Einheit hat zwei Haken. Browser drosseln sie: Laut der Einführung von web.dev (opens in a new tab) aktualisieren sich die Werte nicht mit 60 fps. Und alles, was damit bemessen ist, löst bei jeder Aktualisierung einen Reflow aus. WebKit-Bug 266835 (opens in a new tab), gemeldet gegen iOS 17.2.1, beschreibt Ruckeln beim Scrollen, weil dvh mit der Adressleiste die Größe ändert: Abschnitte in voller dvh-Höhe wachsen erst auf die größere Viewport-Höhe, wenn das Scrollen vorbei ist, und das Layout springt am Ende. Stand Oktober 2026 ist der Report weiterhin offen.
Die Unterstützung ist kein Grund mehr zu warten. Safari 15.4, Firefox 101 und Chrome 108 haben alle drei Einheiten ausgeliefert, und caniuse (opens in a new tab) zählt Stand Oktober 2026 rund 95% der weltweiten Nutzung als unterstützt. Die Klassen für Tailwind dvh, svh und lvh kamen mit v3.4 (opens in a new tab), und Tailwind v4 selbst setzt (opens in a new tab) Safari 16.4, Chrome 111 und Firefox 128 voraus. Ein v4-Projekt braucht also keinen vh-Fallback.
Wann nimmst du dvh, svh oder lvh?
Nimm dvh für alles, was auf dem Screen bleibt, svh für alles, was wegscrollt, lvh nur für fixierte Hintergründe und gar keine Einheit für ein fixiertes Element, das an zwei gegenüberliegenden Kanten hängt. Auf die üblichen Teile einer App übertragen:
- Root der App Shell:
min-h-dvh, wenn das Fenster scrollt,h-dvh, wenn eine innere Spalte scrollt. - Bottom Sheets, Popover, Menüs: eine Obergrenze wie
max-h-[92dvh]. - Seitenpanels, Overlays, Vollbildmenüs:
fixed inset-0oderfixed inset-y-0. Ein fixiertes Element, das über seine Kanten bemessen wird, folgt dem sichtbaren Bereich, wenn sich die Leisten bewegen; Chrome hat dieses Verhalten beibehalten, als esvhin Version 56 geändert hat. - Marketing-Hero oder erster Screen:
min-h-svh. Er füllt beim Laden den Screen mit sichtbaren Leisten und wächst beim Lesen nie unter dem Daumen. - Fixiertes Hintergrundbild oder Canvas:
fixed inset-x-0 top-0 h-lvh. So hoch, wie der Screen werden kann, deckt es nie eine Lücke auf und ändert nie seine Größe.
Damit ist die Frage 100dvh vs 100vh entschieden: Wir haben keinen Fall gefunden, in dem einfaches vh die bessere Wahl ist. Und wenn du genau dieses Verhalten willst, macht lvh es ausdrücklich. Keine Komponente von Wingo UI nutzt h-screen oder bemisst ein Layout in voller Höhe in vh; die Komponenten in voller Höhe nutzen min-h-dvh, h-dvh und dvh-Obergrenzen.
Wenn eine bestehende Codebasis voller h-screen ist, kannst du die Klasse zentral an einer Stelle umstellen. In Tailwind v4 hängt ein eigenes Utility mit dem Namen eines Core-Utilitys seine Deklarationen hinter die des Cores:
Mit Tailwind 4.3.3 kompiliert das h-screen und md:h-screen zu height: 100vh; height: 100dvh;, ein Browser ohne dvh behält also den alten Wert. Es macht aber auch jeden Hero zu einem dvh-Hero. Räum deshalb hinterher auf: Such nach h-screen und wähl für jede Stelle eine Einheit.
Wie baust du eine App Shell in voller Höhe, die nicht springt?
Lass das Dokument scrollen, gib der Shell min-h-dvh, fixier den Header mit sticky und die Tab Bar mit fixed. Dann hängt nichts auf der Seite von der Höhe der Toolbar ab, und nichts bewegt sich, wenn sie sich ändert.
Schritt 1: Lass die Seite bis an die Bildschirmränder reichen
Schalte den Bereich um die Notch und den Home-Indikator frei, damit env(safe-area-inset-*) echte Werte liefert. Im App Router ist das der viewport-Export des Root-Layouts:
Der Guide zu Tailwind Safe Areas behandelt die Insets ausführlich; hier polstern sie nur die Leisten.
Schritt 2: eine Shell, die das Fenster scrollt
min-h-dvh wirkt nur, wenn die Seite kürzer als der Screen ist. Dann endet der Inhalt am unteren Rand des sichtbaren Bereichs, und eine so kurze Seite scrollt nicht, die Leisten bleiben also stehen. Eine längere Seite ist ohnehin höher als das Minimum. Die Leisten kommen und gehen beim Scrollen, ohne die Höhe der Seite zu ändern, und die fixierte Tab Bar folgt dem sichtbaren Bereich von selbst.
Window Scroll ist außerdem das Setup, in dem Safari seine Leisten zuverlässig minimiert. Mit einem inneren Scroll-Container hat sich dieses Verhalten zwischen iOS-Releases geändert: Die WebKit-Bugs 228278 (opens in a new tab) und 231878 (opens in a new tab), beide aus dem iOS-15-Zyklus, beschreiben gegensätzliches Verhalten, und beide wurden als beabsichtigt geschlossen. Rechne dort nicht mit dem zusätzlichen Platz.
Schritt 3: eine Shell mit eigenem Scrollbereich
Ein Chat oder ein Board braucht Header und Eingabefeld jederzeit auf dem Screen. Mach die Spalte genau so hoch wie den sichtbaren Bereich und lass ein Kind scrollen:
Das ist der kaputte Screen aus der Einleitung, mit h-dvh statt h-screen. min-h-0 erlaubt dem mittleren Kind zu schrumpfen, sodass es scrollt, statt das Eingabefeld nach unten zu schieben.
Die App Shell baut beide Varianten über eine Prop. scroll="window", der Standard, rendert den Root mit min-h-dvh; scroll="content" rendert h-dvh overflow-hidden. Bei Window Scroll ist die Desktop-Sidebar sticky top-0 h-dvh, sodass ihr Footer auch auf einer langen Seite sichtbar bleibt. Die Doku begründet den Standard so: Window Scroll erhält die Scroll-Wiederherstellung des Routers und das Minimieren der iOS-Toolbar.
Wie hoch sollte ein Bottom Sheet auf dem Smartphone sein?
Höchstens 92dvh: hoch genug für eine lange Liste, kurz genug, um oben einen Streifen der abgedunkelten Seite zu lassen, und in dvh, damit die Adressleiste den Griff nie aus dem Screen schiebt. Solange ein modales Sheet offen ist, ist die Seite dahinter gesperrt, die Leisten bewegen sich also selten, und wenn doch, folgt ihnen eine dvh-Obergrenze.
Seitenpanels brauchen gar keine Einheit: Das kostenlose Sheet platziert sie mit fixed inset-y-0 und polstert oben mit env(safe-area-inset-top). Bei Bottom Sheets entscheiden zwei Details über die Obergrenze:
- Die Einheit. Eine Obergrenze von
92vhsind 92% des Large Viewport. Sobald die Leisten mehr als 8% des Screens belegen, ist das höher als der sichtbare Bereich, und Griff und Titel beginnen oberhalb des oberen Bildschirmrands. - Die 8% Abstand. Der Streifen abgedunkelter Seite über dem Sheet zeigt, dass es eine Ebene ist, gibt den Leuten eine Fläche zum Antippen und Schließen und hält den Griff von der Statusleiste fern.
In reinem Tailwind ist das fixed inset-x-0 bottom-0 flex max-h-[92dvh] flex-col auf dem Panel, min-h-0 flex-1 overflow-y-auto auf dem Body, damit eine lange Liste innerhalb der Obergrenze scrollt, und pb-[max(env(safe-area-inset-bottom),16px)] auf dem letzten Element. Der Guide zum React Bottom Sheet baut dieses Layout mit vaul, Drag und Fokus-Handling.
Der kostenlose Drawer ist genau dieses Layout auf Basis von vaul, mit Drag zum Schließen, Fokus-Handling und dem Safe-Area-Padding schon eingebaut. Seine Sheets wachsen mit ihrem Inhalt bis max-h-[92dvh], und height="full" hält sie auf h-[92dvh], auch wenn eine Suche die Liste verkürzt:
$ npx wingo-ui@latest add drawerWenn ein Sheet den ganzen Screen bedecken muss, gib ihm h-dvh und polstere den oberen Inset. Genau das macht das Sheet als Bottom Sheet mit size="full". Seine kleineren Größen enden bei 50, 70, 85 oder 92 dvh.
$ npx wingo-ui@latest add sheetÄndert sich dvh, wenn die Bildschirmtastatur aufgeht?
Nein. Safari auf iOS, Chrome für Android seit Version 108 (opens in a new tab) und Firefox für Android seit Version 132 (opens in a new tab) verkleinern beim Öffnen der Tastatur nur den Visual Viewport. Der Layout Viewport behält seine Größe, also behalten svh, dvh und lvh ihre Werte, und die Tastatur verdeckt den unteren Teil deines h-dvh-Layouts.
Auf Android kannst du umschalten: interactiveWidget: "resizes-content" im viewport-Export von Next.js (der Schlüssel interactive-widget im Viewport-Meta-Tag) lässt die Tastatur den Layout Viewport verkleinern und jede Viewport-Einheit mit ihm. Das Chat-Layout von oben hält sein Eingabefeld dann ohne JavaScript über der Tastatur. Stand Oktober 2026 listet caniuse (opens in a new tab) keine Unterstützung in Safari, iPhones brauchen also weiterhin Code.
Dieser Code liest window.visualViewport aus: Solange ein Textfeld den Fokus hat, verdeckt die Tastatur innerHeight - visualViewport.height - visualViewport.offsetTop Pixel. Der Hook useKeyboardInset misst das höchstens einmal pro Frame und gibt es als Motion Value zurück, dazu ein open-Flag, das nur neu rendert, wenn es umschlägt. Wenn du nur mit CSS arbeitest, schreibt er den Wert auch in eine Variable auf <html>:
Die Leiste bewegt sich per Transform. Es gibt also keinen Reflow, während die Tastatur hereingleitet. Der Hook meldet 0 auf dem Server, auf dem Desktop, beim Pinch-Zoom und immer dann, wenn kein Textfeld den Fokus hat. Eine verdeckte Höhe von 80px oder weniger (sein threshold) ignoriert er, etwa die Formular-Zubehörleiste von iOS allein. In einem Bottom Sheet brauchst du nichts davon: repositionInputs des Drawers, standardmäßig an, schiebt das Sheet nach oben, damit das fokussierte Feld sichtbar bleibt. Den Rest eines Chat-Screens, vom Auto-Scroll beim Streamen einer Antwort bis zu Enter während einer IME-Eingabe, findest du in unserem Guide zur React AI Chat UI.
$ npx wingo-ui@latest add use-keyboard-insetSolltest du den --vh-Hack mit JavaScript noch nutzen?
Nein. Der Hack setzt in einem Resize-Listener eine Variable --vh aus window.innerHeight. Bis dieses Skript läuft, nutzt die serverseitig gerenderte Seite den Fallback, das Layout kann beim Laden also springen. Danach misst er neu, wenn sich die Leisten bewegen, und springt beim Scrollen genauso wie dvh, nur mit einem zusätzlichen Umweg über den Main Thread. Jeder Browser, den Tailwind v4 unterstützt, hat die nativen Einheiten. Lösch den Hack also, und height: -webkit-fill-available gleich mit.
Wie testest du Viewport-Höhen auf einem echten Smartphone?
Auf dem Smartphone selbst, mit einer Messanzeige auf der Seite. Die Geräteemulation im Desktop-Browser hat keine Leisten, die einklappen. Dort sind die drei Einheiten gleich, und jeder Bug aus diesem Artikel bleibt verborgen. Diese Client Component zeigt die drei Höhen, innerHeight und den Visual Viewport:
Dann, auf einem iPhone und auf einem Android-Smartphone: Lade die Seite und prüf, dass nichts Wichtiges unter der Toolbar liegt. Scroll, bis die Leisten minimiert sind, und zurück, und achte auf Inhalt, der springt. Öffne jedes Sheet und such seinen Griff und seinen letzten Button. Fokussiere ein Textfeld und schau, was die Tastatur verdeckt. Und dreh das Gerät ins Querformat, wo der Screen niedrig ist und jeder Pixel Toolbar zählt.
Wie behebst du 100vh in einer bestehenden App?
Such in deinem Code nach h-screen und 100vh und gib jedem Treffer die Einheit für seine Aufgabe: min-h-dvh oder h-dvh für Shells, min-h-svh für Heros, inset-0 für fixierte Overlays. Installier dann den kostenlosen Drawer und das Sheet mit npx wingo-ui@latest add drawer sheet; beide begrenzen ihre Höhe in dvh und polstern die Safe Areas. Die App Shell und der Hook useKeyboardInset gehören zu Wingo UI Pro. Für die übrigen Regeln auf dem Smartphone, von Bottom Sheets bis zu Touch Targets, lies den Guide zu Mobile-first React-Komponenten oder stöbere in Mobile UI in React.
Häufige Fragen
Warum ist 100vh auf dem Smartphone höher als der Bildschirm?
Alle großen Browser berechnen vh gegen den Large Viewport, also die Größe mit eingeklappter Adressleiste und Toolbar. Solange die Leisten sichtbar sind, ragt 100vh um ihre Höhe über den sichtbaren Bereich hinaus, und ein Button am unteren Rand eines h-screen-Layouts landet unter der Toolbar.
Was ist der Unterschied zwischen 100dvh und 100vh?
100vh ist fest auf die Höhe des Large Viewport gesetzt. 100dvh folgt den Leisten: Es entspricht 100svh, solange sie ausgeklappt sind, und 100lvh, sobald sie einklappen. Damit folgt es der sichtbaren Höhe, ändert aber seine Größe, während die Seite scrollt.
Unterstützt Tailwind CSS dvh?
Ja. Seit Tailwind CSS v3.4 gibt es h-dvh, h-svh und h-lvh samt min-h- und max-h-Varianten, und Arbitrary Values wie max-h-[92dvh] funktionieren auch. h-screen kompiliert in Tailwind 4.3 weiterhin zu height: 100vh.
Ändert sich dvh, wenn die Bildschirmtastatur aufgeht?
Standardmäßig nicht. Safari auf iOS, Chrome für Android seit Version 108 und Firefox für Android seit Version 132 verkleinern für die Tastatur nur den Visual Viewport, die Viewport-Einheiten behalten also ihre Werte. Android-Browser lassen dich mit interactive-widget=resizes-content umschalten; auf iOS liest du window.visualViewport.
Wird dvh in Safari unterstützt?
Ja, seit Safari 15.4 auf macOS und iOS. Chrome und Edge haben die Einheiten mit Version 108 eingeführt, Firefox mit Version 101, und die eigene Baseline von Tailwind CSS v4 (Safari 16.4, Chrome 111, Firefox 128) ist neuer als all diese Versionen.
Sind Sheet und Drawer von Wingo UI kostenlos?
Ja. Sheet und Drawer sind kostenlos, begrenzen ihre Höhe bereits in dvh und polstern die Safe Areas. Die App Shell und der Hook useKeyboardInset gehören zu Pro.
- Mobile UI
- Tailwind CSS
- CSS
- React


