Das Karussell ist die Komponente, die in jedem Projekt noch einmal von vorn gebaut wird. Mal kommt eine Bibliothek mit dreißig Kilobyte mit, mal sind es zweihundert eigene Zeilen, die Breiten messen, einen Index verwalten und bei jedem Umbruch neu rechnen. Dabei kann der Browser den schwierigen Teil längst selbst: Wischen, Trägheit, Einrasten, Richtung im Textfluss. Das gibt es seit 2019 in jedem großen Browser und heißt scroll-snap. Seit Chrome 135 kommt dazu, was bisher den eigenen Code gerechtfertigt hat, nämlich die Punkte unter der Reihe und die Pfeile daneben, erzeugt vom Browser und bedienbar mit der Tastatur. Dieser Beitrag baut das Karussell in zwei Schichten: unten der Teil, der heute schon überall funktioniert, darüber der Teil, der in Chromium dazukommt und anderswo einfach fehlt, ohne dass etwas kaputtgeht.
- Der Unterbau ist ein Scroll-Container, kein Widget. overflow-x auf auto, scroll-snap-type auf der Reihe, scroll-snap-align auf den Kindern: Damit ist das Wischen fertig, inklusive Trägheit und Einrasten.
- scroll-snap-stop mit always verhindert, dass ein kräftiger Wisch über drei Elemente hinwegfliegt. Ohne diese Zeile fühlt sich ein Karussell auf dem Telefon unkontrolliert an.
- scroll-marker-group und scroll-button erzeugen Punkte und Pfeile ohne Markup. Der Browser gibt ihnen Knopf-Semantik, Pfeiltasten und einen Zustand, der von selbst mitläuft.
- Diese zweite Schicht gibt es bisher nur in Chromium. Weil sie nichts ersetzt, sondern etwas hinzufügt, braucht sie keine Rückfallebene, sondern nur eine Reihenfolge: zuerst die Fassung, die überall trägt, danach die Zugabe.
Warum wird das Karussell immer wieder neu gebaut?
Ein Karussell besteht aus vier Dingen: einer Reihe, die sich seitlich bewegen lässt, einem Einrasten an den Elementen, einer Anzeige, an welcher Stelle man gerade ist, und zwei Knöpfen für vor und zurück. Nur das dritte und das vierte Ding sind wirklich Arbeit. Trotzdem landet in den meisten Projekten eine Lösung im Paket, die alle vier mitbringt, und damit auch das Wischen selbst nachbaut.
Das ist der eigentliche Aufwand, und er fällt an einer Stelle an, an der niemand ihn sehen will. Wer Bewegung in JavaScript nachbaut, übernimmt Trägheit, Abbremsen, das Verhalten beim Loslassen mitten in der Bewegung, die Richtung bei rechtsläufiger Schrift, das Verhalten beim Zoomen und den Umgang mit einem Trackpad, das anders beschleunigt als ein Finger. Jeder dieser Punkte ist für sich klein und zusammen sind sie der Grund, warum eigene Karussells sich immer eine Spur falsch anfühlen.
Der Browser hat das alles bereits, und zwar nicht als Zusatz, sondern als seine Hauptaufgabe. Ein Element mit zu viel Inhalt ist ein Scroll-Container, und ein Scroll-Container scrollt richtig. Die einzige Angabe, die fehlt, ist die, wo er zur Ruhe kommen soll. Genau diese Angabe macht scroll-snap, und damit ist der schwierige Teil erledigt, bevor er angefangen hat.
Ein Karussell ist kein Widget. Es ist eine Reihe, die scrollt, mit zwei Angaben dazu.
Was macht aus einer Reihe einen Scroll-Container?
Drei Angaben genügen. Die Reihe bekommt overflow-x mit auto, damit sie überhaupt scrollt. Sie bekommt scroll-snap-type mit der Achse und der Strenge, meist x mandatory, damit der Browser nach dem Loslassen auf einen Punkt zusteuert. Und die Kinder bekommen scroll-snap-align, das festlegt, welche ihrer Kanten am Container ausgerichtet wird. Mit center liegt das aktuelle Element in der Mitte, mit start an der linken Kante.
Zwei weitere Zeilen entscheiden darüber, ob sich das Ergebnis gut anfühlt. scroll-snap-stop mit always sorgt dafür, dass jedes Element angesteuert wird und ein kräftiger Wisch nicht über drei hinwegfliegt. Und scroll-padding hält an den Kanten des Containers einen Streifen frei, sodass das angesteuerte Element nicht bündig an der Kante klebt. Beides fehlt in den meisten Beispielen und beides merkt man sofort, wenn es fehlt.
Die Breite der Elemente gehört zur selben Entscheidung. Eine feste Breite macht aus dem Karussell eine Reihe gleich großer Karten, ein Wert wie min(28rem, 85%) sorgt dafür, dass auf dem Telefon ein Stück des nächsten Elements sichtbar bleibt. Dieses sichtbare Stück ist nicht Dekoration, es ist die einzige Stelle, an der die Oberfläche von selbst verrät, dass es weitergeht. Wer es weglässt, braucht die Pfeile aus dem nächsten Abschnitt doppelt so dringend.
<section className=" px-6"> <div className="mx-auto flex flex-col "> <p>Kicker</p> <h1>Überschrift</h1> <p>Zwei Sätze</p> <div className="flex "> <a>Termin buchen</a> <a>Projekte</a> </div> </div></section>
Zeig auf eine markierte Klasse, dann siehst du ihre Stelle in der Vorschau.
Drei Angaben auf der Reihe, zwei auf den Kindern. Das Wischen selbst steuert niemand.
.carousel { display: flex; gap: 1rem; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: 1.5rem;} .carousel > li { flex: 0 0 min(28rem, 85%); scroll-snap-align: center; scroll-snap-stop: always;}
Woher kommen Punkte und Pfeile ohne Markup?
Die Punkte unter einem Karussell sind bisher Handarbeit: ein Element pro Bild, ein Klick-Listener pro Element, eine Klasse, die den aktuellen Stand abbildet, und eine Berechnung, die diesen Stand beim Scrollen nachführt. Genau diese vier Dinge nimmt der Browser jetzt ab. Die Reihe bekommt scroll-marker-group mit before oder after, und damit entsteht eine Fläche, in der die Marker liegen. Jedes Kind kann ein ::scroll-marker erzeugen, sobald es ein content bekommt, und zwar auch das leere, also content mit zwei Anführungszeichen.
Der aktuelle Stand läuft von selbst mit. Die Pseudo-Klasse :target-current trifft immer genau den Marker, dessen Element gerade im Bild ist, und das ohne eine Zeile, die beim Scrollen rechnet. Ein Klick auf einen Marker springt zum zugehörigen Element, auch das ohne Zutun. Was man dort noch schreibt, ist reine Gestaltung: Größe, Form, Abstand und der Unterschied zwischen aktiv und nicht aktiv.
Die Pfeile funktionieren nach demselben Muster. ::scroll-button nimmt eine Richtung als Argument, also left, right, up und down oder die Entsprechungen im Textfluss, und erzeugt bei einem content einen echten Knopf. Ein Druck darauf scrollt um etwa eine Containerbreite weiter, bei gesetztem Einrasten also zum nächsten Element. Und sobald in einer Richtung nichts mehr kommt, schaltet der Browser den Knopf von selbst ab, abfragbar mit :disabled. Das ist die Zeile, die man in eigenen Lösungen am häufigsten vergisst.
.carousel { scroll-marker-group: after; } .carousel::scroll-marker-group { display: flex; justify-content: center; gap: 0.5rem;} .carousel > li::scroll-marker { content: ''; inline-size: 0.5rem; block-size: 0.5rem; border: 1px solid currentColor; border-radius: 50%;} .carousel > li::scroll-marker:target-current { background: currentColor;} .carousel::scroll-button(left) { content: '\2039'; }.carousel::scroll-button(right) { content: '\203A'; } .carousel::scroll-button(*):disabled { opacity: 0.3; cursor: default;}
Der Zustand wird nicht gepflegt, sondern abgefragt. Das ist der ganze Unterschied.
Gute Heroes nehmen Entscheidungen ab.
Drei Anordnungen, die tragen. Welche passt, entscheidet das Bildmaterial, nicht der Geschmack.
Was hat die Tastatur davon?
Mehr, als die meisten eigenen Karussells liefern. Sobald eine Marker-Gruppe vorhanden ist, bekommt die Reihe die Semantik einer Registerkartenleiste: Die Gruppe ist ein Tabstopp, und innerhalb der Gruppe wechselt man mit den Pfeiltasten von Element zu Element. Marker und Knöpfe sind dabei keine gestylten Divs, sondern Knöpfe mit allem, was dazugehört, und der Browser vergibt die zugänglichen Namen selbst. Das Zeichen im content braucht deshalb keine Beschriftung daneben.
Wo before und after stehen, ist dabei keine Geschmacksfrage. Der Wert bestimmt nicht nur die Lage im Layout, sondern auch die Stelle in der Reihenfolge der Tabstopps. Eine Gruppe, die unter der Reihe sitzt, aber vor ihr angesprungen wird, führt genau zu dem Sprung, den man bei fremden Seiten als Unordnung empfindet. Setz den Wert deshalb nach dem, was man sieht.
Bleibt der Scroll-Container selbst. Chromium gibt einem Scroll-Container seit Version 132 einen Tabstopp, aber nur dann, wenn darin nichts anderes fokussierbar ist. Darauf lässt sich nicht bauen, denn die anderen Browser handhaben das unterschiedlich. Die verlässliche Regel ist deshalb einfach: Enthalten die Elemente ohnehin Links oder Knöpfe, erreicht die Tastatur sie auf dem normalen Weg, und das Scrollen folgt dem Fokus. Enthalten sie nur Bilder oder Text, gib der Reihe selbst ein tabindex mit null und einen Namen, damit sie überhaupt erreichbar ist und angesagt wird.
Welcher Teil läuft heute wo?
Die beiden Schichten sind unterschiedlich weit. Das Einrasten ist alt und überall vorhanden: scroll-snap-type und scroll-snap-align gibt es in Chrome seit Version 69, in Safari seit Version 11 und in Firefox seit Version 68, scroll-snap-stop ist in Firefox seit Version 103 dabei. Diese Schicht braucht keine Abfrage, sie ist Alltag.
Die Marker und die Knöpfe sind neu. ::scroll-marker, ::scroll-marker-group, ::scroll-button und :target-current kamen zusammen in Chrome 135 und damit auch in Edge. In Safari und in Firefox fehlen sie bisher. Das ist für die Entscheidung wichtiger, als es klingt: Es geht nicht um einen Rückfall, sondern um eine Reihenfolge. Was der Browser nicht kennt, erzeugt er nicht, und übrig bleibt die untere Schicht, die ohnehin vollständig ist.
Eine Abfrage braucht trotzdem genau eine Stelle, nämlich die, an der du etwas wegnimmst. Das Verstecken der Scrollleiste gehört in einen Block, den nur der Browser liest, der auch Marker erzeugt, und dafür reicht @supports mit selector und dem Namen des Pseudo-Elements. Wer die Marker auch dort möchte, wo es sie noch nicht gibt, hat seit Chrome 140 eine zweite Möglichkeit: scroll-target-group mit auto macht aus einem vorhandenen Element voller Sprungmarken eine Markerleiste und färbt die aktuelle mit :target-current. Die Links darin sind echte Links und funktionieren in jedem Browser, nur das Mitlaufen des Zustands bleibt vorerst Chromium vorbehalten.
/* Everywhere: the scrollbar stays as the indicator */.carousel { scrollbar-width: auto; } @supports selector(::scroll-marker) { .carousel { scrollbar-width: none; }}
- Die Scrollleiste pauschal ausblenden und die Bedienung den Markern überlassen. In Safari und Firefox bleibt dann eine Reihe ohne jeden Hinweis, dass sie weitergeht.
- Die Marker in JavaScript nachbauen, wo sie fehlen. Damit ist die Bibliothek wieder im Paket, nur mit zwei Fassungen statt einer.
- Das Karussell daran knüpfen, dass die Marker da sind, etwa indem die Elemente erst durch einen Marker erreichbar werden.
- Auf @supports verzichten, weil ja nichts kaputtgeht. Das stimmt für alles, was hinzukommt, und nicht für das, was du wegnimmst.
- Die Reihe so bauen, dass sie mit Scrollen allein vollständig bedienbar ist, inklusive sichtbarer Leiste und einem Stück des nächsten Elements.
- Marker und Knöpfe darüberlegen, ohne dass sie etwas ersetzen. Wo es sie gibt, sind sie bequemer, wo nicht, fehlt nur Bequemlichkeit.
- Genau eine Abfrage schreiben, und zwar um das Verstecken der Scrollleiste herum.
- Die Fassung ohne Marker einmal wirklich ansehen, am besten in Firefox. Das ist der Zustand, den ein Teil der Besucher sieht.
Wo hört CSS auf?
Vier Dinge bleiben außerhalb, und sie sind leicht zu merken, weil sie alle dasselbe gemeinsam haben: Bei allen vieren bewegt sich etwas, ohne dass jemand es bewegt hat.
- Der eigene Lauf. Ein Karussell, das alle fünf Sekunden weiterspringt, braucht einen Zeitgeber und damit Code. Falls es sein muss: Halte bei Zeigen und bei Fokus an, und prüfe prefers-reduced-motion, bevor der Zeitgeber überhaupt startet.
- Der Ring ohne Ende. Nach dem letzten Element wieder beim ersten anzufangen, ist in CSS nicht vorgesehen. Die Knöpfe schalten stattdessen am Rand ab, und das ist kein Mangel: Ein abgeschalteter Knopf sagt, dass hier Schluss ist, ein Sprung an den Anfang sagt nichts.
- Das Nachladen. Elemente, die erst beim Erscheinen geladen werden, brauchen einen Beobachter. Für Bilder reicht allerdings oft schon loading mit lazy, und dann bleibt auch dieser Punkt ohne eigenen Code.
- Das weiche Springen. scroll-behavior mit smooth lässt einen Sprung laufen statt springen. Die Angabe gehört in eine Abfrage auf prefers-reduced-motion, denn eine quer laufende Reihe ist genau die Art Bewegung, die manche Menschen vermeiden möchten.
Bleibt die Frage, die sich am Ende jedes Karussells stellt und die nichts mit CSS zu tun hat: Gehört der Inhalt überhaupt hinter eine Bewegung? Was an zweiter Stelle liegt, wird selten gesehen, und das gilt unabhängig davon, wie gut das Einrasten funktioniert. Für acht gleichrangige Produktbilder ist das in Ordnung, für zwei Aussagen, die beide wichtig sind, nicht.
Wenn der Inhalt dort hingehört, ist die Bilanz dieses Ansatzes allerdings klar: Was früher eine Abhängigkeit war, sind jetzt zwanzig Zeilen CSS, die niemand aktualisieren muss. Und der Teil, der heute noch fehlt, fehlt auf die angenehmste Art, nämlich ohne Spuren.
Alles, was von selbst läuft, braucht Code. Alles, was jemand bewegt, nicht mehr.




