Wenn der Viewport kein
Rechteck mehr ist.

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:

resources/css/buchung.css
/* 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:

resources/js/haltung.js
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.

resources/css/split.css
/* 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:

resources/css/split.css
@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.

resources/css/split.css
/* 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.

resources/js/diagramm.js
// Ä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:

resources/css/hinweis.css
/* 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.segments Gemeldete Segmente
nicht geprüft
Auf einem gewöhnlichen Bildschirm steht hier je nach Browser „trifft nicht zu“ oder „kennt dein Browser nicht“ – und das ist der eigentliche Punkt: Ohne die Doppelabfrage wären beide Zustände nicht zu unterscheiden. Auf einem geteilten Gerät wechseln die Werte beim Auf- und Zuklappen, ohne Neuladen.

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.

Fragen zum Thema – oder ein Projekt, das genau daran hängt?

Schreib mir über das Kontaktformular kurz, worum es geht. Ich schaue mir deinen Fall an und sage dir konkret, was sich verbessern lässt – unverbindlich und ohne Verkaufsgespräch.