Responsive дизайн
Накратко: Responsive дизайнът прави страницата удобна на всеки екран: метатаг viewport, mobile first и медийни заявки, които сменят подредбата според ширината.
Проблем
Бутонът е извън екрана
Страницата на продукта е направена на монитор и там изглежда добре. На телефон купувач пише: „Не мога да поръчам, няма бутон“. Ето я на ширината на телефон, 360px:
Конзола
Бутонът „Поръчай“ е вдясно, извън екрана. Страницата се плъзга встрани, а повечето хора не се сещат да я плъзнат. Кодът е без грешка: снимката е 400px, колоната с текста е 360px и са една до друга. На 1000px широк монитор това се побира. На 360px не.
Идеята
Екраните са с различна ширина
Една и съща страница се отваря на телефон (360–430px), на таблет (около 800px) и на компютър (1000px и повече). Страница, която се нагажда към всяка ширина, се казва адаптивна (responsive).
Първата стъпка е в <head>:
<meta name="viewport" content="width=device-width, initial-scale=1">Без нея телефонът се преструва на монитор: подрежда страницата на 980px и я смалява, докато се побере. Текстът става ситен, а CSS-ът ти никога не вижда истинските 360px. С нея ширината на страницата е ширината на екрана. В пясъчника този ред го слагаме ние, но в твоите страници го пишеш ти.
Втората стъпка е редът, в който пишеш CSS-а. Mobile first значи: основните правила са за най-тесния екран. Там всичко е едно под друго, а това почти винаги се побира. После за по-широките екрани добавяш промени с медийна заявка (media query):
.product { display: grid; gap: 16px;}@media (min-width: 700px) { .product { grid-template-columns: 1fr 1fr; }}@media (min-width: 700px) е условие: „ширината е поне 700px“. Правилата вътре важат само когато условието е вярно. Иначе браузърът ги прескача.
Третата стъпка са картинките. width="400" е хубаво за HTML-а: браузърът пази място, докато картинката се зарежда. На тесен екран обаче ѝ трябва таван:
img { max-width: 100%; height: auto;}max-width: 100% значи „не по-широка от кутията си“, а height: auto пази пропорциите, за да не се сплеска.
Виж го
Виж как се включват заявките
Прозорецът става по-широк от 360px до 1000px. Гледай линийката горе и редовете в кода.
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); }}Стъпка по стъпка, с думи
- Телефон, 360px. Основното правило е за най-тесния екран: една колона. Картата е 360 − 2 × 16 = 328px.
- Прозорецът става по-широк и колоната расте с него. При 599px условието min-width: 600px още е невярно и браузърът прескача правилата в него.
- При 600px условието е вярно. Правилото в него е след основното и печели: две колони по 276px. Картите се пренареждат.
- Още по-широко: двете колони растат до 425,5px. min-width: 900px още е невярно.
- При 900px и двете заявки са верни. Задават едно и също свойство, затова печели по-късната: четири колони по 205px.
- При 1000px не се включва нищо ново. Четирите колони растат до 230px.
- Браузърът проверява заявките наново при всяка смяна на ширината: обърнат телефон, по-тесен прозорец. Стесниш ли го, колоните се връщат по същия път.
Медийната заявка не е изчислена веднъж. Браузърът я проверява при всяка смяна на ширината и пренарежда страницата веднага. Двете заявки задават едно и също свойство със същата специфичност, затова при 900px печели по-късната. Това е каскадата, която вече познаваш.
Пробвай
Поправи го за телефон
Страницата е на 360px. Направи я mobile first: снимката и текстът да са един под друг и нищо да не излиза встрани. Снимката да не е по-широка от кутията си. От 700px нагоре снимката и текстът да са в две колони. Проверката гледа за хоризонтален скрол и на колко реда са снимката и колоната с текста.
Конзола
Решение
index.html<!doctype html>
<html lang="bg">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Хляб „Добруджа“ · Магазинът</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="product">
<img src="img/bread.svg" width="400" height="400" alt="Кръгъл бял хляб на дъска">
<div class="info">
<h1>Хляб „Добруджа“</h1>
<p>Бял хляб, 650 г, изпечен тази сутрин.</p>
<p class="price">1,80 €</p>
<button type="button">Поръчай</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 става display: grid с една колона. img { max-width: 100%; height: auto; } смалява снимката до ширината на кутията. Двете колони са в @media (min-width: 700px), затова на телефона не важат. overflow-x: hidden на body не е поправка: скролът изчезва, а бутонът остава извън екрана.
Пробвай
Сега за компютър
Тази страница вече е за телефон: всичко е в една колона. Тук ширината е 1000px. Добави медийна заявка, с която от 700px нагоре снимката и текстът са в две равни колони.
Конзола
Решение
index.html<!doctype html>
<html lang="bg">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Хляб „Добруджа“ · Магазинът</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="product">
<img src="img/bread.svg" width="400" height="400" alt="Кръгъл бял хляб на дъска">
<div class="info">
<h1>Хляб „Добруджа“</h1>
<p>Бял хляб, 650 г, изпечен тази сутрин.</p>
<p class="price">1,80 €</p>
<button type="button">Поръчай</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 е обратното: „най-много 700px“. Тя се включва само на тесни екрани. min-width: 700 без px е невалидно, а невалидна заявка не е вярна никога.
Пробвай
Намери грешката
На компютър колоните са две, но страницата стои в една. Защо?
Избери реда с грешката.
Медийната заявка е вярна, но основното правило е след нея. Двете са .product, със същата специфичност, и печели по-късното: grid-template-columns: 1fr на ред 8. При mobile first основните правила са първи, а медийните заявки са след тях.
Пробвай
Попълни
Каква дума трябва, за да важат двете колони от 700px нагоре?
@media (: 700px) {
.product {
grid-template-columns: 1fr 1fr;
}
}min-width: „ширината е поне 700px“. При mobile first заявките са почти винаги с min-width.
Идеята
Цветът на едно място
Зеленото на магазина е в CSS-а на четири места: цената, връзките, бутона и рамката. Магазинът сменя цвета си и някой трябва да ги намери всичките. Пропусне ли едно, страницата е на две бои.
CSS променливата (custom property) е стойност с име. Името започва с две тирета, а var() я чете:
:root { --accent: #1a7f37;}button { background: var(--accent);}:root е най-горният елемент, <html>. Променливите се наследяват като color, затова това, което е в :root, се вижда навсякъде в страницата. Цветът се сменя на един ред.
Същото дава тъмна тема. @media (prefers-color-scheme: dark) е вярно, когато човек е избрал тъмна тема на телефона или на компютъра. Вътре сменяш само стойностите на променливите. Правилата, които ги ползват, остават същите.
Виж го
Светла и тъмна
Конзола
Пясъчникът следва темата на браузъра ти, а не бутона за тема на този сайт. Смени темата на системата на тъмна и натисни „Изпълни“: фонът, текстът и зеленото се сменят, а CSS правилата за .card, .price и button са същите. Зеленото на тъмно е по-светло, за да се чете върху тъмния фон.
Пробвай
Нов цвят на магазина
Магазинът сменя зеленото #1a7f37 с оранжево #c2410c. Направи променлива --accent в :root със стойност #c2410c и я ползвай на четирите места, където сега е зеленото.
Конзола
Решение
index.html<!doctype html>
<html lang="bg">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Хляб „Добруджа“ · Магазинът</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<article class="card">
<h1>Хляб „Добруджа“</h1>
<p class="price">1,80 €</p>
<p>Виж и <a href="/bread">другите хлябове</a></p>
<button type="button">Поръчай</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;}После на четирите места пише var(--accent). Проверката чете и променливата, и истинските цветове. Затова оранжево, написано на ръка на четири места, не минава. Не минава и var(accent) без тиретата: браузърът изхвърля такава декларация и бутонът остава сив.
Идеята
Състояния: посочен, на фокус, изключен
Бутонът не е само един вид. Той може да е посочен с мишката, да е на фокус от клавиатурата или да е изключен. CSS ги хваща с псевдокласове: дума с двоеточие след селектора.
button:hover { background: #116329;}button:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px;}button:disabled { background: #9ca3af; cursor: not-allowed;}:hover: мишката е върху елемента. На телефона мишка няма, затова важна информация не се крие зад:hover.:focus-visible: елементът е на фокус и браузърът преценява, че фокусът трябва да се вижда. От клавиатурата винаги се вижда. При клик с мишката на бутон обикновено не се вижда. Затоваoutline: noneна:focusе грешка: човек с клавиатура губи къде е.:disabled: елементът е с атрибутаdisabled. Бутонът „Изчерпано“ трябва да изглежда изключен, не само да е.
И размерът. На телефон се натиска с пръст, а той е по-неточен от мишката. Бутон с височина 24px се изпуска. Дай на бутоните поне 44px височина: min-height: 44px.
Пробвай
Бутоните на телефона
Колега е махнал контура на фокуса, а бутоните са ниски. Направи така: бутоните са поне 44px високи (min-height); при фокус от клавиатурата се вижда контур 3px solid в цвета --accent; изключеният бутон „Изчерпано“ е сив (#9ca3af) с курсор not-allowed. Проверката натиска Tab и гледа бутона „Поръчай“.
Конзола
Решение
index.html<!doctype html>
<html lang="bg">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Хляб „Добруджа“ · Магазинът</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<article>
<h2>Хляб „Добруджа“</h2>
<p>1,80 €</p>
<button type="button">Поръчай</button>
</article>
<article>
<h2>Кашкавал</h2>
<p>7,50 €</p>
<button type="button" disabled>Изчерпано</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;
}
Махни button:focus { outline: none; }. Ако остане след :focus-visible, той печели: специфичността е същата, а е по-късно. Контурът е на button:focus-visible, а сивото на button:disabled. Псевдокласът :disabled хваща атрибута disabled от HTML-а, затова клас .disabled не трябва.
По-точно, за любопитните
Viewport по-точно. Без метатага мобилните браузъри подреждат страницата на 980px и я смаляват. В Chromium с емулиран телефон 360px innerWidth е 980, а @media (min-width: 700px) е вярно: телефонът получава подредбата за компютър. initial-scale=1 казва „без начално смаляване“. Не забранявай мащабирането (user-scalable=no, maximum-scale=1): хората със слабо зрение увеличават страницата с два пръста.
Точките на прекъсване. 600, 700, 900 не са свещени числа. Слагаш точка там, където подредбата ти започва да изглежда зле, а не по модела на някой телефон. Някои пишат заявките в em (min-width: 40em): тогава те следват и размера на шрифта, който човек е избрал.
Заявки не само за ширина. (hover: hover) е вярно, когато основното устройство за посочване може да посочва (мишка), а (pointer: coarse) е вярно при пръст. (prefers-reduced-motion: reduce) казва, че човек не иска движение: махни анимациите. (orientation: landscape) е за хоризонтален екран.
Променливите по-точно. var(--accent, #1a7f37) има резервна стойност, ако променливата я няма. Променлива може да се смени за част от страницата: .sale { --accent: #c2410c; } и всичко в .sale е оранжево. Стойността се проверява чак когато се ползва: --accent: 12px е позволено, но color: var(--accent) тогава е невалидно и цветът става наследения. Свойството color-scheme: light dark казва на браузъра да оцвети и своите части (полета, скрол) в тъмната тема.
Размер за пръста. WCAG 2.2 (критерий 2.5.8) иска цел поне 24 × 24px или достатъчно място около нея. Apple препоръчва 44 × 44pt, а Material Design 48 × 48dp.
Упражнение
Каталогът на телефон
Каталогът е направен за компютър: четири карти по 220px в един ред. Тук ширината е 360px. Направи го за телефон: картите в две колони, без хоризонтален скрол, картинките не по-широки от картата си, бутоните „Купи“ поне 44px високи (min-height). Зеленото #1a7f37 сложи в променлива --accent в :root и бутоните да я ползват. Ако искаш, добави и четири колони от 700px нагоре.
Конзола
Решение
index.html<!doctype html>
<html lang="bg">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Каталог · Магазинът</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<h1>Каталог</h1>
<div class="cards">
<article class="card">
<img src="img/bread.svg" width="220" height="220" alt="">
<h2>Хляб „Добруджа“</h2>
<p>1,80 €</p>
<button type="button">Купи</button>
</article>
<article class="card">
<img src="img/milk.svg" width="220" height="220" alt="">
<h2>Прясно мляко 3%</h2>
<p>2,40 €</p>
<button type="button">Купи</button>
</article>
<article class="card">
<img src="img/cheese.svg" width="220" height="220" alt="">
<h2>Кашкавал</h2>
<p>7,50 €</p>
<button type="button">Купи</button>
</article>
<article class="card">
<img src="img/apples.svg" width="220" height="220" alt="">
<h2>Ябълки, 1 кг</h2>
<p>2,20 €</p>
<button type="button">Купи</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 дели ширината на две, а repeat(2, 220px) е пак твърдо и не се побира: две карти и разстоянието са 2 × 220 + 16 = 456px, повече от 360px. Картинките трябва max-width: 100%, иначе с width: 220px разтягат колоните. Четирите колони за компютър са в @media (min-width: 700px) след основното правило. Проверката е на телефона и тях не ги вижда, но страницата е завършена с тях.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Селектори и каскада · Box model · Flexbox: ред и колона · Grid: редове и колони
- Задачи по темата: CSS: как изглежда страницата