Responsive Design
Kurz gesagt: Responsive Design lässt eine Seite auf jedem Bildschirm funktionieren: das Viewport-Meta-Tag, Mobile First und Media Queries, die das Layout nach der Breite ändern.
Problem
Der Button liegt außerhalb des Bildschirms
Die Produktseite wurde an einem Monitor gebaut, und dort sieht sie gut aus. Ein Kunde am Handy schreibt: „Ich kann nicht bestellen, da ist kein Button.“ Hier ist sie in der Breite eines Handys, 360px:
Konsole
Der Button „Bestellen“ liegt rechts, außerhalb des Bildschirms. Die Seite lässt sich seitwärts schieben, und die meisten kommen nicht auf die Idee. Der Code hat keinen Fehler: Das Bild ist 400px breit, die Textspalte 360px, und sie stehen nebeneinander. Auf einem Monitor mit 1000px Breite passt das. Auf 360px nicht.
Die Idee
Bildschirme haben unterschiedliche Breiten
Dieselbe Seite öffnet sich auf einem Handy (360–430px), auf einem Tablet (etwa 800px) und auf einem Computer (1000px und mehr). Eine Seite, die sich jeder Breite anpasst, heißt responsiv.
Der erste Schritt steht in <head>:
<meta name="viewport" content="width=device-width, initial-scale=1">Ohne diese Zeile gibt sich das Handy als Monitor aus: Es legt die Seite mit 980px an und verkleinert sie, bis sie passt. Der Text wird winzig, und dein CSS sieht die echten 360px nie. Mit ihr ist die Breite der Seite die Breite des Bildschirms. In der Sandbox fügen wir diese Zeile für dich hinzu, auf deinen eigenen Seiten schreibst du sie selbst.
Der zweite Schritt ist die Reihenfolge, in der du das CSS schreibst. Mobile First heißt: Die Basisregeln sind für den schmalsten Bildschirm. Dort steht alles untereinander, und das passt fast immer. Dann fügst du mit einer Media Query Änderungen für breitere Bildschirme hinzu:
.product { display: grid; gap: 16px;}@media (min-width: 700px) { .product { grid-template-columns: 1fr 1fr; }}@media (min-width: 700px) ist eine Bedingung: „Die Breite ist mindestens 700px.“ Die Regeln darin gelten nur, wenn die Bedingung wahr ist. Sonst überspringt der Browser sie.
Der dritte Schritt sind Bilder. width="400" ist gut für das HTML: Der Browser hält Platz frei, während das Bild lädt. Auf einem schmalen Bildschirm braucht es aber eine Obergrenze:
img { max-width: 100%; height: auto;}max-width: 100% heißt „nicht breiter als seine Box“, und height: auto behält die Proportionen, damit es nicht gestaucht wird.
Sieh es dir an
So schalten sich die Queries ein
Das Fenster wächst von 360px auf 1000px. Achte auf das Lineal oben und die Zeilen im Code.
body { margin: 0; padding: 16px; }.cards { display: grid; grid-template-columns: 1fr; gap: 16px;}@media (min-width: 600px) { .cards { grid-template-columns: 1fr 1fr; }}@media (min-width: 900px) { .cards { grid-template-columns: repeat(4, 1fr); }}Schritt für Schritt, in Worten
- Ein Handy, 360px. Die Basisregel gilt für den schmalsten Bildschirm: eine Spalte. Eine Karte ist 360 − 2 × 16 = 328px breit.
- Das Fenster wird breiter und die Spalte wächst mit. Bei 599px ist die Bedingung min-width: 600px noch falsch, und der Browser überspringt die Regeln darin.
- Bei 600px ist die Bedingung wahr. Ihre Regel steht nach der Basisregel und gewinnt: zwei Spalten zu je 276px. Die Karten ordnen sich neu.
- Noch breiter: Die zwei Spalten wachsen auf je 425,5px. min-width: 900px ist noch falsch.
- Bei 900px sind beide Queries wahr. Sie setzen dieselbe Eigenschaft, also gewinnt die spätere: vier Spalten zu je 205px.
- Bei 1000px schaltet sich nichts Neues ein. Die vier Spalten wachsen auf 230px.
- Der Browser prüft die Queries bei jeder Änderung der Breite neu: ein gedrehtes Handy, ein schmaleres Fenster. Machst du es schmaler, gehen die Spalten denselben Weg zurück.
Eine Media Query wird nicht einmal berechnet. Der Browser prüft sie bei jeder Änderung der Breite und legt die Seite sofort neu an. Die zwei Queries setzen dieselbe Eigenschaft mit derselben Spezifität, also gewinnt bei 900px die spätere. Das ist die Kaskade, die du schon kennst.
Probier es aus
Für ein Handy reparieren
Die Seite ist 360px breit. Mach sie Mobile First: Bild und Text untereinander, und nichts ragt seitwärts heraus. Das Bild ist nicht breiter als seine Box. Ab 700px stehen Bild und Text in zwei Spalten. Die Prüfung sucht nach seitlichem Scrollen und danach, in wie vielen Zeilen Bild und Textspalte stehen.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dobrudscha-Brot · Der Shop</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="product">
<img src="img/bread.svg" width="400" height="400" alt="Ein rundes Weißbrot auf einem Brett">
<div class="info">
<h1>Dobrudscha-Brot</h1>
<p>Weißbrot, 650 g, heute früh gebacken.</p>
<p class="price">1,80 €</p>
<button type="button">Bestellen</button>
</div>
</main>
</body>
</html>
style.cssbody {
margin: 0;
font-family: system-ui, sans-serif;
}
.product {
display: grid;
gap: 16px;
padding: 16px;
}
img {
max-width: 100%;
height: auto;
}
.price {
font-size: 1.5rem;
font-weight: 700;
}
button {
background: #1a7f37;
color: #fff;
border: 0;
border-radius: 6px;
padding: 12px 20px;
font-size: 1rem;
}
@media (min-width: 700px) {
.product {
grid-template-columns: 1fr 1fr;
gap: 32px;
}
}
.product wird display: grid mit einer Spalte. img { max-width: 100%; height: auto; } verkleinert das Bild auf die Breite seiner Box. Die zwei Spalten stehen in @media (min-width: 700px), gelten also auf dem Handy nicht. overflow-x: hidden an body ist keine Lösung: Das Scrollen verschwindet, und der Button bleibt außerhalb des Bildschirms.
Probier es aus
Jetzt für einen Computer
Diese Seite ist schon für ein Handy gemacht: alles in einer Spalte. Hier ist die Breite 1000px. Ergänze eine Media Query, damit ab 700px Bild und Text in zwei gleich breiten Spalten stehen.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dobrudscha-Brot · Der Shop</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="product">
<img src="img/bread.svg" width="400" height="400" alt="Ein rundes Weißbrot auf einem Brett">
<div class="info">
<h1>Dobrudscha-Brot</h1>
<p>Weißbrot, 650 g, heute früh gebacken.</p>
<p class="price">1,80 €</p>
<button type="button">Bestellen</button>
</div>
</main>
</body>
</html>
style.cssbody {
margin: 0;
font-family: system-ui, sans-serif;
}
.product {
display: grid;
gap: 16px;
padding: 16px;
}
img {
max-width: 100%;
height: auto;
}
.price {
font-size: 1.5rem;
font-weight: 700;
}
button {
background: #1a7f37;
color: #fff;
border: 0;
border-radius: 6px;
padding: 12px 20px;
font-size: 1rem;
}
@media (min-width: 700px) {
.product {
grid-template-columns: 1fr 1fr;
gap: 32px;
}
}
@media (min-width: 700px) { .product { grid-template-columns: 1fr 1fr; gap: 32px; }}max-width: 700px ist das Gegenteil: „höchstens 700px“. Es schaltet sich nur auf schmalen Bildschirmen ein. min-width: 700 ohne px ist ungültig, und eine ungültige Query ist nie wahr.
Probier es aus
Finde den Fehler
Auf einem Computer sollten es zwei Spalten sein, aber die Seite bleibt bei einer. Warum?
Wähle die Zeile mit dem Fehler.
Die Media Query ist wahr, aber die Basisregel steht danach. Beide gelten für .product und haben dieselbe Spezifität, und die spätere gewinnt: grid-template-columns: 1fr in Zeile 8. Bei Mobile First stehen die Basisregeln zuerst und die Media Queries danach.
Probier es aus
Lücke füllen
Welches Wort lässt die zwei Spalten ab 700px gelten?
@media (: 700px) {
.product {
grid-template-columns: 1fr 1fr;
}
}min-width: „Die Breite ist mindestens 700px.“ Bei Mobile First nehmen Queries fast immer min-width.
Die Idee
Die Farbe an einer Stelle
Das Grün des Shops steht im CSS an vier Stellen: im Preis, in den Links, im Button und im Rahmen. Der Shop ändert seine Farbe, und jemand muss alle finden. Übersieht er eine, hat die Seite zwei Farben.
Eine CSS-Variable (Custom Property) ist ein Wert mit einem Namen. Der Name beginnt mit zwei Strichen, und var() liest ihn:
:root { --accent: #1a7f37;}button { background: var(--accent);}:root ist das oberste Element, <html>. Variablen werden wie color vererbt, also sieht man alles aus :root überall auf der Seite. Die Farbe ändert sich in einer Zeile.
Dasselbe ergibt ein dunkles Theme. @media (prefers-color-scheme: dark) ist wahr, wenn jemand auf Handy oder Computer ein dunkles Theme gewählt hat. Darin änderst du nur die Werte der Variablen. Die Regeln, die sie nutzen, bleiben gleich.
Sieh es dir an
Hell und dunkel
Konsole
Die Sandbox folgt dem Theme deines Browsers, nicht dem Theme-Button dieser Seite. Stell dein System auf dunkel und klick auf „Ausführen“: Hintergrund, Text und Grün ändern sich, während die CSS-Regeln für .card, .price und button gleich bleiben. Das Grün auf dunklem Grund ist heller, damit man es auf dem dunklen Hintergrund lesen kann.
Probier es aus
Eine neue Farbe für den Shop
Der Shop ändert das Grün #1a7f37 in Orange #c2410c. Lege in :root eine Variable --accent mit dem Wert #c2410c an und nutze sie an den vier Stellen, an denen jetzt das Grün steht.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dobrudscha-Brot · Der Shop</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<article class="card">
<h1>Dobrudscha-Brot</h1>
<p class="price">1,80 €</p>
<p>Auch interessant: <a href="/bread">weitere Brote</a></p>
<button type="button">Bestellen</button>
</article>
</main>
</body>
</html>
style.css:root {
--accent: #c2410c;
}
body {
margin: 0;
padding: 16px;
font-family: system-ui, sans-serif;
}
.card {
max-width: 320px;
padding: 16px;
border-top: 4px solid var(--accent);
}
.price {
color: var(--accent);
font-size: 1.5rem;
font-weight: 700;
}
a {
color: var(--accent);
}
button {
background: var(--accent);
color: #fff;
border: 0;
border-radius: 6px;
padding: 12px 20px;
font-size: 1rem;
}
:root { --accent: #c2410c;}Dann steht an den vier Stellen var(--accent). Die Prüfung liest die Variable und die echten Farben. Orange, an vier Stellen von Hand getippt, besteht sie also nicht. Auch var(accent) ohne die Striche nicht: Der Browser verwirft eine solche Deklaration, und der Button bleibt grau.
Die Idee
Zustände: berührt, fokussiert, deaktiviert
Ein Button sieht nicht nur auf eine Art aus. Er kann unter der Maus liegen, per Tastatur fokussiert sein oder deaktiviert. CSS fängt das mit Pseudoklassen ab: einem Wort mit Doppelpunkt hinter dem Selektor.
button:hover { background: #116329;}button:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px;}button:disabled { background: #9ca3af; cursor: not-allowed;}:hover: Die Maus ist über dem Element. Ein Handy hat keine Maus, deshalb versteckt man wichtige Informationen nicht hinter:hover.:focus-visible: Das Element hat den Fokus, und der Browser findet, dass der Fokus sichtbar sein soll. Bei der Tastatur ist er immer sichtbar. Bei einem Mausklick auf einen Button meistens nicht. Deshalb istoutline: nonean:focusein Fehler: Wer mit der Tastatur arbeitet, verliert den Überblick, wo er ist.:disabled: Das Element hat das Attributdisabled. Der Button „Ausverkauft“ soll deaktiviert aussehen, nicht nur deaktiviert sein.
Und die Größe. Auf einem Handy drückt man mit dem Finger, und der ist ungenauer als eine Maus. Ein Button von 24px Höhe wird verfehlt. Gib Buttons mindestens 44px Höhe: min-height: 44px.
Probier es aus
Die Buttons auf einem Handy
Ein Kollege hat den Fokusrahmen entfernt, und die Buttons sind niedrig. Sorg dafür, dass die Buttons mindestens 44px hoch sind (min-height), dass beim Tastaturfokus ein 3px breiter durchgehender Rahmen in der Farbe --accent erscheint und dass der deaktivierte Button „Ausverkauft“ grau (#9ca3af) ist, mit dem Cursor not-allowed. Die Prüfung drückt Tab und schaut sich den Button „Bestellen“ an.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dobrudscha-Brot · Der Shop</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<article>
<h2>Dobrudscha-Brot</h2>
<p>1,80 €</p>
<button type="button">Bestellen</button>
</article>
<article>
<h2>Kaschkawal</h2>
<p>7,50 €</p>
<button type="button" disabled>Ausverkauft</button>
</article>
</main>
</body>
</html>
style.css:root {
--accent: #1a7f37;
}
body {
margin: 0;
padding: 16px;
font-family: system-ui, sans-serif;
}
button {
min-height: 44px;
background: var(--accent);
color: #fff;
border: 0;
border-radius: 6px;
padding: 8px 16px;
}
button:hover {
background: #116329;
}
button:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
Entferne button:focus { outline: none; }. Bleibt die Regel nach :focus-visible stehen, gewinnt sie: Die Spezifität ist gleich, und sie kommt später. Der Rahmen kommt an button:focus-visible und das Grau an button:disabled. Die Pseudoklasse :disabled fängt das Attribut disabled aus dem HTML ab, eine Klasse .disabled braucht es also nicht.
Genauer, für Neugierige
Viewport, genauer. Ohne das Meta-Tag legen mobile Browser die Seite mit 980px an und verkleinern sie. In Chromium mit einem emulierten Handy von 360px ist innerWidth 980, und @media (min-width: 700px) ist wahr: Das Handy bekommt das Layout des Computers. initial-scale=1 heißt „am Anfang nicht verkleinern“. Schalte den Zoom nicht ab (user-scalable=no, maximum-scale=1): Menschen mit schwachen Augen vergrößern die Seite mit zwei Fingern.
Breakpoints. 600, 700, 900 sind keine heiligen Zahlen. Setz einen Breakpoint dorthin, wo dein Layout schlecht auszusehen beginnt, nicht ans Modell irgendeines Handys. Manche schreiben Queries in em (min-width: 40em): Dann folgen sie auch der Schriftgröße, die jemand gewählt hat.
Queries nicht nur für die Breite. (hover: hover) ist wahr, wenn das Hauptzeigegerät schweben kann (eine Maus), und (pointer: coarse) ist wahr für einen Finger. (prefers-reduced-motion: reduce) sagt, dass jemand keine Bewegung möchte: Nimm die Animationen weg. (orientation: landscape) ist für einen Bildschirm im Querformat.
Variablen, genauer. var(--accent, #1a7f37) hat einen Ersatzwert für den Fall, dass die Variable fehlt. Eine Variable kann sich für einen Teil der Seite ändern: .sale { --accent: #c2410c; }, und alles in .sale ist orange. Der Wert wird erst geprüft, wenn er benutzt wird: --accent: 12px ist erlaubt, aber dann ist color: var(--accent) ungültig, und die Farbe wird die geerbte. Die Eigenschaft color-scheme: light dark sagt dem Browser, seine eigenen Teile (Felder, Scrollbars) auch im dunklen Theme zu färben.
Größe für den Finger. WCAG 2.2 (Kriterium 2.5.8) verlangt ein Ziel von mindestens 24 × 24px oder genug Platz drumherum. Apple empfiehlt 44 × 44pt und Material Design 48 × 48dp.
Übung
Der Katalog auf einem Handy
Der Katalog wurde für einen Computer gemacht: vier Karten zu 220px in einer Reihe. Hier ist die Breite 360px. Mach ihn für ein Handy: die Karten in zwei Spalten, kein seitliches Scrollen, Bilder nicht breiter als ihre Karte, die Buttons „Kaufen“ mindestens 44px hoch (min-height). Lege das Grün #1a7f37 in eine Variable --accent in :root und lass die Buttons sie nutzen. Wenn du magst, ergänze ab 700px auch vier Spalten.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Katalog · Der Shop</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<h1>Katalog</h1>
<div class="cards">
<article class="card">
<img src="img/bread.svg" width="220" height="220" alt="">
<h2>Dobrudscha-Brot</h2>
<p>1,80 €</p>
<button type="button">Kaufen</button>
</article>
<article class="card">
<img src="img/milk.svg" width="220" height="220" alt="">
<h2>Frischmilch 3 %</h2>
<p>2,40 €</p>
<button type="button">Kaufen</button>
</article>
<article class="card">
<img src="img/cheese.svg" width="220" height="220" alt="">
<h2>Kaschkawal</h2>
<p>7,50 €</p>
<button type="button">Kaufen</button>
</article>
<article class="card">
<img src="img/apples.svg" width="220" height="220" alt="">
<h2>Äpfel, 1 kg</h2>
<p>2,20 €</p>
<button type="button">Kaufen</button>
</article>
</div>
</main>
</body>
</html>
style.css:root {
--accent: #1a7f37;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
}
main {
padding: 16px;
}
.cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.card img {
max-width: 100%;
height: auto;
}
.card button {
min-height: 44px;
background: var(--accent);
color: #fff;
border: 0;
border-radius: 6px;
padding: 8px 16px;
}
@media (min-width: 700px) {
.cards {
grid-template-columns: repeat(4, 1fr);
}
}
grid-template-columns: 1fr 1fr teilt die Breite in zwei, während repeat(2, 220px) wieder fest ist und nicht passt: Zwei Karten und der gap sind 2 × 220 + 16 = 456px, mehr als 360px. Die Bilder brauchen max-width: 100%, sonst dehnen sie mit width: 220px die Spalten. Die vier Spalten für den Computer stehen in @media (min-width: 700px) nach der Basisregel. Die Prüfung läuft auf dem Handy und sieht sie nicht, aber mit ihnen ist die Seite vollständig.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Selektoren und die Kaskade · Das Box-Modell · Flexbox: Reihe und Spalte · Grid: Zeilen und Spalten
- Aufgaben zum Thema: CSS: Wie die Seite aussieht