Lesereihenfolge im Raster
Ein Broken Grid, bei dem Tab und Screenreader dank reading-flow so wandern, wie das Auge liest; ohne Unterstützung bleibt das Raster in HTML-Reihenfolge.
Bento- und Broken-Grids in Portfolios, Agentur- und Kulturseiten, bei denen jede Kachel frei sitzt.
Bento- und Broken-Grids setzen Kacheln frei ins Raster. Das Auge liest dann Zeile für Zeile, die Tab-Taste und der Screenreader aber folgen weiter dem HTML. Der Fokus springt kreuz und quer. Die CSS-Eigenschaft reading-flow ordnet Tab-Reihenfolge und Vorlesereihenfolge nach dem Layout: grid-rows folgt den Zeilen, grid-columns den Spalten. Mit reading-flow: source-order und reading-order an einzelnen Kacheln zieht man ein Element nach vorn oder hinten, der Rest bleibt in HTML-Reihenfolge. Die freie Anordnung steht in @supports (reading-flow: grid-rows). Browser ohne reading-flow zeigen das Raster in HTML-Reihenfolge, damit Bild und Fokus nie auseinanderlaufen.
Die Komposition darf mutig sein, ohne Tastatur- und Screenreader-Nutzer abzuhängen. Der Fokus wandert so, wie das Auge liest. Und die Absicherung über @supports macht aus einer neuen Eigenschaft keinen Bruch in älteren Browsern.
Wann einsetzen
Bento-Raster mit Kacheln unterschiedlicher Größe, Broken Grids in Portfolios, Projekt-Übersichten mit hervorgehobenem Projekt, Layouts, die sich je Breite umsortieren (Telefon in HTML-Reihenfolge, Desktop frei).
Wann nicht
Nicht, um falsch sortiertes HTML zu reparieren: Wenn die Reihenfolge überall gleich sein soll, das HTML umstellen. Keine positiven tabindex-Werte als Ersatz. Nicht bei Inhalten, deren Reihenfolge Bedeutung trägt (Schritte, Ranglisten), wenn das Layout sie optisch anders zeigt.
Varianten
- Broken Grid mit reading-flow: grid-rows
- Hervorgehobene Kachel vorn: reading-flow: source-order und reading-order: -1
- Spaltenweise lesen mit reading-flow: grid-columns (Masonry-artige Spalten)
- Flex mit order oder row-reverse: reading-flow: flex-visual
Umsetzung
Grundzustand: .raster { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)) } mit Spannweiten, die Kacheln fließen in HTML-Reihenfolge. Freie Platzierung als Variable je Kachel (--zeilen: 1 / 3 / 3 / 7) und nur angewandt in @media (min-width: 761px) { @supports (reading-flow: grid-rows) { .raster { reading-flow: grid-rows } .raster > .k { grid-area: var(--zeilen) } } }. Hervorhebung: @supports (reading-order: 1) { .raster { reading-flow: source-order } .k { grid-area: auto / span 3 } .k-highlight { grid-area: 1 / 1 / 3 / 4; reading-order: -1 } }. Jede Kachel ist ein <a> mit h3 als Titel; keine Rollen überschreiben, kein positiver tabindex. Prüfen: mit Tab durchgehen und den Accessibility-Baum in den DevTools ansehen. Die Demo zeigt im Zustand „Vorher“ absichtlich das Anti-Muster (freie Platzierung ohne reading-flow) und liest die tatsächliche Reihenfolge aus getComputedStyle (readingFlow, readingOrder) und den Kachel-Positionen; „Rücksprünge“ zählt Schritte gegen die Leserichtung.
Robust umsetzen
Telefon. Auf schmalen Bildschirmen eine Spalte in HTML-Reihenfolge, reading-flow ist dort nicht nötig. Deshalb gehört die freie Platzierung in eine min-width-Media-Query.
Ohne Bewegung. Das Muster selbst bewegt nichts. In der Hilfsansicht erscheinen Pfad und Nummern ohne Zeichnen.
Barrierefrei. WCAG 1.3.2 Bedeutungstragende Reihenfolge: Vorlesereihenfolge folgt dem Layout. 2.4.3 Fokusreihenfolge: Tab folgt dem Lesefluss statt quer durchs Raster. 2.4.7 Fokus sichtbar: deutlicher Rahmen um die ganze Kachel. 2.4.11 Fokus nicht verdeckt. 2.4.4 Linkzweck: jede Kachel ist ein Link mit Projektname als Namen. Ohne reading-flow fällt das Layout auf HTML-Reihenfolge zurück, statt die Reihenfolge zu brechen. Kein positiver tabindex.
Code
<!DOCTYPE html>
<!--
Lesereihenfolge im Raster
Fundus · Zugang & Robustheit · https://cecodesign.de/lab/fundus/zugang/reading-flow-reihenfolge/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Arbeiten · Paulsstadt</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'TikTok Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/tiktok-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 300 900; font-stretch: 75% 150%; font-display: swap; }
@font-face { font-family: 'DM Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/dm-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'DM Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/dm-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
:root {
/* Rezept kreativstudio-buehne: Schwarz und Weiß tragen alles, eine Signalfarbe */
--nacht: #0B0B0B; --tief: #161616; --hell: #F2F2F2; --weiss: #FFFFFF; --meta: #8A8A8A; --signal: #FF5A1F;
--kurve: cubic-bezier(0.16, 1, 0.3, 1); /* Expo-Out, eine Kurve für alles */
--pfad: 1200ms; --nummer: 400ms; --rahmen: 300ms;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--nacht); color: var(--weiss); font: 400 18px/1.4 'TikTok Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; }
button, input { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.label { font: 500 12px/1.3 'DM Mono', ui-monospace, monospace; letter-spacing: .04em; text-transform: uppercase; }
.pultlabel { font: 500 10.5px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--meta); }
.seite { max-width: 1320px; margin: 0 auto; padding: 0 clamp(16px, 3vw, 40px) 64px; }
.kopf { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 22px 0; }
.wortmarke { font: 800 22px/1 'TikTok Sans', sans-serif; font-stretch: 75%; letter-spacing: -.02em; text-transform: uppercase; }
.wortmarke span { color: var(--signal); }
.kopf nav ul { list-style: none; display: flex; gap: 26px; }
.kopf nav a { display: inline-block; padding: 8px 0; font: 500 12px/1 'DM Mono', monospace; text-transform: uppercase; letter-spacing: .04em; color: var(--meta); }
.kopf nav a[aria-current] { color: var(--weiss); }
.kopf a:focus-visible { outline: 3px solid var(--signal); outline-offset: 4px; }
.titel { display: grid; grid-template-columns: 1fr minmax(0, 340px); align-items: end; gap: 24px; padding: clamp(12px, 3vh, 36px) 0 clamp(18px, 3vh, 30px); border-bottom: 1px solid #262626; }
.titel h1 { font-size: clamp(3.5rem, 11vw, 11rem); font-stretch: 75%; font-weight: 800; line-height: .86; letter-spacing: -.03em; text-transform: uppercase; }
.titel h1 sup { font: 500 .14em/1 'DM Mono', monospace; letter-spacing: 0; vertical-align: top; color: var(--signal); margin-left: .2em; }
.titel p { color: #BDBDBD; font-size: 17px; line-height: 1.45; padding-bottom: .6em; }
/* ---------- Pult ---------- */
.pult { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 22px; padding: 16px 0; }
.schalter { display: inline-flex; align-items: center; gap: 10px; background: none; border: 0; cursor: pointer; padding: 6px 0; }
.schalter .spur { width: 38px; height: 22px; border-radius: 999px; background: #2a2a2a; position: relative; transition: background-color var(--rahmen) var(--kurve); }
.schalter .spur::after { content: ''; position: absolute; left: 3px; top: 3px; width: 16px; height: 16px; border-radius: 50%; background: var(--weiss); transition: translate var(--rahmen) var(--kurve); }
.schalter[aria-pressed="true"] .spur { background: var(--signal); }
.schalter[aria-pressed="true"] .spur::after { translate: 16px 0; background: var(--nacht); }
.schalter:focus-visible, .wahl label:has(input:focus-visible), .varianten button:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; border-radius: 999px; }
.wahl { border: 0; display: inline-flex; padding: 3px; border-radius: 999px; background: #1d1d1d; }
.wahl legend { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.wahl label { position: relative; padding: 8px 14px; border-radius: 999px; cursor: pointer; font: 500 10.5px/1 'Geist Mono', monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--meta); transition: background-color var(--rahmen) var(--kurve), color var(--rahmen) var(--kurve); }
.wahl input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.wahl label:has(input:checked) { background: var(--weiss); color: var(--nacht); }
.wahl label.nachher:has(input:checked) { background: var(--signal); }
.varianten { display: inline-flex; align-items: center; gap: 10px; }
.varianten [role="group"] { display: inline-flex; padding: 3px; border-radius: 999px; border: 1px solid #2a2a2a; }
.varianten button { border: 0; background: none; cursor: pointer; padding: 7px 12px; border-radius: 999px; font: 500 10.5px/1 'Geist Mono', monospace; letter-spacing: .12em; text-transform: uppercase; color: var(--meta); }
.varianten button[aria-pressed="true"] { color: var(--weiss); background: #262626; }
.zaehler { margin-left: auto; display: flex; gap: 22px; align-items: baseline; }
.zaehler div { display: grid; gap: 2px; justify-items: end; }
.zaehler b { font: 800 34px/1 'TikTok Sans', sans-serif; font-stretch: 75%; font-variant-numeric: tabular-nums; }
.zaehler b.null { color: var(--signal); }
.stand { display: inline-flex; align-items: center; gap: 8px; }
.stand i { width: 8px; height: 8px; border-radius: 50%; background: #e0b100; }
.stand.ja i { background: #3ccf7a; }
/* ---------- Raster ---------- */
.raster { position: relative; display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); grid-auto-rows: minmax(clamp(118px, 12.5vw, 176px), auto); gap: 12px; } /* minmax: Kacheln wachsen mit erhöhten Textabständen (1.4.12) */
.k { position: relative; display: flex; flex-direction: column; justify-content: space-between; padding: 16px 18px; border-radius: 6px; overflow: hidden; isolation: isolate; outline: 3px solid transparent; outline-offset: 4px; transition: outline-color var(--rahmen) var(--kurve), outline-offset var(--rahmen) var(--kurve); }
.k:focus-visible, .k.fokus-sim { outline-color: var(--signal); outline-offset: 5px; }
.k .label { position: relative; z-index: 1; opacity: .8; }
.k h3 { position: relative; z-index: 1; font-size: clamp(22px, 2.4vw, 38px); font-stretch: 75%; font-weight: 800; line-height: .92; letter-spacing: -.02em; text-transform: uppercase; max-width: 12ch; }
.k h3 small { display: block; font: 400 14px/1.3 'TikTok Sans', sans-serif; font-stretch: 100%; letter-spacing: 0; text-transform: none; margin-top: 6px; opacity: .78; }
.k .kunst { position: absolute; z-index: 0; right: 0; top: 0; width: 62%; height: 100%; pointer-events: none; }
.hell { background: var(--hell); color: var(--nacht); }
.dunkel { background: var(--tief); color: var(--weiss); box-shadow: inset 0 0 0 1px #262626; }
.signal { background: var(--signal); color: var(--nacht); }
.signal h3 { max-width: 9ch; }
.signal .pfeil { position: absolute; right: 16px; bottom: 12px; font: 800 44px/1 'TikTok Sans', sans-serif; transition: translate var(--rahmen) var(--kurve); }
.signal:hover .pfeil, .signal:focus-visible .pfeil { translate: 6px 0; }
.k:hover .kunst { scale: 1.03; }
.kunst { transition: scale 800ms var(--kurve); transform-origin: 80% 30%; }
/* Grundzustand und Fallback: HTML-Reihenfolge, nur Spannweiten */
.k-a { grid-column: span 4; grid-row: span 2; }
.k-b { grid-column: span 2; grid-row: span 2; }
.k-c, .k-d, .k-e { grid-column: span 2; }
.k-f, .k-g { grid-column: span 3; }
/* Freie Platzierungen je Variante (als Variablen, damit „vorher“ und „nachher“ dieselben nutzen) */
.k-a { --zeilen: 1 / 3 / 3 / 7; --reihe: auto / span 3; }
.k-b { --zeilen: 3 / 5 / 5 / 7; --reihe: auto / span 3; }
.k-c { --zeilen: 1 / 1 / 2 / 3; --reihe: auto / span 3; }
.k-d { --zeilen: 4 / 3 / 5 / 5; --reihe: 1 / 1 / 3 / 4; }
.k-e { --zeilen: 2 / 1 / 4 / 3; --reihe: auto / span 3; }
.k-f { --zeilen: 4 / 1 / 5 / 3; --reihe: auto / span 3; }
.k-g { --zeilen: 3 / 3 / 4 / 5; --reihe: auto / span 3; }
@media (min-width: 761px) {
/* VORHER (Anti-Muster): frei platziert ohne reading-flow. Tab folgt dem HTML, das Auge dem Raster. */
.raster[data-modus="vorher"][data-var="zeilen"] > .k { grid-area: var(--zeilen); }
.raster[data-modus="vorher"][data-var="reihenfolge"] > .k { grid-area: var(--reihe); }
/* NACHHER: freie Platzierung nur, wenn der Browser die Reihenfolge mitziehen kann */
@supports (reading-flow: grid-rows) {
.raster[data-modus="nachher"][data-var="zeilen"] { reading-flow: grid-rows; }
.raster[data-modus="nachher"][data-var="zeilen"] > .k { grid-area: var(--zeilen); }
}
@supports (reading-order: 1) {
.raster[data-modus="nachher"][data-var="reihenfolge"] { reading-flow: source-order; }
.raster[data-modus="nachher"][data-var="reihenfolge"] > .k { grid-area: var(--reihe); }
.raster[data-modus="nachher"][data-var="reihenfolge"] > .k-d { reading-order: -1; }
}
}
/* ---------- Hilfsansicht: Reihenfolge ---------- */
.ebene { position: absolute; inset: 0; pointer-events: none; z-index: 5; opacity: 0; transition: opacity var(--rahmen) var(--kurve); }
.zeigen .ebene { opacity: 1; }
.zeigen .k > *:not(.kunst) { opacity: .28; transition: opacity var(--rahmen) var(--kurve); }
.ebene svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.ebene path { fill: none; stroke: var(--signal); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.ebene path.schatten { stroke: var(--nacht); stroke-width: 8; opacity: .55; }
.ebene .zeichnen { animation: zeichnen var(--pfad) var(--kurve) both; }
@keyframes zeichnen { from { stroke-dashoffset: var(--laenge); } to { stroke-dashoffset: 0; } }
.nr { position: absolute; width: 48px; height: 48px; margin: -24px 0 0 -24px; border-radius: 50%; display: grid; place-items: center; font: 800 24px/1 'TikTok Sans', sans-serif; font-stretch: 75%; font-variant-numeric: tabular-nums;
background: var(--weiss); color: var(--nacht); box-shadow: 0 0 0 3px var(--nacht), 0 10px 24px rgba(0, 0, 0, .45); animation: nr-rein var(--nummer) var(--kurve) both; animation-delay: calc(var(--i) * 60ms); transition: scale var(--rahmen) var(--kurve), background-color var(--rahmen) var(--kurve); }
.nachher-an .nr { background: var(--signal); }
.nr.an { scale: 1.28; }
.nr.rueck::after { content: '↶'; position: absolute; right: -10px; top: -10px; width: 22px; height: 22px; border-radius: 50%; background: var(--nacht); color: var(--signal); font: 700 14px/22px system-ui, sans-serif; text-align: center; }
@keyframes nr-rein { from { opacity: 0; scale: .6; } }
.hinweis { margin-top: 16px; display: flex; flex-wrap: wrap; gap: 6px 24px; color: var(--meta); font-size: 15px; }
.hinweis b { color: var(--weiss); font-weight: 600; }
.werte { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px 16px; }
.werte b { color: var(--weiss); font-weight: 500; }
/* Reduzierte Bewegung: Pfad und Nummern ohne Zeichnen */
.reduziert .ebene .zeichnen, .reduziert .nr { animation: none; }
.reduziert .kunst, .reduziert .k, .reduziert .signal .pfeil, .reduziert .ebene { transition: none; }
.reduziert .k:hover .kunst { scale: 1; }
@media (hover: none) {
.k:hover .kunst { scale: 1; }
.wahl label { padding: 11px 15px; }
.varianten button { padding: 10px 13px; }
}
@media (max-width: 760px) {
.raster { grid-template-columns: 1fr 1fr; grid-auto-rows: minmax(150px, auto); gap: 10px; }
.kopf { flex-wrap: wrap; }
.k-a { grid-column: span 2; grid-row: span 2; }
.k-b { grid-column: span 1; grid-row: span 2; }
.k-c, .k-d, .k-e { grid-column: span 1; }
.k-f, .k-g { grid-column: span 2; }
.titel { grid-template-columns: 1fr; }
.kopf nav ul { gap: 16px; }
.zaehler { margin-left: 0; width: 100%; justify-content: space-between; }
.zaehler div { justify-items: start; }
.k h3 { font-size: 26px; }
}
@media (max-width: 359px) {
/* 320 px (Reflow 1.4.10): eine Spalte, kein Titel wird angeschnitten */
.k-a, .k-b, .k-c, .k-d, .k-e, .k-f, .k-g { grid-column: 1 / -1; }
.k-b { grid-row: span 1; }
}
/* Vorschau 960 × 720: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .kopf, .vorschau .varianten, .vorschau .hinweis, .vorschau .werte, .vorschau .titel p { display: none; }
.vorschau .titel { padding-top: 22px; grid-template-columns: 1fr; }
.vorschau .titel h1 { font-size: 104px; }
.vorschau .raster { grid-auto-rows: 112px; }
.vorschau .pult { padding: 12px 0; }
/* Windows-Kontrastmodus: Schalter und Varianten behalten eine Kante, die Wahl bleibt erkennbar */
@media (forced-colors: active) {
.schalter, .varianten button { border: 1px solid ButtonText; }
.schalter[aria-pressed="true"], .varianten button[aria-pressed="true"] { outline: 2px solid Highlight; outline-offset: -4px; }
}
</style>
</head>
<body>
<div class="seite">
<header class="kopf">
<a class="wortmarke" href="#">Paulsstadt<span>.</span></a>
<nav aria-label="Hauptmenü"><ul><li><a href="#" aria-current="page">Arbeiten</a></li><li><a href="#">Studio</a></li><li><a href="#">Kontakt</a></li></ul></nav>
</header>
<main>
<div class="titel">
<h1>Arbeiten<sup>07/41</sup></h1>
<p>Studio für Marken, Räume und Bewegtbild in Schwerin. Sieben Projekte aus drei Jahren, ausgewählt, weil sie etwas verändert haben.</p>
</div>
<div class="pult">
<button type="button" class="schalter" aria-pressed="false" data-zeigen><span class="spur" aria-hidden="true"></span><span class="pultlabel">Reihenfolge anzeigen</span></button>
<fieldset class="wahl" data-modus-wahl>
<legend>Lesereihenfolge</legend>
<label class="vorher"><input type="radio" name="modus" value="vorher">Vorher</label>
<label class="nachher"><input type="radio" name="modus" value="nachher" checked>Nachher</label>
</fieldset>
<div class="varianten">
<span class="pultlabel" id="v-titel">Variante</span>
<div role="group" aria-labelledby="v-titel">
<button type="button" data-v="zeilen" aria-pressed="true">grid-rows</button>
<button type="button" data-v="reihenfolge" aria-pressed="false">reading-order</button>
</div>
</div>
<div class="zaehler">
<div aria-live="polite"><b data-rueck>0</b><span class="pultlabel">Rücksprünge</span></div>
<div><span class="pultlabel stand" data-stand><i></i><span></span></span></div>
</div>
</div>
<h2 class="sr" id="arbeiten-titel">Ausgewählte Arbeiten</h2>
<!-- HTML-Reihenfolge = redaktionelle Reihenfolge (Telefon). Das Raster ordnet erst ab 761 px frei. -->
<div class="raster" data-raster data-modus="nachher" data-var="zeilen" aria-labelledby="arbeiten-titel" role="region">
<a href="#" class="k k-a hell" data-name="Seebühne Nord">
<p class="label">2026 · Kampagne</p>
<h3>Seebühne Nord<small>Spielzeit 26/27, Plakat und Bewegtbild</small></h3>
<svg class="kunst" viewBox="0 0 400 300" preserveAspectRatio="xMaxYMax meet" aria-hidden="true">
<circle cx="300" cy="96" r="44" fill="#FF5A1F"/>
<g fill="none" stroke="#0B0B0B" stroke-width="14"><path d="M90 300a210 210 0 0 1 420 0"/><path d="M140 300a160 160 0 0 1 320 0"/><path d="M190 300a110 110 0 0 1 220 0"/><path d="M240 300a60 60 0 0 1 120 0"/></g>
</svg>
</a>
<a href="#" class="k k-b dunkel" data-name="Fähre 7">
<p class="label">2025 · Erscheinungsbild</p>
<h3>Fähre 7<small>Brauerei am Ziegelsee</small></h3>
<svg class="kunst" viewBox="0 0 200 300" preserveAspectRatio="xMaxYMid meet" aria-hidden="true">
<g stroke="#F2F2F2" stroke-width="6" fill="none" stroke-linecap="round"><path d="M20 150q20-12 40 0t40 0 40 0 40 0"/><path d="M20 176q20-12 40 0t40 0 40 0 40 0" opacity=".6"/><path d="M20 202q20-12 40 0t40 0 40 0 40 0" opacity=".35"/></g>
<path d="M70 128h70l-12 16H82z" fill="#FF5A1F"/><path d="M104 128V70l30 46z" fill="#F2F2F2"/>
</svg>
</a>
<a href="#" class="k k-c dunkel" data-name="Hafenkino">
<p class="label">2026 · Website</p>
<h3>Hafenkino<small>Programm und Tickets</small></h3>
<svg class="kunst" viewBox="0 0 200 120" preserveAspectRatio="xMaxYMin meet" aria-hidden="true">
<g fill="#F2F2F2"><rect x="80" y="18" width="34" height="24" rx="2"/><rect x="120" y="18" width="34" height="24" rx="2" fill="#FF5A1F"/><rect x="160" y="18" width="34" height="24" rx="2"/></g>
<g fill="#F2F2F2" opacity=".5"><rect x="80" y="8" width="114" height="4"/><rect x="80" y="48" width="114" height="4"/></g>
</svg>
</a>
<a href="#" class="k k-d hell" data-name="Plattenbau Records">
<p class="label">2024 · Cover-Serie</p>
<h3>Plattenbau Records<small>Zwölf Cover, ein System</small></h3>
<svg class="kunst" viewBox="0 0 200 120" preserveAspectRatio="xMaxYMin meet" aria-hidden="true">
<g fill="#0B0B0B"><circle cx="112" cy="30" r="20"/><circle cx="158" cy="30" r="20"/><circle cx="112" cy="76" r="20"/><circle cx="158" cy="76" r="20" fill="#FF5A1F"/></g>
<g fill="#F2F2F2"><circle cx="112" cy="30" r="4"/><circle cx="158" cy="30" r="4"/><circle cx="112" cy="76" r="4"/><circle cx="158" cy="76" r="4"/></g>
</svg>
</a>
<a href="#" class="k k-e dunkel" data-name="Lichtsalz">
<p class="label">2025 · Verpackung</p>
<h3>Lichtsalz<small>Meersalz aus der Wismarbucht</small></h3>
<svg class="kunst" viewBox="0 0 200 300" preserveAspectRatio="xMaxYMin meet" aria-hidden="true">
<g stroke="#F2F2F2" stroke-width="2" opacity=".55"><path d="M60 0 200 140M90 0l110 110M120 0l80 80M150 0l50 50M30 0l170 170"/></g>
<g fill="#F2F2F2"><rect x="128" y="40" width="22" height="22" transform="rotate(18 139 51)"/><rect x="160" y="78" width="14" height="14" transform="rotate(-12 167 85)"/><rect x="104" y="92" width="10" height="10" fill="#FF5A1F"/></g>
</svg>
</a>
<a href="#" class="k k-f dunkel" data-name="Archiv der Stille">
<p class="label">2024 · Ausstellung</p>
<h3>Archiv der Stille<small>Raum und Leitsystem</small></h3>
<svg class="kunst" viewBox="0 0 240 120" preserveAspectRatio="xMaxYMin meet" aria-hidden="true">
<g stroke="#F2F2F2" stroke-width="1.5" opacity=".5"><path d="M110 20h120M110 34h120M110 48h120M110 62h120M110 76h120M110 90h120"/></g>
<rect x="188" y="44" width="42" height="8" fill="#FF5A1F"/>
</svg>
</a>
<a href="#" class="k k-g signal" data-name="Projekt anfragen">
<p class="label">Offen ab Januar</p>
<h3>Neues Projekt? Schreib uns.</h3>
<span class="pfeil" aria-hidden="true">→</span>
</a>
<div class="ebene" aria-hidden="true" data-ebene><svg><path class="schatten" data-pfad-s/><path data-pfad/></svg></div>
</div>
<div class="hinweis" data-hinweis></div>
<p class="werte pultlabel" data-werte aria-hidden="true"></p>
</main>
</div>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = (s, r = document) => r.querySelector(s), $$ = (s, r = document) => [...r.querySelectorAll(s)];
const raster = $('[data-raster]'), ebene = $('[data-ebene]'), kacheln = $$('.k', raster);
const pfad = $('[data-pfad]'), pfadS = $('[data-pfad-s]'), knopf = $('[data-zeigen]');
const KANN = CSS.supports('reading-flow', 'grid-rows'), KANN_ORDER = CSS.supports('reading-order', '1');
let variante = 'zeilen';
const modus = () => $('[name="modus"]:checked').value;
// Reihenfolge so, wie der Browser sie gerade nutzt (aus den berechneten Stilen)
function reihenfolge() {
const cs = getComputedStyle(raster), fluss = cs.readingFlow || 'normal';
const box = k => k.getBoundingClientRect();
if (fluss === 'grid-rows') return [...kacheln].sort((a, b) => (Math.round(box(a).top) - Math.round(box(b).top)) || (box(a).left - box(b).left));
if (fluss === 'source-order') return kacheln.map((k, i) => [k, +(getComputedStyle(k).readingOrder || 0), i]).sort((a, b) => a[1] - b[1] || a[2] - b[2]).map(x => x[0]);
return [...kacheln];
}
// Rücksprung: die nächste Kachel liegt optisch vor der aktuellen (höher, oder gleiche Zeile weiter links)
function rueckspruenge(liste) {
const b = liste.map(k => k.getBoundingClientRect());
return liste.map((k, i) => i > 0 && (b[i].top < b[i - 1].top - 4 || (Math.abs(b[i].top - b[i - 1].top) <= 4 && b[i].left < b[i - 1].left)));
}
let nummern = [];
function zeichne() {
const liste = reihenfolge(), rueck = rueckspruenge(liste);
const n = rueck.filter(Boolean).length;
const z = $('[data-rueck]'); z.textContent = n; z.classList.toggle('null', n === 0);
nummern.forEach(x => x.remove()); nummern = [];
if (!raster.classList.contains('zeigen')) return;
const p = liste.map(k => [k.offsetLeft + k.offsetWidth / 2, k.offsetTop + k.offsetHeight / 2]);
const d = p.map((q, i) => (i ? 'L' : 'M') + q[0].toFixed(1) + ' ' + q[1].toFixed(1)).join(' ');
[pfad, pfadS].forEach(el => {
el.setAttribute('d', d);
const l = el.getTotalLength();
el.style.strokeDasharray = l; el.style.setProperty('--laenge', l);
el.classList.remove('zeichnen'); void el.getBBox(); el.classList.add('zeichnen');
});
liste.forEach((k, i) => {
const s = document.createElement('span');
s.className = 'nr' + (rueck[i] ? ' rueck' : ''); s.textContent = i + 1;
s.style.left = p[i][0] + 'px'; s.style.top = p[i][1] + 'px'; s.style.setProperty('--i', i);
s.dataset.fuer = k.dataset.name;
ebene.appendChild(s); nummern.push(s);
});
raster.classList.toggle('nachher-an', modus() === 'nachher');
}
function stand() {
const ok = variante === 'zeilen' ? KANN : KANN_ORDER;
const el = $('[data-stand]');
el.classList.toggle('ja', ok);
el.querySelector('span').textContent = ok ? (variante === 'zeilen' ? 'reading-flow aktiv' : 'reading-order aktiv') : 'nicht unterstützt';
const h = $('[data-hinweis]');
if (matchMedia('(max-width: 760px)').matches) h.innerHTML = '<span><b>Telefon:</b> eine Spalte in HTML-Reihenfolge. Hier läuft nichts auseinander, reading-flow ist nicht nötig.</span>';
else if (modus() === 'vorher') h.innerHTML = '<span><b>Vorher:</b> frei platziert, Tab folgt dem HTML. Der Fokus springt quer durchs Raster.</span>';
else if (ok) h.innerHTML = variante === 'zeilen'
? '<span><b>Nachher:</b> reading-flow: grid-rows. Tab und Screenreader lesen Zeile für Zeile.</span>'
: '<span><b>Nachher:</b> reading-flow: source-order, das Highlight mit reading-order: -1 zuerst, der Rest in HTML-Reihenfolge.</span>';
else h.innerHTML = '<span><b>Fallback:</b> Dieser Browser kennt reading-flow nicht. Das Raster bleibt in HTML-Reihenfolge, Bild und Fokus passen trotzdem zusammen.</span>';
}
function aktualisieren() { raster.dataset.modus = modus(); raster.dataset.var = variante; stand(); requestAnimationFrame(zeichne); }
$$('[name="modus"]').forEach(r => r.addEventListener('change', aktualisieren));
knopf.addEventListener('click', () => { const an = knopf.getAttribute('aria-pressed') !== 'true'; knopf.setAttribute('aria-pressed', String(an)); raster.classList.toggle('zeigen', an); zeichne(); });
kacheln.forEach(k => k.addEventListener('click', e => e.preventDefault()));
raster.addEventListener('focusin', e => {
const k = e.target.closest('.k'); if (!k) return;
nummern.forEach(s => s.classList.toggle('an', s.dataset.fuer === k.dataset.name));
});
raster.addEventListener('focusout', () => nummern.forEach(s => s.classList.remove('an')));
addEventListener('resize', () => { stand(); zeichne(); });
// Varianten (?v=zeilen | ?v=reihenfolge)
const WERTE = {
gemeinsam: '<span>Kurve <b>cubic-bezier(0.16, 1, 0.3, 1)</b></span><span>Pfad <b>1200 ms</b></span><span>Nummern <b>400 ms</b> · Versatz 60 ms</span><span>Fokus-Rahmen <b>3 px</b> #FF5A1F · 5 px Abstand</span>',
zeilen: '<span>reading-flow: <b>grid-rows</b></span>',
reihenfolge: '<span>reading-flow: <b>source-order</b> · reading-order: <b>-1</b></span>'
};
const schalter = $$('[data-v]');
function setzeVariante(v, url) {
variante = v === 'reihenfolge' ? 'reihenfolge' : 'zeilen';
schalter.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === variante)));
$('[data-werte]').innerHTML = WERTE.gemeinsam + WERTE[variante];
if (url) { const q = new URLSearchParams(location.search); q.set('v', variante); history.replaceState(null, '', '?' + q.toString()); }
aktualisieren();
}
schalter.forEach(b => b.addEventListener('click', () => { const lief = html.classList.contains('vorfuehrung'); neustart(); setzeVariante(b.dataset.v, true); if (lief) vorfuehren(); }));
// ---------- Vorführung: vorher zeichnen, durchtabben, nachher zeichnen, durchtabben ----------
let zeit = [], laeuft = false;
const nach = (ms, f) => zeit.push(setTimeout(f, ms));
function setzeModus(m) { $(`[name="modus"][value="${m}"]`).checked = true; aktualisieren(); }
function wandern(t0) {
const liste = reihenfolge();
liste.forEach((k, i) => nach(t0 + i * 520, () => {
$$('.fokus-sim').forEach(x => x.classList.remove('fokus-sim')); k.classList.add('fokus-sim');
nummern.forEach(s => s.classList.toggle('an', s.dataset.fuer === k.dataset.name));
}));
nach(t0 + liste.length * 520, () => { $$('.fokus-sim').forEach(x => x.classList.remove('fokus-sim')); nummern.forEach(s => s.classList.remove('an')); });
return t0 + liste.length * 520 + 200;
}
function runde() {
knopf.setAttribute('aria-pressed', 'true'); raster.classList.add('zeigen');
setzeModus('vorher');
let t = wandern(1300);
nach(t + 500, () => setzeModus('nachher'));
t = wandern(t + 1800);
nach(t + 1200, runde);
}
function vorfuehren() { neustart(); html.classList.add('vorfuehrung'); laeuft = true; runde(); }
function pause() { zeit.forEach(clearTimeout); zeit = []; laeuft = false; }
function neustart() { pause(); html.classList.remove('vorfuehrung'); $$('.fokus-sim').forEach(x => x.classList.remove('fokus-sim')); knopf.setAttribute('aria-pressed', 'false'); raster.classList.remove('zeigen'); setzeModus('nachher'); }
function weiter() { if (!laeuft && html.classList.contains('vorfuehrung')) { laeuft = true; runde(); } }
setzeVariante(new URLSearchParams(location.search).get('v'), false);
document.fonts.ready.then(zeichne);
})();
</script>
</body>
</html>