Responsive Design ruht auf einer Annahme, die nie jemand aufgeschrieben hat: dass die Fläche, auf die wir rendern, zusammenhängend ist. Bei einem geteilten Display stimmt sie zum ersten Mal nicht mehr – und keine einzige Breitenabfrage merkt es.
Wenn die Breite lügt
Ein Kunde schickt ein Foto seines neuen Telefons. Auf der Preisseite steht die mittlere von drei Spalten schief im Bild, ein Teil des Textes ist gestaucht, und die Schaltfläche darunter liegt in einer sichtbaren Rille. Der erste Reflex ist, das Layout zu prüfen – und genau dort findet man nichts. Die Seite bekommt rund 1800 CSS-Pixel gemeldet, der Desktop-Breakpoint greift korrekt, das Raster rechnet richtig. Die Zahl beschreibt nur nicht mehr, was sie früher beschrieben hat.
Ein aufgeklapptes Gerät mit Falz liefert eine Breite wie ein kleines Notebook, aber die Fläche dahinter ist in der Mitte physisch unterbrochen. Alles, was diese Mitte kreuzt, verliert: eine Textspalte, ein Formularfeld, eine Bildunterschrift, eine Tabelle. Der Fehler ist nicht das Layout, sondern die Datengrundlage – wir haben fünfzehn Jahre lang aus einer einzigen Zahl auf die Beschaffenheit einer Fläche geschlossen.
Dass das auffällt, ist neu; das Problem selbst nicht. Es ist derselbe blinde Fleck, der auch bei sehr breiten Monitoren zuschlägt, wenn eine Zeile über 2000 Pixel läuft, weil das Stylesheet keinen Grund sah, sie zu begrenzen. Der Unterschied ist, dass dieses Gerät den Fehler nicht nur unschön macht, sondern die Bedienung stellenweise verhindert.
Zwei Fragen, zwei APIs
Für das Thema gibt es inzwischen zwei getrennte Schnittstellen, die im Alltag gern verwechselt werden. Sie beantworten unterschiedliche Fragen, und die meisten Layout-Probleme hängen an der zweiten:
- In welcher Haltung ist das Gerät gerade? Darauf antwortet die Device Posture API – flach oder abgewinkelt. Sie sagt nichts darüber, wo die Kante verläuft.
- Aus wie vielen Teilen besteht die Fläche, und wo liegen sie? Darauf antworten die Viewport Segments: Anzahl per Media Query, Geometrie per env()-Variablen.
Die Haltung ist die einfachere von beiden. Sie kennt zwei Werte:
continuous für eine durchgehende Fläche – das gilt auch für jeden
Desktop, jedes Tablet und jedes gewöhnliche Telefon – und folded für ein
Gerät, das gerade abgewinkelt benutzt wird, etwa aufgestellt wie ein kleines Notebook:
/* Abgewinkelt benutzt: die untere Hälfte liegt flach vor dem Nutzer und
ist die bequemere Fläche für alles, was angetippt wird. */
@media (device-posture: folded) and (vertical-viewport-segments: 2) {
.zw-buchung__aktionen {
position: static;
align-self: end;
}
}
Dieselbe Auskunft gibt es in JavaScript, inklusive einer Benachrichtigung beim Wechsel:
const haltung = navigator.devicePosture;
const uebernehmen = () => {
// "continuous" oder "folded" – auf allen anderen Geräten dauerhaft
// "continuous", eine Fallunterscheidung braucht es also nicht.
document.body.dataset.haltung = haltung.type;
};
haltung.addEventListener('change', uebernehmen);
uebernehmen();
In der Praxis ist die Haltung selten das, was du brauchst. Sie ist nützlich für Anwendungen, die auf die körperliche Situation reagieren – ein Videogespräch, das die Vorschau in die aufgestellte Hälfte legt, eine Kamera, die den Auslöser nach unten zieht. Für ein Seitenlayout ist die interessantere Frage, wie die Fläche geteilt ist.
Die Abfrage nach den Segmenten
Dafür gibt es zwei Media Features, die die Anzahl der Teile abfragen. Beide nehmen eine ganze Zahl ab 1 entgegen:
- horizontal-viewport-segments: die Segmente liegen nebeneinander, die Trennung verläuft also senkrecht.
- vertical-viewport-segments: die Segmente liegen übereinander, die Trennung verläuft waagerecht.
- Auf einem ungeteilten Gerät sind beide 1 – das ist kein Sonderfall, sondern der Normalwert.
Der Name beschreibt die Anordnung der Segmente, nicht die Lage der Kante. Das ist die Stelle, an der man beim ersten Einbau zuverlässig danebengreift: Ein senkrechtes Scharnier erzeugt horizontale Segmente, weil sie horizontal nebeneinander liegen.
/* Basis: untereinander. Diese Regel gilt überall – auch in jedem
Browser, der von den Abfragen darunter noch nie gehört hat. */
.zw-split {
display: flex;
flex-direction: column;
}
/* Zwei Segmente nebeneinander: je ein Bereich pro Hälfte. */
@media (horizontal-viewport-segments: 2) {
.zw-split {
flex-direction: row;
}
}
/* Zwei Segmente übereinander: die Reihenfolge bleibt, aber die
Bedienelemente wandern in die untere, greifbare Hälfte. */
@media (vertical-viewport-segments: 2) {
.zw-split__aktionen {
order: 1;
}
}
Solange beide Hälften gleich groß sind, reicht das aus – zwei Flex-Kinder teilen sich die Fläche von selbst mittig. Sobald sie es nicht sind, und sobald die Trennung selbst eine Breite hat, brauchst du die Maße.
Wo genau der Knick liegt
Die Geometrie kommt über sechs Umgebungsvariablen, die jedes Segment über zwei
Indizes ansprechen: erst die Spalte, dann die Zeile, gezählt ab null von links oben.
Bei zwei nebeneinanderliegenden Segmenten ist 0 0 also das linke und
1 0 das rechte.
- env(viewport-segment-width x y) und env(viewport-segment-height x y) für die Maße eines Segments.
- env(viewport-segment-top x y), -right, -bottom und -left für seine Kanten im Viewport.
- Die Breite der Trennung selbst steht nirgends – sie ergibt sich aus dem Abstand zwischen zwei benachbarten Segmenten.
Damit lässt sich die Unterbrechung als eigene, leere Rasterspalte modellieren. Das ist aus meiner Sicht die sauberste Bauweise, weil danach kein einziges Element mehr über die Kante ragen kann – die Hardware bekommt schlicht ihren eigenen Platz im Grid:
@media (horizontal-viewport-segments: 2) {
.zw-split {
display: grid;
grid-template-columns:
/* linkes Segment */
env(viewport-segment-width 0 0, 50%)
/* die Trennung: Abstand zwischen rechter Kante des linken und
linker Kante des rechten Segments. Liegen beide direkt
aneinander, rechnet sich diese Spalte von selbst auf 0. */
calc(env(viewport-segment-left 1 0, 0px) - env(viewport-segment-right 0 0, 0px))
/* rechtes Segment */
env(viewport-segment-width 1 0, 50%);
}
.zw-split__liste { grid-column: 1; }
.zw-split__detail { grid-column: 3; }
}
Die Ersatzwerte in jedem env()-Aufruf sind kein Zierrat, und der Grund
dafür ist der unangenehmste Fallstrick des ganzen Themas. Laut Spezifikation sind
diese Variablen nur dann definiert, wenn es mindestens zwei Segmente gibt.
Auf jedem gewöhnlichen Gerät existieren sie also nicht.
/* Falsch: sieht auf dem Testgerät gut aus und lässt das Raster
überall sonst still zusammenfallen. */
.zw-split {
grid-template-columns: env(viewport-segment-width 0 0) 1fr;
}
/* Richtig: jeder Aufruf bekommt den Wert, der ohne Teilung gelten soll. */
.zw-split {
grid-template-columns: env(viewport-segment-width 0 0, 22rem) 1fr;
}
Innerhalb der Media Query aus dem Beispiel oben kann der Fall streng genommen nicht eintreten – wo zwei Segmente gemeldet werden, sind die Variablen da. Verlassen würde ich mich darauf nicht: Die Regel wandert beim nächsten Umbau irgendwann aus der Abfrage heraus, und dann bricht sie an einer Stelle, an der niemand danach sucht. Ersatzwert überall, ausnahmslos.
Wenn CSS nicht hinreicht
Alles, was der Browser layoutet, löst du besser im Stylesheet. Es bleibt aber ein Rest, den CSS nicht erreicht: eine Zeichenfläche, eine Karte, ein Diagramm, ein Spielfeld. Wer selbst zeichnet, muss die Maße kennen.
// Ältere Anleitungen zeigen window.visualViewport.segments – das
// Attribut ist inzwischen nach window.viewport umgezogen.
const segmente = () => window.viewport?.segments ?? [];
const zeichnen = () => {
segmente().forEach((segment) => {
// Jedes Segment ist ein DOMRect: x, y, width, height in CSS-Pixeln.
// Auf ungeteilten Geräten enthält die Liste genau einen Eintrag mit
// den Maßen des ganzen Viewports – kein null, kein Sonderfall.
diagrammInBereichZeichnen(segment.x, segment.y, segment.width, segment.height);
});
};
// Die Liste ist eine Momentaufnahme, ein eigenes Ereignis dafür gibt es
// nicht. Nach Drehen, Auf- oder Zuklappen also neu abfragen.
window.addEventListener('resize', zeichnen, { passive: true });
zeichnen();
Der Hinweis auf den Umzug ist wichtiger, als er klingt. Zwischen den ersten
Veröffentlichungen zu dem Thema und heute ist das Attribut von
window.visualViewport nach window.viewport gewandert, und
mit ihm hat sich das Verhalten auf ungeteilten Geräten geändert: Früher kam dort
null zurück, heute eine Liste mit einem Eintrag. Ein aus einem älteren
Beitrag kopiertes if (segments) prüft damit heute etwas anderes als
damals.
Sauber einbauen
Ausgeliefert wird das Ganze bisher nur von Chromium-Browsern: die Device Posture API seit Chrome 132, die Viewport Segments seit Chrome und Edge 138 im Juni 2025. Firefox und Safari setzen weder das eine noch das andere um, und die 2024 gestellten Anfragen nach einer offiziellen Haltung dazu sind bei Mozilla bis heute unbeantwortet. Das ist keine Baseline, und danach richtet sich der Einbau: Die Basis muss ohne alles funktionieren, die Segmente sind die Zugabe.
Für die Prüfung, ob ein Browser ein Media Feature überhaupt kennt, hilft
@supports nicht weiter – das prüft Eigenschaften, keine Abfragen. Eine
unbekannte Bedingung ergibt laut Spezifikation den Wahrheitswert „unknown“, der im
@media zu falsch wird. Ein nicht unterstütztes Feature sieht damit exakt
aus wie ein nicht zutreffendes. Trennen lassen sich die beiden über eine
Doppelabfrage:
/* Kennt der Browser das Feature, trifft genau einer der beiden Zweige
zu – egal, wie das Gerät gerade aussieht. Kennt er es nicht, sind
beide "unknown" und damit falsch. */
@media not all and (horizontal-viewport-segments), (horizontal-viewport-segments) {
.zw-hinweis--geteilt {
display: block;
}
}
Dieselbe Abfrage funktioniert in JavaScript über matchMedia. Die Tabelle
unten ist genau das, live in deinem Browser – links die Abfrage, rechts die Antwort,
die er gerade gibt:
-
(horizontal-viewport-segments: 2)Zwei Segmente nebeneinander - nicht geprüft
-
(vertical-viewport-segments: 2)Zwei Segmente übereinander - nicht geprüft
-
(device-posture: folded)Gerät abgewinkelt benutzt - nicht geprüft
-
window.viewport.segmentsGemeldete Segmente - nicht geprüft
Testen lässt sich das ohne Hardware: Die Geräteemulation in den Chrome- und Edge-Entwicklerwerkzeugen kennt faltbare Profile und schaltet die Segmente samt Haltung mit um. Für einen ersten Durchlauf reicht das. Für die Feinheiten – wie breit die Rille wirklich ist, wie sich das Gerät in der Hand anfühlt – reicht es nicht, aber die betreffen ohnehin fast nur Anwendungen, die die Teilung aktiv ausnutzen wollen.
Fazit
Die praktisch wertvollste Erkenntnis aus dem Thema hat mit Foldables wenig zu tun. Es ist die Einsicht, dass die Viewport-Breite eine Zahl ist und keine Beschreibung: Sie sagt, wie viel Platz da ist, nicht wie er beschaffen ist. Wer sein Layout so baut, dass keine einzelne Spalte über die gesamte Breite eines sehr großen Viewports läuft, hat den schlimmsten Teil des Problems bereits gelöst – auf geteilten Displays ebenso wie auf dem 34-Zoll-Monitor, auf dem niemand testet.
Alles darüber hinaus ist eine bewusste Entscheidung. Die Schnittstellen sind da, sie fügen sich ohne neue Konzepte in bestehendes CSS ein, und sie kosten nichts, solange du sie als Ergänzung baust und nicht als Voraussetzung. Zwei Ersatzwerte an der richtigen Stelle sind der Unterschied zwischen einer Verbesserung für wenige und einem Fehler für alle.