Einleitung
Layout Breakouts mit CSS Grid sind aktuell sehr populär. Sie ermöglichen die Positionierung von Inhalten auf voller Viewport-Breite, in einem Layout mit beschränkter Maximalbreite.

Vor CSS Grid gab es verschiedene Lösungen, um so ein Layout zu realisieren. Hier musste man aber aufpassen, dass die Scrollbars der Browser berücksichtigt werden.
Einige Lösungen für dieses Seiten-Layout mit CSS Grid findet ihr hier:
- www.joshwcomeau.com/
css/full-bleed/ - ryanmulligan.dev/blog/
layout-breakouts/ - smolcss.dev/#smol-breakout-grid
- layout-breakouts-builder.
vercel.app/
Was mir bei meiner Recherche aufgefallen war: Es gibt zwar Layouts mit mehreren Tracks (z.B. normaler Inhaltsbereich, etwas breitere Feature-Spalte sowie Full-Bleed). Aber alle Tracks sind einspaltig im Layout zentriert.
Daher habe ich mir die Frage gestellt: Lässt sich ein klassisches Gestaltungsraster, etwa mit 12 Spalten, mit Layout Breakouts kombinieren?

In meinem aktuellen Website-Projekt habe ich erstmals versucht, dieses Layout umzusetzen. Und tatsächlich, es ist möglich! Das finale Grid bietet viel Flexibilität, hat allerdings auch seine Eigenheiten.
In diesem Tutorial zeige ich euch erst die Basis-Version, um das grundlegende Konzept sowie die gefundenen Stolpersteine zu erläutern. Danach folgt die finale Fassung.
Begrenzung der Maximalbreite
In vielen Websites wird das Seitenlayout – und damit der Inhalt – auf eine bestimmte Breite begrenzt und im Browser zentriert. Das möchten wir ebenfalls erreichen.
Üblicherweise wird der Inhaltsbereich dazu in ein Wrapper- oder Container-Element gesetzt, das eine max-width sowie margin-inline: auto erhält.
Bei einem Layout Breakout per CSS Grid müssen allerdings alle Spalten Teil des selben Grid-Elternelements bleiben. Daher können wir kein Wrapper-Element verwenden.
Wir benötigen also einen anderen Weg, um das (aus mehreren Grid-Spalten bestehende) Seitenlayout auf eine maximale Breite zu beschränken.
Responsive Spaltenzahl
Die Anzahl der Spalten sollten wir sinnvollerweise von der Viewport-Breite abhängig machen. 12 Spalten auf Desktop sind praktisch, auf Mobilgeräten sind das zu viele. Vor allem: Die Breite der Spalten-Abstände (column-gap) bleibt immer vorhanden. Bei 12 Spalten sind das 11 Abstände à 24px (1.5rem), also 264px Breite allein für die Spaltenzwischenräume. Je nach Viewport kann es dann passieren, dass das Grid zu breit wird.
Also wird unser Grid je nach Viewport 6, 8 oder 12 Spalten verwenden. Die Änderung der Spaltenzahl müssen wir bei der Positionierung der Inhalte beachten. Aber keine Sorge, die finale Version des Grids hat dafür eine praktikable Lösung.
Die hier verwendeten Breakpoints, Spaltenzahl und -abstände sind allesamt beispielhaft und können nach Belieben an euer Projekt angepasst werden.
Die Basis-Version
Wie begrenzen wir das Seitenlayout also auf eine Maximalbreite? Mit Mathe! 🤓
Okay, ich bin wirklich nicht der große Mathematiker. Aber wenn wir die Breite nicht per Wrapper beschränken können, gibt es einen anderen Lösungsweg: Wir ermitteln, welche Maximalbreite eine einzelne Spalte besitzen darf. Die Breite der Zwischenräume (column-gap) kennen wir bereits, im Beispiel sind das 1.5rem.
Gehen wir also von der gewünschten Maximalbreite aus (hier: 1400px). Davon müssen wir zuerst die Abstände zwischen den Spalten abziehen (Spaltenzahl minus 1). Und das Ergebnis anschließend durch die Spaltenzahl teilen. Voilà! Wir haben die maximale Breite jeder Spalte.
// Formel: calc((MAXIMALBREITE – ((SPALTENZAHL - 1) * GAP-BREITE)) / SPALTENZAHL)
$spaltenbreite: calc((1400px – ((12 - 1) * 1.5rem)) / 12)Die Berechnung können wir jetzt im Grid mittels minmax(0, $spaltenbreite) verwenden. So kann sich das Grid weiter responsiv dem Viewport anpassen.
Natürlich möchten wir die vollständige Berechnung nicht vielfach in grid-template-columns wiederholen und dann auch noch an die aktuelle Spaltenzahl je Viewport anpassen müssen. Also setzen wir alle Werte über CSS Custom Properties:
:root {
--sk-grid-padding: 4%;
--sk-grid-max-width: 1400px;
--sk-grid-gap: 1.5rem;
--sk-grid-cols: 6;
--sk-grid-col-size: calc((var(--sk-grid-max-width) - (var(--sk-grid-cols) - 1) * var(--sk-grid-gap)) / var(--sk-grid-cols));
@media screen and (min-width: 768px) {
--sk-grid-gap: 2rem;
--sk-grid-cols: 8;
}
@media screen and (min-width: 1024px) {
--sk-grid-cols: 12;
}
}
.c-page-grid {
display: grid;
column-gap: var(--sk-grid-gap);
grid-template-columns:
[full-start]
minmax(var(--sk-grid-padding), 1fr)
[content-start]
repeat(var(--sk-grid-cols), minmax(0, var(--sk-grid-col-size)))
[content-end]
minmax(var(--sk-grid-padding), 1fr)
[full-end];
}Das sieht doch schon ganz gut aus. Und wir können sowohl Spaltenzahl als auch -abstände je Viewport einfach konfigurieren.
Die beiden äußeren Grid-Spalten (für das Layout Breakout) erhalten in unserem Beispiel eine Minimalbreite von 4% und eine Maximalbreite von 1fr. Dadurch besitzt das innere Raster immer einen Abstand zum Viewport und wird auf der Seite zentriert.
Mit grid-column: content begrenzen wir jetzt alle Inhalte erst einmal auf das Seitenlayout:
.c-page-grid {
> * {
// Kurzform für "grid-column: content-start / content-end";
grid-column: content;
}
}Das ist natürlich noch langweilig, dazu brauchen wir auch kein mehrspaltiges Raster. Das folgende CodePen zeigt daher ein etwas interessanteres Layout, bei dem Überschriften und die Einleitung anders positioniert werden als der restliche Text. Außerdem gibt es zwei Beispiele für Layout Breakouts: Diese Vorversion auf CodePen
Nachteile der Basis-Version
Die aktuelle Lösung hat noch zwei Probleme, die wir im nächsten Schritt lösen werden:
- Die CSS-Eigenschaft column-gap ergänzt einen einheitlichen Abstand zwischen allen Spalten, also auch bei den beiden äußeren Spalten. Das sorgt auf kleineren Viewports für mehr Abstand, als wir es uns wünschen.
- Es gibt aktuell nur die beiden Grid-Tracks "full" und "content". Sobald wir Inhalte an den anderen verfügbaren Spalten im Rastersystem ausrichten möchten, müssen wir das über die Liniennummer (grid-column: 3 / span 6;) festlegen. Und in einigen Fällen möglicherweise per Viewport anpassen, wenn sich die Spaltenzahl ändert.
Die vollständige, funktionierende Lösung
Achtung, jetzt wird es umfangreich.
- Wir erweitern die benannten Grid-Lines um Tracks wie "heading" und "text" zur einfachen Positionierung. In Media Queries richten wir die jeweils gewünschte Position ein.
- Und, der Schritt mag auf den ersten Blick überraschen: Wir ersetzen die Eigenschaft column-gap durch echte Spalten im Grid. Das erlaubt es uns, die Abstände zu den äußeren Spalten zu entfernen.
Beide Anpassungen erläutere ich nachfolgend noch etwas näher.
:root {
--sk-grid-padding: 4%;
--sk-grid-max-width: 1400px;
--sk-grid-gap: 1.5rem;
--sk-grid-cols: 6;
--sk-grid-col-size: calc((var(--sk-grid-max-width) - (var(--sk-grid-cols) - 1) * var(--sk-grid-gap)) / var(--sk-grid-cols));
@media screen and (min-width: 768px) {
--sk-grid-gap: 2rem;
--sk-grid-cols: 8;
}
@media screen and (min-width: 1024px) {
--sk-grid-cols: 12;
}
}
.c-page-grid {
display: grid;
grid-template-columns:
[full-start] minmax(var(--sk-grid-padding), 1fr)
[col-1-start content-start heading-start] minmax(0, var(--sk-grid-col-size)) [col-1-end]
var(--sk-grid-gap)
[col-2-start text-start] minmax(0, var(--sk-grid-col-size)) [col-2-end]
var(--sk-grid-gap)
[col-3-start] minmax(0, var(--sk-grid-col-size)) [col-3-end]
var(--sk-grid-gap)
[col-4-start] minmax(0, var(--sk-grid-col-size)) [col-4-end]
var(--sk-grid-gap)
[col-5-start] minmax(0, var(--sk-grid-col-size)) [col-5-end]
var(--sk-grid-gap)
[col-6-start] minmax(0, var(--sk-grid-col-size)) [col-6-end content-end heading-end text-end]
minmax(var(--sk-grid-padding), 1fr) [full-end];
@media screen and (min-width: 768px) {
grid-template-columns:
[full-start] minmax(var(--sk-grid-padding), 1fr)
[col-1-start content-start heading-start] minmax(0, var(--sk-grid-col-size)) [col-1-end]
var(--sk-grid-gap)
[col-2-start text-start] minmax(0, var(--sk-grid-col-size)) [col-2-end]
var(--sk-grid-gap)
[col-3-start] minmax(0, var(--sk-grid-col-size)) [col-3-end]
var(--sk-grid-gap)
[col-4-start] minmax(0, var(--sk-grid-col-size)) [col-4-end]
var(--sk-grid-gap)
[col-5-start] minmax(0, var(--sk-grid-col-size)) [col-5-end]
var(--sk-grid-gap)
[col-6-start] minmax(0, var(--sk-grid-col-size)) [col-6-end]
var(--sk-grid-gap)
[col-7-start] minmax(0, var(--sk-grid-col-size)) [col-7-end text-end]
var(--sk-grid-gap)
[col-8] minmax(0, var(--sk-grid-col-size)) [col-8-end content-end heading-end]
minmax(var(--sk-grid-padding), 1fr) [full-end];
}
@media screen and (min-width: 1024px) {
grid-template-columns:
[full-start] minmax(var(--sk-grid-padding), 1fr)
[col-1-start content-start] minmax(0, var(--sk-grid-col-size)) [col-1-end]
var(--sk-grid-gap)
[col-2-start] minmax(0, var(--sk-grid-col-size)) [col-2-end]
var(--sk-grid-gap)
[col-3-start heading-start] minmax(0, var(--sk-grid-col-size)) [col-3-end]
var(--sk-grid-gap)
[col-4-start text-start] minmax(0, var(--sk-grid-col-size)) [col-4-end]
var(--sk-grid-gap)
[col-5-start] minmax(0, var(--sk-grid-col-size)) [col-5-end]
var(--sk-grid-gap)
[col-6-start] minmax(0, var(--sk-grid-col-size)) [col-6-end]
var(--sk-grid-gap)
[col-7-start] minmax(0, var(--sk-grid-col-size)) [col-7-end]
var(--sk-grid-gap)
[col-8-start] minmax(0, var(--sk-grid-col-size)) [col-8-end]
var(--sk-grid-gap)
[col-9-start] minmax(0, var(--sk-grid-col-size)) [col-9-end text-end]
var(--sk-grid-gap)
[col-10-start] minmax(0, var(--sk-grid-col-size)) [col-10-end]
var(--sk-grid-gap)
[col-11-start] minmax(0, var(--sk-grid-col-size)) [col-11-end]
var(--sk-grid-gap)
[col-12-start] minmax(0, var(--sk-grid-col-size)) [col-12-end content-end heading-end]
minmax(var(--sk-grid-padding), 1fr) [full-end];
}
}In diesem CodePen wird das fertige Grid mit verschiedenen Inhalten angewendet: Finales Grid-Layout auf CodePen
Wer sich noch einen Zwischenschritt ansehen mag – hier wurden erste Grid-Lines ergänzt, aber die Umstellung von column-gap ist noch nicht erfolgt: Zwischenschritt auf CodePen
Das "column-gap"-Dilemma
Lasst uns zuerst die Spaltenabstände besprechen. column-gap war, wie oben gezeigt, problematisch.
Ich habe mich daher dazu entschieden, die Abstände mit echten Grid-Spalten abzubilden.
- Vorteil: Der zusätzliche Abstand zu den äußeren Spalten ist entfallen
- Nachteil: Die Verwendung des Schlüsselworts span (z.B. grid-column: content-start / span 4;) ist ungewohnt, da jetzt die "Gap-Spalten" mitgezählt werden müssen
Meiner Ansicht nach überwiegt hier der Vorteil. Ein Auto-Placement mit span direkt in diesem Grid wäre generell schwierig, da die äußeren, flexiblen Spalten mit berücksichtigt würden. Aber über ein separates Grid in einem Kindelement lässt sich das weiterhin umsetzen, wie die “Card Group”-Beispiele im CodePen zeigen.
Die neuen benannten Grid-Lines
Zugegeben, die Einrichtung des Grids in grid-template-columns ist deutlich umfangreicher geworden und benötigt jetzt Media Queries. Das hat zwei Gründe: Durch die neuen “Gap-Spalten” müssen wir auf die repeat()-Funktion verzichten. Wir machen uns das aber zum Vorteil, indem wir benannte Grid-Lines für jede Spalte setzen.
Die benannten Grid-Lines erlauben uns eine einfachere Positionierung der verschiedenen Inhalte. Diese könnt ihr je Viewport nach Belieben anders im Grid positionieren und auch mehrere Tracks an der selben Spalte beginnen oder enden lassen.
Für jeden Track setzen wir einen Start- und Endpunkt, also beispielsweise [text-start] und [text-end].
Und noch etwas:
[col-1-start content-start] minmax(0, var(--sk-grid-col-size)) [col-1-end]Die Ergänzung von [col-N-start] und [col-N-end] in jeder "echten" Inhaltsspalte hebt diese in grid-template-columns nicht nur hervor, sodass ihr sie einfacher von den "Gap-Spalten" unterscheiden könnt.
Ihr könnt sie vor allem dazu verwenden, um ein weiteres Problem zu umgehen: Die Zählung der Liniennummern im Grid beginnt ganz links. Durch das Layout Breakout also bei der äußeren, flexiblen Spalte.
Die Liniennummer 2 entspricht dem Beginn des inneren Rasters. Indem wir sie als [col-1-start] definieren, erhalten wir eine besser nachvollziehbare Zählung im Grid.
Ein Element mit grid-column: col-2 / col-5 wird automatisch bei [col-2-start] beginnen und bei [col-5-end] enden.

Möglichkeiten mit Subgrid
Subgrid gehört zur Baseline 2023; alle seit September 2023 erschienenen Browserversionen unterstützen das CSS-Feature.
Über grid-template-columns: subgrid können wir das Grid zu Kindelementen vererben. Die CodePen-Demo zeigt das an zwei Beispielen:
Die blaue Teaser-Box läuft rechts bis an den Rand des Viewports. Dessen Inhalt ist durch Subgrid weiterhin auf den "text"-Track begrenzt.
.c-teaser-box {
background: #bdcee5;
grid-column: text-start / full-end;
display: grid;
grid-template-columns: subgrid;
margin-bottom: 2rem;
}
.c-teaser-box__content {
padding: var(--sk-grid-gap);
padding-right: 0;
grid-column: text-start / text-end;
> *:last-child {
margin-bottom: 0;
}
}<div class="c-teaser-box">
<div class="c-teaser-box__content">
<p>Some content.</p>
</div>
</div>Das Full-Bleed-Image läuft über die gesamte Viewport-Breite. Der Inhalt der <figcaption> wird von uns aber im "text"-Track ausgerichtet. Wie wir sehen, kann Subgrid dazu auch mehrfach vererbt werden:
.c-full-bleed--image {
margin-block: 2rem;
grid-template-columns: subgrid;
display: grid;
grid-column: full;
img {
height: calc(30rem + 16vw);
max-height: 80vh;
object-fit: cover;
width: 100%;
grid-column: full;
}
figcaption {
font-size: .8em;
margin-top: 1rem;
display: grid;
grid-template-columns: subgrid;
grid-column: text;
}
.c-figcaption-content {
grid-column: text;
}
}<figure class="c-full-bleed c-full-bleed--image">
<img src="https://picsum.photos/id/379/2400/900" alt="Photo of an empty asphalt road through mountains">
<figcaption>
<div class="c-figcaption-content">
Photo by <a href="https://unsplash.com/photos/empty-asphalt-road-through-mountain-2f5Ktwb8YXk" target="_blank">
Kamil Lehmann</a> on <a href="https://unsplash.com/" target="_blank">Unsplash</a>
</div>
</figcaption>
</figure>Vorteile und Eigenschaften des Grids
- Konfigurierbare Maximalbreite des inneren Seitenlayouts
- Flexible Spalten links/rechts für randlose Positionierung (Full Bleed)
- Grid-Zeilennamen (Tracks) für häufig verwendete Positionen (z.B. "Heading", "Content").
- Nummerierte Zeilennamen für spezielle Anwendungsfälle
- Responsive Spalten (z.B. Mobil: 6, Tablet: 8, Desktop: 12)
Zu beachten
Gilt speziell für diese Grid-Lösung: Da Spaltenabstände über echte Spalten abgebildet werden, müssen diese bei der Verwendung des Schlüsselworts span (grid-column: content-start / span 4;) mitgezählt werden.
Gilt für alle Grid-Lösungen, bei denen sich die Spaltenzahl je Viewport ändert: Bei der Verwendung linienbasierter Platzierung (grid-column: 6 / 9; oder auch grid-column: col-6 / col-9;) müssen die Grid-Inhalte ggf. (mittels Media Queries) neu positioniert werden. Eine neunte Spalte gibt es (im hier gezeigten Beispiel) erst ab 1024px Breite.
Hier hilft es, Inhalte möglichst an selbst definierten Grid-Lines wie z.B. “text” auszurichten.
Fazit
Das ist sicher kein Grid-Layout, das für jedes Projekt passt. Aber bei einem etwas außergewöhnlichem Seitenlayout ist dieses Grid möglicherweise hilfreich. Und es hat Spaß gemacht, diese Lösung zu erstellen.
In einem folgenden, zweiten Teil werde ich in diesem Grid noch Komponenten ergänzen, bei denen Bilder von Text umflossen werden können. Ziel ist es natürlich, dass sich Text und Bild weiterhin am Grid ausrichten. float kann nicht direkt in einem CSS Grid angewendet werden, also wird wieder etwas getrickst.
Wenn ich Zeit finde, erstelle ich auf CodePen noch einige ausgefallenere Layouts mit dem Grid.
Noch etwas: Je nach Layout muss ein CSS Grid nicht so kompliziert werden. Mit den zu Beginn verlinkten "Layout Breakout"-Lösungen kommt ihr schon recht weit. Ihr könntet Inhalte dort von [full-start] zu [content-end] positionieren, um ein asymmetrisches Layout auf einfacherem Wege zu erhalten.
Ich freue mich sehr über Kritik! Habe ich an einer Stelle zu kompliziert gedacht? Sehr ihr für euch einen Anwendungsfall? Habt ihr eine bessere Lösung für das column-gap-Problem? Schreibt mir auf Mastodon oder sendet mir eine E-Mail!



