Tailwind Safe Area: Bottom Bars, FABs und Toasts

Du lässt die Seite mit viewport-fit=cover den ganzen Screen füllen, fixierst eine Tab-Leiste auf bottom-0, öffnest alles auf dem iPhone, und der Home-Indikator liegt auf deinen Labels. Drehst du das Smartphone quer, schneidet die Notch den Anfang jeder Zeile ab. In Tailwind CSS v4 ist die Tailwind Safe Area deine Sache, denn der Core bringt dafür keine Klassen mit: Alles, was an einem Rand klebt, muss sich sein Padding selbst aus env(safe-area-inset-*) holen. Dieses Tutorial baut den unteren Teil eines Smartphone-Screens mit Tab-Leiste, Kaufleiste, schwebendem Action-Button und Toasts. Keines davon verschwindet unter der Notch oder dem Home-Indikator, und keines verdoppelt den Abstand, wenn sie übereinanderliegen.
Safe Areas sind eine der neun Regeln im Guide zu Mobile-first React-Komponenten. Dieser Beitrag ist die Schritt-für-Schritt-Version, mit der Rechnung für übereinanderliegende Leisten.
Was ändert viewport-fit cover?
viewport-fit=cover sagt dem Browser, dass sich deine Seite selbst um die Aussparungen des Displays kümmert. Mit dem Standard viewport-fit=auto rückt Safari auf dem iPhone die Seite ein, damit nichts unter der Notch oder den abgerundeten Ecken landet, und füllt die übrigen Streifen mit der Hintergrundfarbe von <html> oder <body> (WebKit, 2017 (opens in a new tab)). Mit cover füllt der Viewport das ganze Display, und die Viewport-Referenz von MDN (opens in a new tab) empfiehlt ausdrücklich, dann die Variablen für die Safe Area Insets zu nutzen, damit wichtige Inhalte sichtbar bleiben.
Android kam 2025 dazu. Ab Chrome 135 (opens in a new tab) zeichnet Chrome auf Android-Smartphones Edge to Edge. Standardmäßig zeigt es dabei einen „Chin“ über der Gestenleiste, der beim Scrollen wegrutscht, und eine Leiste mit position: fixed; bottom: 0 landet hinter der Navigationsleiste, sobald der Chin weg ist. Eine Seite mit viewport-fit=cover geht schon beim Laden bis an den Rand und zeigt den Chin nie. So oder so zählen die Insets jetzt auch auf Android.
Im Next.js App Router kommt das Meta-Tag aus einem viewport-Export im Root-Layout, und das muss eine Server Component sein:
Next.js rendert daraus <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">. Ohne Next.js schreibst du das Tag selbst. Versteh cover als Versprechen: Ab jetzt sorgt jedes Element, das du an einen Rand heftest, selbst für sein Padding.
Warum liefert env safe-area-inset-bottom 0?
Meist fehlt der Seite viewport-fit=cover, oder auf deinem Testgerät verdeckt nichts diesen Rand. Ohne cover hält Safari die Seite selbst innerhalb der Safe Area, also bleibt auf dem iPhone kein Inset zu melden. Desktop-Browser melden ebenfalls 0 (MDN zu env() (opens in a new tab)). Ein Layout, das in einem schmalen Desktop-Fenster gut aussieht, beweist also nichts. Ein iPhone mit Home-Button wie das iPhone SE hat keinen Home-Indikator und meldet unten 0. Deshalb kombiniert jede Regel unten das Inset mit einem Mindestwert, statt sich allein darauf zu verlassen.
Prüf die gerenderte Seite, nicht deinen Quellcode: Öffne den Head im Inspector und stell sicher, dass es genau ein Viewport-Tag gibt und dass es viewport-fit=cover enthält. Ein zweites Tag, das ein Plugin oder ein handgeschriebener <head> einfügt, kann das erste überschreiben. Außerdem unterscheiden die Variablennamen Groß- und Kleinschreibung, env(SAFE-AREA-INSET-BOTTOM) fällt also stillschweigend auf den Fallback zurück.
Wie schreibst du eine Utility für Tailwind Safe Area Inset Bottom?
Nimm für einen Einzelfall einen Arbitrary Value und definier ein paar @utility-Klassen, sobald er an mehr als zwei Stellen auftaucht. Tailwind CSS v4 hat im Core keine Safe-Area-Utilities: Stand Version 4.3.3 sind die einzigen -safe-Klassen solche für die Ausrichtung wie justify-center-safe. Sie bilden das CSS-Schlüsselwort safe für Overflow ab und haben mit der Notch nichts zu tun.
Der Arbitrary Value funktioniert überall: pb-[max(env(safe-area-inset-bottom),12px)]. Für ein Projekt legst du ein kleines Set in dein Stylesheet:
Das -* am Ende eines Namens macht eine Utility funktional, und --value(integer) liest die Zahl dahinter. pb-safe-or-3 wird also zu padding-bottom: max(env(safe-area-inset-bottom, 0px), calc(var(--spacing) * 3)), und Varianten wie md:pb-safe funktionieren wie bei jeder anderen Klasse. Willst du lieber das komplette Set installieren (Margins, Scroll Padding, h-dvh-safe sowie -offset-- und -or--Formen für alles), ergänzt das Paket tailwindcss-safe-area (opens in a new tab) es mit @import "tailwindcss-safe-area"; (Version 1.3.0, MIT-Lizenz, Stand Oktober 2026).
Die beiden Funktionen haben unterschiedliche Aufgaben. Nimm max(), wenn das Inset deinen normalen Abstand ersetzt, etwa beim unteren Padding einer Leiste oder beim Rand eines Toasts: Auf einem Smartphone mit Home-Indikator gewinnt das Inset, auf dem Laptop deine 12px. Nimm calc(), wenn du zusätzlich zum Inset einen Abstand brauchst, etwa für einen Button, der 16px über dem Indikator schwebt. Der ursprüngliche Beitrag von WebKit nutzt die Form mit max() aus demselben Grund für das seitliche Padding.
Wie bekommt eine fixierte untere Leiste das richtige Padding?
Gib der Leiste innen Padding und lass sie selbst auf bottom-0. Ihr Hintergrund läuft dann unter den Home-Indikator, und das Padding hebt die tippbare Zeile darüber. Der ganze Fix für iPhone Home Indicator CSS ist eine einzige Klasse auf der Leiste, pb-safe:
Die Zeile bleibt überall 64px hoch. Auf einem Smartphone mit Home-Indikator wächst die Leiste um das Inset; auf dem Desktop ist das Inset 0 und nichts ändert sich. px-safe-or-2 hält den ersten und den letzten Tab im Querformat von der Notch fern. Jeder Tab ist ein Viertel des Screens breit und 64px hoch, also deutlich über der minimalen Touch-Target-Größe.
Die Leiste mit bottom-[env(safe-area-inset-bottom)] nach oben zu schieben, sieht auf einem Screenshot ähnlich aus. Die Seite scrollt dann aber durch einen sichtbaren Streifen unter der Leiste. Chromes Guide zeigt denselben Effekt auf Android.
Der Inhalt hinter der Leiste braucht denselben Platz, sonst scrollt die letzte Zeile einer Liste nie ins Bild:
Was ist mit Chromes Warnung vor Padding mit dem Inset?
Die Warnung gilt für Seiten, die den Chin behalten. Chromes Guide rät davon ab, padding-bottom auf env(safe-area-inset-bottom) zu setzen: Ohne cover ändert sich das Inset, während der Chin wegrutscht, das Padding erzwingt bei jedem Schritt ein neues Layout, und Chrome hört auf, den Chin wegzuschieben, sobald es dieses Muster erkennt. Mit viewport-fit=cover gibt es keinen Chin, der wegrutschen könnte.
Kannst du cover nicht setzen, nimm Chromes Alternative: Reservier vorab das größte Inset und verschieb die Leiste mit bottom, über ein Muster calc(env(safe-area-inset-bottom, ...) - ...), für das Chrome einen Fast Path hat. Tailwinds Preflight setzt box-sizing: border-box, also muss die Höhe das Padding mit einschließen:
safe-area-max-inset-bottom kam mit Chrome 135. Der Fallback von 36px ist der Wert, den Chromes Guide für Browser ohne diese Variable nutzt.
Wie stapelst du einen FAB über einer Tab-Leiste, ohne den Abstand zu verdoppeln?
Leg die volle Höhe der Leiste inklusive Inset in eine CSS-Variable und positionier den Button beim größeren Wert aus Variable und Inset, plus deinem Abstand. Wer beides addiert, zählt das Inset doppelt: Die Höhe der Leiste enthält das Inset schon, also schwebt der Button bei bar + inset + 16px um einen ganzen Home-Indikator-Streifen zu hoch.
Für die statische Leiste von oben reicht für die Variable reines CSS. Setz sie auf einen Wrapper, damit der Button sie erbt, und setz sie dort zurück, wo die Leiste versteckt ist:
Ab md ist die Variable 0px, also fällt max() auf das Inset zurück, und der Button schwebt 16px über der Unterkante, bei Geräten mit Inset 16px über dem Inset. Derselbe Wrapper gibt main sein unteres Padding, die Höhe der Leiste steht also nur an einer Stelle.
Ist die Höhe vorher nicht bekannt (eine Leiste mit anderer Größe pro Seite, eine Kaufleiste, die nach dem Scrollen hereingleitet), miss sie stattdessen: Ein ResizeObserver auf der Leiste schreibt ihre Höhe in die Variable auf <html>. Die Bottom Tab Bar von Wingo UI schreibt --bottom-bar-offset auf diese Weise, wenn sie fixiert ist, und der Wert fällt auf 0, solange md:hidden die Leiste versteckt. Die Sticky CTA Bar schreibt --cta-bar-offset, ihre sichtbare Höhe, die auf 0 fällt, solange sie versteckt ist. Der FAB liest beide:
Die Komponenten brauchen also keine Props, um zusammenzuspielen. Render sie nebeneinander, und der Button bleibt über den Leisten, die gerade zu sehen sind:
Ein Haken in Next.js: Ohne LinkProvider rendern die Tabs einfache <a>-Elemente. Jedes Antippen lädt dann die Seite neu, und die Leiste kennt ihren aktiven Tab nur aus ihrem eigenen State. Umschließ die App einmal mit <LinkProvider component={Link} currentPath={usePathname()}> aus @/lib/navigation (in einer Client Component), dann navigieren die Tabs über next/link und heben die aktuelle Route hervor.
$ npx wingo-ui@latest add fabDie Demo hat keine Tab-Leiste, und auf dem Desktop ist das Inset 0, also sitzt der Button 16px über der Unterkante des Rahmens. Wir haben in Chromium mit dem Test am Ende dieses Beitrags ein unteres Inset von 34px emuliert: Die Regel des FAB wurde zu bottom: 50px (34 + 16) berechnet, das untere Padding der Bottom Tab Bar zu 34px.
Wie setzt du eine Kaufleiste auf eine Tab-Leiste?
Positionier die Kaufleiste mit bottom: var(--bottom-bar-offset) und gib ihr nur für den Teil des Insets Padding, den die Tab-Leiste nicht schon abdeckt. Die Sticky CTA Bar nutzt für ihr unteres Padding diese Klasse:
Liegt eine Tab-Leiste darunter, wird die Subtraktion negativ, max() klemmt sie auf 0, und die Kaufleiste fügt keinen eigenen Abstand hinzu. Auf einer Produktseite ohne Tab-Leiste ist die Variable 0, und die Kaufleiste übernimmt das ganze Inset selbst. Ihre Seiten nutzen pl-[max(1rem,env(safe-area-inset-left,0px))] und die passende Klasse für rechts, damit der Preis auf einem Smartphone im Querformat frei von der Notch bleibt.
Render sie neben der BottomTabBar aus dem vorigen Abschnitt, und sie sitzt auf der Tab-Leiste; render sie allein, und sie sitzt an der Unterkante. Solange das Promise aus onAction aussteht, zeigt der Button einen Spinner.
$ npx wingo-ui@latest add sticky-cta-barDie Demo fixiert die Leiste mit position="sticky" in einem Smartphone-Rahmen, denn fixed würde sie an dein Browserfenster heften. Das Safe-Area-Padding ist in beiden Modi gleich, und hideFrom="md" versteckt die Leiste auf breiten Screens per CSS, wo die Seite ihren eigenen Kaufen-Button zeigt.
Wohin gehören Toasts auf einem Smartphone mit Home-Indikator?
Setz den Toast-Stapel auf den größeren Wert aus Inset und Rand. Hat die App eine untere Tab-Leiste, zeig Toasts auf Smartphones oben an. Den ersten Teil erledigt der Toast von Wingo UI für dich. Unter 768px gehen Toasts über die volle Breite mit 16px Rand (mobileOffset), und jede Kante nutzt max(env(safe-area-inset-*), 16px). Ein Stapel unten bleibt so frei vom Home-Indikator, ein Stapel oben frei von Notch und Statusleiste. Unser Vergleich der Sonner-Alternativen zeigt, welche anderen Toast-Bibliotheken das Safe-Area-Padding für dich übernehmen.
Der Toaster liest --bottom-bar-offset nicht, ein Stapel unten würde auf dem Smartphone also eine Tab-Leiste verdecken. Hat deine App eine, schieb die Toasts auf Smartphones nach oben. Mit dem Viewport-Export aus dem ersten Abschnitt sieht das Root-Layout am Ende so aus:
$ npx wingo-ui@latest add toastDie Platzierung auf dem Desktop behält ihre eigenen Props position und offset, der Stapel in der Ecke bleibt auf dem Laptop also, wo er war.
Wie testest du Safe Areas ohne iPhone?
Auf einem Mac führt der iOS Simulator von Xcode Safari mit den echten Insets jedes simulierten Geräts aus. In der CI kann Chromium sie vortäuschen: Das Chrome DevTools Protocol hat den experimentellen Befehl Emulation.setSafeAreaInsetsOverride, der die Werte von env(safe-area-inset-*) und env(safe-area-max-inset-*) überschreibt, und Playwright kann ihn über eine CDP-Session senden:
Wir haben dieses Muster im Oktober 2026 mit Playwright 1.63 und dem mitgelieferten Chromium gegen die Previews von Bottom Tab Bar, FAB und Sticky CTA Bar laufen lassen. Nimm die Inset-Werte der Geräte, die dir wichtig sind, und ergänz einen zweiten Fall mit bottom: 0. So fallen Leisten auf, die auf Smartphones ohne Home-Indikator ihr Mindest-Padding verlieren. Der Befehl ist als experimentell markiert, also leg deine Playwright-Version fest.
Wo fängst du an?
Ergänz viewportFit: "cover" und die fünf Utilities von oben und reparier dann die Leisten von unten nach oben: zuerst die Tab-Leiste, dann alles, was darüber schwebt. Safe Areas betreffen aber mehr als Leisten: In Wingo UI 1.7.0 lesen 38 Komponentendateien env(safe-area-inset-*), von Sheets und Dialogen bis zum Cookie-Consent-Banner und zur Image Lightbox. Der Guide zum Bottom Sheet zeigt, wie der Footer eines Sheets mit dem Home-Indikator umgeht.
Willst du die Toast-Platzierung aus diesem Beitrag, ohne sie selbst zu schreiben: Der Toast ist kostenlos. Führ npx wingo-ui@latest add toast aus und bind den Toaster einmal in dein Root-Layout ein. Bottom Tab Bar, Sticky CTA Bar und FAB gehören zu Wingo UI Pro. Weitere Guides wie diesen findest du unter Mobile UI in React.
Häufige Fragen
Hat Tailwind CSS Safe-Area-Utilities?
Nicht im Core. Stand Tailwind CSS 4.3.3 (Oktober 2026) gibt es keine Klasse pb-safe. Du nutzt also Arbitrary Values wie pb-[max(env(safe-area-inset-bottom),12px)], definierst ein paar Klassen mit @utility oder installierst das Paket tailwindcss-safe-area.
Warum ist env(safe-area-inset-bottom) immer 0?
Auf dem iPhone fehlt meist viewport-fit=cover. Dann hält Safari die Seite selbst innerhalb der Safe Area und hat kein Inset zu melden. Desktop-Browser und iPhones mit Home-Button melden unten ebenfalls 0, also kombinierst du das Inset über max() mit einem Mindestwert.
Brauche ich viewport-fit=cover auf Android?
Nicht für Edge to Edge: Seit Chrome 135 zeichnet Chrome auf Android-Smartphones ohnehin bis an den Rand, mit einem Chin über der Gestenleiste, der beim Scrollen wegrutscht. Mit viewport-fit=cover reicht die Seite von Anfang an bis zur Unterkante und der Chin erscheint nie, also brauchen fixierte untere Leisten das Safe-Area-Inset auf Android genauso wie auf dem iPhone.
Sollte eine untere Leiste Padding nutzen oder bottom: env(safe-area-inset-bottom)?
Padding, auf einer Seite mit viewport-fit=cover. Die Leiste bleibt an der Unterkante und ihr Hintergrund läuft unter den Home-Indikator, während bottom: env() einen Streifen lässt, durch den die Seite scheint. Seiten, die den Chin von Chrome behalten, nutzen stattdessen das Muster von Chrome mit safe-area-max-inset-bottom.
Wie teste ich Safe Area Insets ohne iPhone?
Nutz den iOS Simulator von Xcode auf einem Mac oder emuliere die Insets in Chromium mit dem experimentellen DevTools-Protocol-Befehl Emulation.setSafeAreaInsetsOverride, den Playwright über eine CDP-Session senden kann.
- Tailwind CSS
- Safe Area
- Mobile
- iOS
- CSS


