Съдържание на курса
Урок 5 — Страница, която става за всеки екран
От Dorian Chávez · основател на Hábil и архитект на интеграции ·
Време: две сесии от около 90 минути. Разпределение, което работи: в първата — „Защо, преди как“ (320, тагът viewport и как се мери) и колоните, които се броят сами (5.1); във втората — таблицата, която се превърта, @media, @container, завършеното табло, пренасянето му в твоя revisor (5.2.7) и упражненията. Всяка сесия завършва със страница, която можеш да отвориш и да измериш.
Какво изграждаш: таблото, което става за телефон
Какво научаваш: какво прави тагът viewport; адаптивност с minmax() и auto-fit преди @media; таблицата, която се превърта в кутията си; @media за страницата и @container за кутията; от 320 до 1440 px без преливане
Откъде идваш. Носиш таблото от Урок 4, подредено с Flexbox и Grid: заглавната част в един ред, четирите числа на обобщението в четири колони и лентата с контроли в една линия. На широк екран изглежда добре; на телефон още не. Всички страници на този урок са в programas/05-pagina-adaptable/ на хранилището на курса, а изходната точка е fig05_01.html, която е таблото, с което завърши Урок 4, такова, каквото е. Стиловият ѝ лист, fig05_01/styles.css, е копие на fig04_06/styles.css, стиловия лист от предишния урок: повторен е нарочно, за да работи тази папка сама, без да зависи от папката на друг урок. Страниците за изпробване на този урок го зареждат с <link rel="stylesheet" href="fig05_01/styles.css">, относителен път, който браузърът търси в папката fig05_01 до страницата; ако копираш само един .html, без тази папка, страницата ще се види без стилове и разделът „Мрежа“ ще ти покаже 404 на стиловия лист. Завършеното табло, fig05_06.html, ползва собствения си стилов лист, fig05_06/styles.css. Ако твоят revisor не е останал същият в края на Урок 4, няма значение: имаш пълните fig05_01.html и стиловия ѝ лист в края на 5.2.6. Работиш отново с index.html и css/styles.css от папката си revisor, а локалният сървър си остава python3 -m http.server 8000 --bind 127.0.0.1.
Какво не прави този урок. Не пипа JavaScript, освен един ред, който ще поставиш в конзолата, за да мериш. Числата на обобщението си остават написани на ръка; една програма ще ги преброи в Урок 6, а Урок 7 ще ги начертае в таблицата. Днес, както в предишния урок, се променя само къде стои всяко нещо и колко е голямо.
След урока ще можеш да
- Обясняваш какво казва на един телефон тагът
viewportи какво става без него: страницата се изчертава във виртуален прозорец, по-широк от екрана (980 px в Chrome), и се смалява. - Проверяваш с измерване, не на око, че една страница не прелива между 320 и 1440 px, и намираш виновника, когато прелива.
- Декларираш колони, които се броят сами, с
repeat(auto-fit, minmax(min(100%, 11rem), 1fr))и обясняваш какво прави всяка част от този ред. - Обясняваш защо
1frне стига в колона, която ще съдържа нещо широко, и пишешminmax(0, 1fr)на негово място. - Оставяш една широка таблица да се превърта вътре в собствената си кутия, без да губи семантиката си и без да изоставя този, който ползва клавиатура.
- Решаваш кога трябва медийна заявка (
@media), кога контейнерна (@container) и кога никаква. - Пренасяш адаптивното табло в своя
revisor, мериш го при пет ширини и го запазваш в Git.
Защо, преди как
Два часът през нощта е и на този, който е на дежурство, му звъни телефонът. Отваря таблото, за да види коя услуга е паднала. Няма компютър подръка: има екран в ръка и палец. Онова, което трябва да знае, се побира в едно изречение („Inventario не отговаря“), но страницата, такава, каквато я остави предишният урок, го кара да работи повече, отколкото трябва.
Измери го, вместо да го предполагаш. Отвори fig05_01.html на локалния си сървър, отвори инструментите на браузъра с F12 и включи адаптивния режим на екрана (в Chrome и Edge е иконата на телефон и таблет; във Firefox е Ctrl+Shift+M). Постави ширината на 320 пиксела. Ще видиш хоризонтална лента за превъртане: страницата е по-широка от екрана и който я ползва, трябва да влачи встрани, за да види таблица с три колони. Измерих го с Chrome 154, автоматизирано и без прозорец, и излезе това: при 320 px страницата е 414 px широка, същото число, с което завършиха Урок 3 и Урок 4; при 375 px също е 414; при 768 px, 1024 px и 1440 px е точно колкото прозореца, без да остава нищо. Два елемента излизат от ръба. Първият е таблицата, която с трите си колони и вътрешния отстъп на всяка клетка стига до 414 px. Вторият е нов и го донесе предишният урок: обобщението, чиито четири фиксирани колони не се побират на телефон и стигат до 369 px. Останалото —заглавната част, полето за търсене, радио бутоните, бутонът— вече се побира, защото слиза на нов ред с flex-wrap и защото никой стилов лист не им е сложил фиксирана ширина. Това е добродетел, която не бива да се губи.
А в обратния край остава един по-малък дълг: на екран от 1440 px съдържанието продължава да е колона от 60 rem (960 px) в средата, с две празни ивици отстрани, а таблицата остава под обобщението, когато двете биха се събрали една до друга.
Двете неща имат една и съща причина. Таблото подрежда, но не се адаптира. Четирите колони на обобщението са четири при всяка ширина, защото така са написани; таблицата е колкото е съдържанието ѝ, защото никой не ѝ е казал друго; а страницата е една колона, макар да остава място. Този урок е онзи, който го учи да решава според мястото, което има.
Какво значи „320 пиксела“ и защо е това число
CSS пикселът не е физическа точка на екрана. Това е единица, която браузърът поддържа постоянна нарочно, за да се вижда кутия с ширина 100 px с приблизително същия размер на екран с висока плътност, както на обикновен. Телефон с екран от 1,080 физически точки на ширина обикновено се декларира като прозорец от около 360 до 430 CSS пиксела (точното число варира според модела); всеки CSS пиксел се рисува с няколко физически точки. Когато един стилов лист казва width: 320px, говори на тази единица.
320 идва от публично правило: критерият за успех 1.4.10 („Reflow“, прегрупиране) на Указанията за достъпност на уеб съдържанието 2.2, който изисква съдържанието да може да се представя „без загуба на информация или функционалност и без нужда от превъртане в две измерения“ в прозорец, еквивалентен на 320 CSS пиксела ширина. Причината е увеличението: тази ширина е равна на отваряне на страницата на екран от 1,280 px и приближаването ѝ до 400 %. Човек със слабо зрение, който увеличава текста, докато се чете удобно, без да знае, превръща монитора си в екран от 320 px. Ако страницата прелива там, този човек трябва да влачи страницата от едната страна на другата, за да прочете всеки ред. 320 не е мания на стари телефони: това е подът за всички.
Същата норма носи едно изключение, което е добре да знаеш отсега, защото ще го ползваш разумно: частите от съдържанието, които изискват двуизмерно оформление за употребата или значението си —карти, видео, а също и таблици с данни— могат да се превъртат. Но изключението покрива само тази част: заглавието на таблицата и онова, което я заобикаля, трябва да се прегрупират. По-долу, в понятие 5.2, таблицата на услугите ще се превърта вътре в кутията си, а всичко останало ще се подрежда само.
Тагът, който вече написа, без да знаеш какво прави
В Урок 2 сложи в <head> реда <meta name="viewport" content="width=device-width, initial-scale=1">, защото „така се прави“. Сега се разбира. Телефоните от първите години на мобилния уеб са срещнали страници, направени за настолни компютри, и, за да не ги покажат счупени, са измислили трик: изчертават страницата във виртуален прозорец, по-широк от екрана —обикновено 980 пиксела— и после смаляват резултата, за да се побере. Това поведение продължава да е по подразбиране за страница, която не декларира нищо. MDN го документира и ползва 980 px като пример, но никоя норма не определя това число: всеки браузър избира своето. Chrome ползва 980, както ще измериш веднага; друг браузър или друг телефон може да ти даде друго число и онова, което не се променя, е ефектът: прозорец, по-широк от екрана, и всичко дребно.
Тагът viewport моли браузъра да ползва реалната ширина на устройството. Доказателството е в fig05_02.html, страница, която нарочно не го носи. Измерих я с Chrome 154 на емулиран телефон с ширина 390 px: без тага window.innerWidth е 980; с него (както в fig05_03.html, която ще видиш в 5.1.2) е 390. Всичко, което научаваш в този урок, зависи от това: правило като „от ширина 64 em нататък“ не значи нищо, ако браузърът вярва, че прозорецът е 980 px, когато е 390.
Това е страницата на теста, цялата:
<!-- fig05_02.html -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Página sin la etiqueta de ventana</title>
<link rel="stylesheet" href="fig05_01/styles.css">
</head>
<body>
<h1>Revisor de servicios</h1>
<p>Esta página se ve bien en una computadora y diminuta en un teléfono.</p>
</body>
</html>
Едно предупреждение, което MDN повтаря и което нормата за достъпност подкрепя: не отнемай на читателя възможността да приближава страницата. Има ръководства, които добавят user-scalable=no или maximum-scale=1, за да не прави телефонът мащабиране при докосване на поле. Който има слабо зрение, ползва това мащабиране, за да чете. Този ред си остава сам, такъв, какъвто е.
Как ще проверяваш
Има два начина да провериш, че една страница не прелива, и ще ползваш и двата през целия урок.
Първият е да погледнеш: адаптивният режим на инструментите на браузъра, с ширина 320 px. Ако се появи хоризонтална лента за превъртане върху страницата (не вътре в определена кутия), прелива.
Вторият е да измериш, защото окото греши с по няколко пиксела. Отвори раздела с конзолата на инструментите, постави този ред и натисни Enter:
document.documentElement.scrollWidth > document.documentElement.clientWidth
Това е JavaScript, който още не изучаваш: ще го разбереш изцяло в Урок 6, а засега е достатъчно да знаеш какво пита. clientWidth е видимата ширина на страницата, а scrollWidth е ширината, която съдържанието ѝ реално заема. Ако втората надвишава първата, съдържанието е по-широко от прозореца и се връща true. Ако върне false, няма преливане. В fig05_01.html с прозорец 320 px този ред връща true; в края на урока, със завършеното ти табло, ще връща false при всички ширини.
Понятията
Две идеи, в този ред: колоните, които се броят сами, които решават обобщението, без да се пише нито едно число за ширина на прозореца; и адаптивното, което не е поредният инструмент, а начин да ползваш Flexbox и Grid, така че страницата да се подрежда сама, с медийните и контейнерните заявки само за онова, което наистина ги изисква. Всяка се обяснява първо с минимален пример, а после с таблото.
5.1 Колони, които се броят сами
5.1.1 Капанът на 1fr: скритият минимум
repeat(4, 1fr) има проблем, който се вижда щом екранът се стесни: винаги са четири колони, каквито и да са ширините. Видя го в края на раздел 4.2.3 на Урок 4, със страницата fig04_05.html от предишния урок, която декларира четирите числа на обобщението в четири фиксирани колони: при 1024 px и при 1440 px изглежда добре, но при 320 px четирите числа се стягат и страницата е 344 px широка (при 375 px вече се побират, едва).
Има фина причина, която си струва да разбереш, защото е най-честият капан на Grid: 1fr не е „един дял“ просто така. То е minmax(auto, 1fr). Минимумът на колоната е auto, което значи „колкото е най-тясното възможно съдържание“, и една колона не се свива под това. Тук най-дългата дума на всяко число задава под за колоната си. Измерих го: четирите пода правят 280 px, а трите пролуки от 16 px още 48; това са 328 px, повече от 288-те, които страницата оставя при 320 px след вътрешния си отстъп, така че решетката прелива, вместо да се свие.
Практическото следствие е важно: да напишеш повече колони, отколкото се побират, не ги свива; прелива ги. А да напишеш по-малко (Упражнение 2 от Урок 4 пробва с две) пилее място на широк екран. Никакъв фиксиран брой колони не върши работа за всички ширини. Което трябва, е да кажеш на браузъра колко е голяма една колона най-малко и да го оставиш той да преброи колко се побират.
5.1.2 minmax(), auto-fit и min()
minmax(минимум, максимум) е функцията, която ти позволява да зададеш пода и тавана на една колона. minmax(11rem, 1fr) казва: „тази колона е поне 11rem (176 px) и, ако има място, расте, като го разпределя с останалите“. С този известен под браузърът вече знае колко колони се побират в дадена ширина. И ето частта, която свързва всичко:
grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
Чете се отвътре навън:
minmax(11rem, 1fr): всяка колона е от 176 px нагоре.repeat(auto-fit, ...): повтаря колоната толкова пъти, колкото се побира. Не му се казва колко; браузърът брои.min(100%, 11rem): подът е по-малкото между 11rem и 100 % от контейнера. Без тази част, в контейнер, по-тесен от 176 px (тясна странична лента, телефон с 150 px полезна ширина), колоната не би се побрала и би преляла; с нея подът никога не надвишава наличната ширина.min()е CSS функция, която връща по-малкия от аргументите си, а MDN я отбелязва като налична във всички браузъри от юли 2020 г.; платформата я води като широко достъпна от януари 2023 г., след строгите 30 месеца.
Има две думи, които си приличат и не правят едно и също: auto-fill и auto-fit. И двете броят колко колони се побират. Разликата се появява, когато има по-малко елементи, отколкото възможни колони: auto-fill запазва празните колони (мястото остава резервирано, макар там да няма нищо), а auto-fit ги свива до нула, така че елементите, които има, се разтягат и заемат целия ред. Измерих го, като оставих само две числа в страницата fig05_03.html по-долу, с прозорец 1440 px (стиловият лист от Урок 3 ограничава съдържанието до 60 rem, така че контейнерът е 928 px): с auto-fill се побират четири колони от 220 px и двете карти остават в първите две, с половин празен ред; с auto-fit двете са по 456 px и запълват реда. За обобщението на таблото, auto-fit: искаме числата, които има, да заемат целия ред, не да оставят дупка.
Страницата с решението е fig05_03.html. Идентична е с fig04_05.html, освен за този ред (и за пътя на стиловия ѝ лист, който е копието на тази папка):
<!-- fig05_03.html -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Resumen que cuenta sus columnas</title>
<link rel="stylesheet" href="fig05_01/styles.css">
<style>
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
gap: 0 1rem;
margin: 0;
}
</style>
</head>
<body>
<main>
<h1>Resumen</h1>
<dl class="summary">
<div>
<dt>Servicios revisados</dt>
<dd>5</dd>
</div>
<div>
<dt>Disponibles</dt>
<dd>4 de 5</dd>
</div>
<div>
<dt>Caídos</dt>
<dd>1</dd>
</div>
<div>
<dt>Respuesta promedio</dt>
<dd>465 ms</dd>
</div>
</dl>
</main>
</body>
</html>
Измерих пет ширини: 320, 375, 768, 1024 и 1440 px. При всичките ширината на страницата е равна на тази на прозореца; не прелива. При 320 px четирите числа образуват колона, всяко по 288 px; при 1024 и 1440 заемат един-единствен ред, с четири колони от по 220 px. Никой не е написал „при еди-каква ширина, една колона“: браузърът е направил сметката с пода от 11 rem.
5.1.3 Другият капан на 1fr: решетка, която се издува
Има вариант на същия капан, който ще те ухапе, когато ползваш Grid за скелета на цялата страница, и затова го превръщаш в навик още днес. Помисли си за страница с една-единствена колона: display: grid без нищо повече или с grid-template-columns: 1fr. Ако някое дете има широко съдържание (например таблица), колоната се издува, докато го побере, защото минимумът ѝ auto е ширината на най-тясното възможно съдържание. Резултатът: таблицата не се превърта в кутията си, а влачи цялата страница.
Измерих го в завършеното табло, като смених само minmax(0, 1fr) с 1fr в решетката на страницата: при 320 px страницата отново прелива и сега е 439 px, макар таблицата да е вътре в кутията си с overflow-x: auto. С minmax(0, 1fr) е 320. Решението, винаги, е да напишеш нулевия минимум нарочно: minmax(0, 1fr) казва „тази колона може да се свие до нищо, ако се налага; не я издувай заради съдържанието“. Когато една решетка ще съдържа нещо потенциално широко, колоната се пише minmax(0, 1fr), не 1fr.
5.1.4 Решен пример: обобщението на таблото
В таблото обобщението вече има своя клас от Урок 4 (<dl class="summary">) и HTML не се пипа. Единственото, което се променя, е един ред от правилото .summary в стиловия лист: repeat(4, 1fr) става редът с колоните, които се броят сами.
.summary {
container-type: inline-size;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
gap: 0 var(--space-3);
margin: 0;
}
Първата декларация, container-type: inline-size, още не я познаваш: подготвя обобщението за контейнерна заявка и се обяснява в 5.2.4. Останалите три са обичайните. С тази промяна вторият виновник за преливането изчезва: обобщението е колкото му позволява секцията му при всякаква ширина, а на телефон числата се подреждат едно под друго. Остава първият, таблицата, която има нужда от друг инструмент.
5.2 Адаптивно: съдържанието да решава
5.2.1 Първо течливостта, после заявката
Което направи дотук, вече е „адаптивно“, макар да не си написал нито една медийна заявка. Лентата с контроли от Урок 4 слиза на нов ред сама, когато не се побира; обобщението брои колоните си само. Техниката се нарича присъщ дизайн (intrinsic design): вместо да казваш „при еди-каква ширина, направи еди-какво“, даваш на браузъра под, таван и предпочитание (flex: 1 1 14rem, minmax(11rem, 1fr)) и той решава с каквато ширина има. Има предимство, което не се оценява, докато не се сравни: работи с ширини, които никой не е предвидил. Таблет във вертикално положение, прозорец наполовина, сгъваем телефон, екран с уголемен шрифт: нито един не е бил в списъка ти с „устройства“ и всички се подреждат еднакво.
Редът на инструментите, от този, който пише най-малко, към този, който пише най-много, е:
- Нормален поток. Ако съдържанието се побира в една колона, остави го в една колона.
- Flexbox с
flex-wrapи разумни основи. За групи неща, които си поделят една линия. - Grid с
repeat(auto-fit, minmax(...)). За карти и блокове с числа. - Медийна или контейнерна заявка. Само когато се променя организацията на страницата и няма начин да се поиска с предишните инструменти.
5.2.2 Таблицата: превъртане вътре в кутията ѝ
Остава другото преливане, това на таблицата. Една таблица с данни не може да се „раздели“, без да се унищожи онова, което значи: ако превърнеш всеки ред в карта, ще загубиш сравнението на колоните, което е точно за това, за което служи таблицата. Това е случаят, който нормата за прегрупиране изключва: таблица може да изисква две измерения. Решението е да оставиш таблицата да запази ширината си, а кутията ѝ да се превърта:
<!-- fig05_04.html -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tabla que se desplaza sola</title>
<link rel="stylesheet" href="fig05_01/styles.css">
<style>
.table-scroll {
overflow-x: auto;
}
</style>
</head>
<body>
<main>
<h1>Servicios</h1>
<div class="table-scroll" role="region" aria-labelledby="table-caption" tabindex="0">
<table>
<caption id="table-caption">Estado de los servicios en la última revisión</caption>
<thead>
<tr>
<th scope="col">Servicio</th>
<th scope="col">Estado</th>
<th scope="col">Tiempo de respuesta</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Catálogo</th>
<td><span class="status status-available">Disponible</span></td>
<td>120 ms</td>
</tr>
<tr>
<th scope="row">Pagos</th>
<td><span class="status status-available">Disponible</span></td>
<td>480 ms</td>
</tr>
<tr>
<th scope="row">Inventario</th>
<td><span class="status status-down">Caído</span></td>
<td>sin respuesta</td>
</tr>
<tr>
<th scope="row">Notificaciones</th>
<td><span class="status status-available">Disponible</span></td>
<td>310 ms</td>
</tr>
<tr>
<th scope="row">Búsqueda</th>
<td><span class="status status-available">Disponible</span></td>
<td>950 ms</td>
</tr>
</tbody>
</table>
</div>
</main>
</body>
</html>
Новият елемент е div с overflow-x: auto, който казва: „ако съдържанието е по-широко от мен, покажи хоризонтална лента за превъртане вътре в мен, не в страницата“. С това цялата страница е 320 px (ширината на прозореца) и само таблицата се плъзга. Измерих тази страница при петте ширини и преливането на страницата е нула при всичките.
Трите атрибута на div заслужават обяснение, защото са нарочно изключение от правилото „не слагай атрибути за достъпност, които не са нужни“:
tabindex="0"кара кутията да получава фокус с клавиша Tab. Без него, който ползва само клавиатура, не може да превърта таблицата: стрелките превъртат само област, която има фокус. Това изисква критерият 2.1.1 („Клавиатура“). Chrome, от версия 132, прави сам фокусируем контейнер, който се превърта и няма фокусируеми деца, но други браузъри не го гарантират, така че се пише на ръка.role="region"иaria-labelledby="table-caption"ѝ дават име —това на заглавието на таблицата— за да каже екранният четец „Estado de los servicios en la última revisión, област“ при достигане, вместо едно мълчаливо „група“. Това е рецептата, която публикува Адриан Розели, а негово обновление от 2026 г. пояснява, че ролята може да е по желание, за да се спази нормата; името помага така или иначе.
Проверих с клавиатурата завършеното табло: с Tab фокусът минава през връзката „Resumen“, връзката „Servicios“, полето за търсене, групата радио бутони (едно-единствено спиране, защото радио бутоните на една група се броят за един), бутона „Revisar ahora“ и накрая кутията на таблицата, идентифицирана като region. С фокус върху кутията стрелките наляво и надясно превъртат таблицата. А контурът на фокуса, който определи Урок 3 с :focus-visible, се вижда около кутията, така че който навигира с клавиатура, знае къде е.
5.2.3 Кога наистина е нужна @media
Все още има една промяна, която нито Flexbox, нито Grid искат сами по себе си: на екран от 1440 px обобщението и таблицата се събират една до друга, а на такъв от 375 px — не. Това не е решаване на колко колони се побират в един ред: това е решаване как се организира цялата страница. За това съществуват медийните заявки (@media): правила, които се прилагат само ако прозорецът отговаря на условие.
Съвременният синтаксис ползва сравнения, както в математиката:
@media (width >= 64em) {
.layout {
grid-template-columns: 20rem minmax(0, 1fr);
}
}
Казва: „когато ширината на прозореца е 64 em или повече, решетката на страницата има колона от 20 rem и друга, която взима остатъка“. Формата (width >= 64em) се нарича синтаксис на диапазон: браузърите го разбират от 2022 и 2023 г. (Chrome и Edge 104, Firefox 102, Safari 16.4), а платформата го води като широко достъпен от септември 2025 г.. Преди се пишеше (min-width: 64em), което значи точно същото; ще го видиш във всеки по-стар код.
Две решения на правилото заслужават обяснение:
Единицата е em, не px. Точката на прекъсване се мери в em (един em е размерът на буквите на браузъра, 16 px по подразбиране). Така, ако някой увеличи базовия размер на буквите на браузъра, точката на прекъсване се движи с него: 64 em са равни на 1,024 px само ако читателят не е пипал настройките си; ако ги е оставил на 150 %, са равни на 1,536 px и страницата сменя организацията си в подходящия момент за тези букви. MDN го препоръчва: точките на прекъсване в относителни единици остаряват по-добре.
Първо се пише това на телефона. Базовото правило (без @media) е това на тясната ширина: една колона. Онова за голямата ширина се добавя вътре в заявката. Това се нарича mobile first и причината е практична: най-простото е първо и е онова, което получават, без претоварване, устройствата с по-малко ресурси; сложното се добавя само там, където има място за него. Обратният ред (да пишеш първо това за настолен компютър и после да го развързваш с max-width) налага да анулираш правила, а всяка анулация е място, където да сбъркаш.
И едно предупреждение, което служи за противотежест: точката на прекъсване не се избира според устройство, а според съдържанието. Правилният въпрос не е „колко е широк един iPhone?“, а „от каква ширина нататък спира да изглежда добре онова, което има?“. Влачи ръба на прозореца, докато нещо се счупи или се види пилеено; там отива точката на прекъсване. Списъкът с модели телефони се променя всяка година; съдържанието — не.
5.2.4 Кутията решава, не прозорецът: @container
Има случай, който @media не решава добре. Виж fig05_05.html. В широк прозорец колоната на обобщението е 20 rem (320 px); в прозорец на телефон от 375 px обобщението заема цялата ширина: 343 px. Това са две ситуации с приблизително същата ширина на обобщението и ширини на прозореца 1440 и 375. Една медийна заявка (която вижда само прозореца) би трябвало да налучква колко място има обобщението във всеки случай. Което наистина има значение, е ширината на кутията, в която се намира, не тази на прозореца.
За това са контейнерните заявки. Една кутия се декларира като контейнер, за който може да се пита, и после се пита за ширината ѝ:
<!-- fig05_05.html -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dos preguntas distintas: la ventana y la caja</title>
<link rel="stylesheet" href="fig05_01/styles.css">
<style>
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 1.5rem;
align-items: start;
}
/* La separación la pone el gap de la rejilla, no el margen de la Lección 3. */
.layout section {
margin-bottom: 0;
}
/* La ventana decide cómo se reparten las dos piezas de la página. */
@media (width >= 64em) {
.layout {
grid-template-columns: 20rem minmax(0, 1fr);
}
}
/* La caja decide cómo se acomoda lo que lleva dentro. */
.summary {
container-type: inline-size;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
gap: 0 1rem;
margin: 0;
}
@container (width < 24rem) {
.summary div {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1rem;
}
.summary dd {
white-space: nowrap;
}
}
</style>
</head>
<body>
<h1>La página en dos piezas</h1>
<main class="layout">
<section id="summary">
<h2>Resumen</h2>
<dl class="summary">
<div>
<dt>Servicios revisados</dt>
<dd>5</dd>
</div>
<div>
<dt>Disponibles</dt>
<dd>4 de 5</dd>
</div>
<div>
<dt>Caídos</dt>
<dd>1</dd>
</div>
<div>
<dt>Respuesta promedio</dt>
<dd>465 ms</dd>
</div>
</dl>
</section>
<section id="services">
<h2>Servicios</h2>
<p>Aquí va la tabla.</p>
</section>
</main>
</body>
</html>
Четири части:
container-type: inline-sizeдекларира.summaryкато контейнер, за който може да се пита по ширината му.@container (width < 24rem)прилага правилата вътре само ако този контейнер е по-малък от 24 rem (384 px). Условието има същия синтаксис на диапазон като@media.- Вътре в заявката четирите кутии на обобщението стават компактни редове (
display: flex, с термина отляво и стойността отдясно). Това е, което виждаш в страничната лента и на телефона: един ред за всяко число, вместо висок квадрат. white-space: nowrapе предпазна мярка: пречи на „465 ms“ да се разчупи на два реда, ако компактният ред се стесни още. С шрифта от стиловия лист на Урок 3 не се наложи (измерих го: числото заема един-единствен ред от 32 px с правилото и без него, в страничната лента и на телефон от 375 px), но струва един ред и пази в деня, в който някой увеличи шрифта или числото е по-дълго.
Две ограничения, които е добре да научиш наведнъж. Първо: контейнерна заявка може да променя само потомците на контейнера, не самия контейнер. Затова правилата сочат към .summary div, а не към .summary. Второ: container-type: inline-size значи, че ширината на контейнера вече не зависи от съдържанието му (дава му я родителят) и затова се декларира в кутия, чиято ширина идва отвън, като решетка или колона.
Контейнерните заявки работят в трите основни двигателя от февруари 2023 г. (Chrome и Edge 105, Safari 16, Firefox 110), а платформата ги води като широко достъпни от 14 август 2025 г.. Когато един компонент ще живее на места с различни ширини —странична лента, основна колона, изскачащ прозорец—, това е правилният инструмент.
Една бележка за навика да валидираш, който научи в Урок 3. Ако прекараш този стилов лист през CSS валидатора на W3C, той вече не отговаря „Congratulations! No Error Found“: проверих го, като му изпратих fig05_06/styles.css, и отговори с две грешки, „Свойството ‚container-type‘ не съществува“ и „at-правилото ‚@container‘ не е имплементирано“. Не са грешки на твоя стилов лист, а ограничение на валидатора, който още не познава контейнерните заявки, макар браузърите да ги ползват от 2023 г. Когато валидираш, провери, че единствените грешки са тези две; всяка друга е твоя.
5.2.5 Размер на целите
Още едно нещо, което си личи най-вече на сензорен екран, където палецът е по-неточен от курсора, но което важи за всеки указател: мишката, писалката или пръста. Критерият 2.5.8 на нормата („Размер на целта, минимум“) изисква целите, които се активират с указател, да са поне 24 на 24 CSS пиксела, с изключения (ако има достатъчно място около тях, ако целта е вътре в изречение, ако размерът е определен от браузъра). Най-голяма нужда от това имат хората с треперене на ръцете или с малка точност на движенията, каквото и устройство да ползват. В таблото бутоните и полето за търсене носят min-height: 2.5rem (40 px) от Урок 3: повече от един и половина пъти минимума, защото 24 px е под, не мярката, която един палец улучва удобно.
5.2.6 Завършеното табло на урока
С всичко горно цялото табло на урока е fig05_06.html. Промените в HTML спрямо таблото от Урок 4 са три и никоя не променя онова, което HTML казва: класът layout върху <main>, div.table-scroll с таблицата вътре и id="table-caption" върху заглавието на таблицата, към което сочи aria-labelledby на обвивката.
<!-- fig05_06.html -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Revisor de servicios</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="fig05_06/styles.css">
</head>
<body>
<header class="page-header">
<h1>Revisor de servicios</h1>
<p>Última revisión:
<time datetime="2026-10-07T10:30:00-06:00">7 de octubre de 2026, 10:30</time>
</p>
<nav>
<p>Ir a: <a href="#summary">Resumen</a> o <a href="#services">Servicios</a></p>
</nav>
</header>
<main class="layout">
<section id="summary">
<h2>Resumen</h2>
<dl class="summary">
<div>
<dt>Servicios revisados</dt>
<dd>5</dd>
</div>
<div>
<dt>Disponibles</dt>
<dd>4 de 5</dd>
</div>
<div>
<dt>Caídos</dt>
<dd>1</dd>
</div>
<div>
<dt>Respuesta promedio</dt>
<dd>465 ms</dd>
</div>
</dl>
</section>
<section id="services">
<h2>Servicios</h2>
<div class="controls">
<p class="field">
<label for="search">Buscar servicio</label>
<input type="search" id="search" name="search">
</p>
<fieldset>
<legend>Mostrar</legend>
<label><input type="radio" name="filter" value="all" checked> Todos</label>
<label><input type="radio" name="filter" value="available"> Disponibles</label>
<label><input type="radio" name="filter" value="down"> Caídos</label>
</fieldset>
<p><button type="button">Revisar ahora</button></p>
</div>
<div class="table-scroll" role="region" aria-labelledby="table-caption" tabindex="0">
<table>
<caption id="table-caption">Estado de los servicios en la última revisión</caption>
<thead>
<tr>
<th scope="col">Servicio</th>
<th scope="col">Estado</th>
<th scope="col">Tiempo de respuesta</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Catálogo</th>
<td><span class="status status-available">Disponible</span></td>
<td>120 ms</td>
</tr>
<tr>
<th scope="row">Pagos</th>
<td><span class="status status-available">Disponible</span></td>
<td>480 ms</td>
</tr>
<tr>
<th scope="row">Inventario</th>
<td><span class="status status-down">Caído</span></td>
<td>sin respuesta</td>
</tr>
<tr>
<th scope="row">Notificaciones</th>
<td><span class="status status-available">Disponible</span></td>
<td>310 ms</td>
</tr>
<tr>
<th scope="row">Búsqueda</th>
<td><span class="status status-available">Disponible</span></td>
<td>950 ms</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
<footer>
<p>Datos de ejemplo escritos a mano.</p>
</footer>
</body>
</html>
А това е целият стилов лист, който е този от Урок 4 с добавеното адаптивно подреждане (коментарите сочат от кой урок е всяка част):
/* fig05_06/styles.css */
/* La hoja del panel al terminar la Lección 5: la de la Lección 3, con sus mismas
capas y variables, más el acomodo de la Lección 4 (Flexbox y Grid) y el de la
Lección 5 (cualquier pantalla). Cada cambio lleva un comentario con su lección. */
/* El orden de las capas se declara una sola vez, arriba. La última gana. */
@layer reset, base, components;
@layer reset {
/* La caja: el ancho que escribes es el ancho que se ve. */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* El navegador no hace que los campos hereden la letra del texto. */
input,
button {
font: inherit;
}
}
@layer base {
/* Las decisiones del panel viven aquí, con nombres que dicen para qué sirven. */
:root {
--color-text: #1b1f24;
--color-muted: #57606a;
--color-page: #f6f8fa;
--color-surface: #ffffff;
--color-border: #d0d7de;
--color-control: #6e7781;
--color-accent: #0b5cad;
--color-available-text: #0f5132;
--color-available-bg: #d1e7dd;
--color-down-text: #842029;
--color-down-bg: #f8d7da;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 1rem;
--space-4: 1.5rem;
--space-5: 2.5rem;
--font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--line-height: 1.6;
--radius: 0.375rem;
}
body {
margin: 0;
background: var(--color-page);
color: var(--color-text);
font-family: var(--font-body);
line-height: var(--line-height);
}
/* Un ancho cómodo para leer, centrado: auto reparte el espacio sobrante.
Lección 5: de 60 a 80 rem, porque en una pantalla ancha ahora caben dos columnas. */
header,
main,
footer {
max-width: 80rem;
margin: 0 auto;
padding: var(--space-3);
}
h1 {
margin: 0 0 var(--space-2);
font-size: 2rem;
line-height: 1.2;
}
h2 {
margin: 0 0 var(--space-3);
font-size: 1.375rem;
line-height: 1.3;
}
a {
color: var(--color-accent);
}
/* Que el foco del teclado se vea siempre: nunca outline: none sin reemplazo. */
:focus-visible {
outline: 3px solid var(--color-accent);
outline-offset: 2px;
}
footer {
color: var(--color-muted);
}
}
@layer components {
/* Cada sección es una hoja blanca sobre el fondo de la página.
Lección 5: sin margen inferior; la separación la pone el gap de .layout. */
section {
padding: var(--space-3) var(--space-4);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
}
/* El resumen: cada par nombre-valor en su renglón. */
dl div {
padding: var(--space-2) 0;
border-bottom: 1px solid var(--color-border);
}
dl div:last-child {
border-bottom: 0;
}
dt {
color: var(--color-muted);
}
dd {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
/* Los controles. */
label {
margin-right: var(--space-3);
}
input[type="search"] {
min-height: 2.5rem;
padding: var(--space-1) var(--space-2);
border: 1px solid var(--color-control);
border-radius: var(--radius);
}
fieldset {
margin: var(--space-3) 0;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius);
}
button {
min-height: 2.5rem;
padding: var(--space-1) var(--space-3);
border: 0;
border-radius: var(--radius);
background: var(--color-accent);
color: var(--color-surface);
font-weight: 600;
cursor: pointer;
}
button:hover {
background: var(--color-text);
}
/* La tabla. */
table {
width: 100%;
margin-top: var(--space-3);
border-collapse: collapse;
}
caption {
padding-bottom: var(--space-2);
color: var(--color-muted);
text-align: left;
}
th,
td {
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--color-border);
text-align: left;
}
/* Los números a la derecha y con cifras del mismo ancho para que alineen. */
th:last-child,
td:last-child {
text-align: right;
font-variant-numeric: tabular-nums;
}
/* La insignia de estado: una sola regla, y las variantes solo cambian variables. */
.status {
display: inline-block;
padding: 0 var(--space-2);
border-radius: 999px;
background: var(--badge-bg);
color: var(--badge-text);
font-size: 0.875rem;
font-weight: 600;
}
.status-available {
--badge-bg: var(--color-available-bg);
--badge-text: var(--color-available-text);
}
.status-down {
--badge-bg: var(--color-down-bg);
--badge-text: var(--color-down-text);
}
/* ---- Lecciones 4 y 5: el acomodo ---- */
/* Lección 5. Las dos piezas de la página: la ventana decide si van una sobre otra
o lado a lado. minmax(0, 1fr) y no 1fr: la columna no se infla con la tabla. */
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--space-4);
align-items: start;
}
@media (width >= 64em) {
.layout {
grid-template-columns: 20rem minmax(0, 1fr);
}
}
/* Grid, dos dimensiones (Lección 4); las columnas se cuentan solas (Lección 5). */
.summary {
container-type: inline-size;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
gap: 0 var(--space-3);
margin: 0;
}
/* Lección 5. La caja, no la ventana, decide si cada cifra va en una fila compacta. */
@container (width < 24rem) {
.summary div {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: var(--space-3);
}
.summary dd {
white-space: nowrap;
}
}
/* Lección 4. Flexbox, una dimensión: el encabezado y la barra de controles
se parten en renglones cuando no caben. */
.page-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: var(--space-1) var(--space-3);
}
.page-header p {
margin: 0;
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: var(--space-3);
}
.controls p,
.controls fieldset {
margin: 0;
}
.field {
display: flex;
flex-direction: column;
gap: var(--space-1);
flex: 1 1 14rem;
}
.field input {
width: 100%;
}
/* Lección 5. La tabla conserva su semántica: lo que se desplaza es su envoltorio. */
.table-scroll {
overflow-x: auto;
}
}
Спрямо стиловия лист от Урок 4 има две корекции, една промяна и три нови правила, и нищо повече. Корекциите: максималната ширина на header, main и footer се вдига от 60 на 80 rem, за да не се стягат двете колони на широк екран; а секциите губят своя margin-bottom, защото вътре в решетката разстоянието го слага gap на .layout, което е правилото от Урок 4 (4.1.2): разстоянието между братята го слага родителят. Промяната: правилото .summary оставя repeat(4, 1fr) за колоните, които се броят сами, и добавя container-type, както в 5.1.4. Новите правила са в блока за подреждане, който сега събира това от двата урока: .layout с minmax(0, 1fr) и, от 64 em нататък, две колони; контейнерната заявка на обобщението; и .table-scroll. Заглавната част, лентата с контроли, слоевете, променливите, фокусът и значките си остават каквито бяха: адаптивното се добавя към онова, което е имало, не го пренаписва. (Редът на правилата вътре в блока се промени спрямо Урок 4: първо страницата, после обобщението, после контролите. Това е редът, в който ги чете този, който отваря файла отгоре надолу, и не променя резултата, защото нито едно от тези правила не се състезава с друго за същото свойство.)
Изходната точка на този урок, fig05_01.html, е таблото от Урок 4 с <link>, сочещ към копието на стиловия лист, което живее в тази папка; имаш го цяло по-долу, в случай че искаш да сравниш или нямаш това от предишния урок:
<!-- fig05_01.html -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Revisor de servicios</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="fig05_01/styles.css">
</head>
<body>
<header class="page-header">
<h1>Revisor de servicios</h1>
<p>Última revisión:
<time datetime="2026-10-07T10:30:00-06:00">7 de octubre de 2026, 10:30</time>
</p>
<nav>
<p>Ir a: <a href="#summary">Resumen</a> o <a href="#services">Servicios</a></p>
</nav>
</header>
<main>
<section id="summary">
<h2>Resumen</h2>
<dl class="summary">
<div>
<dt>Servicios revisados</dt>
<dd>5</dd>
</div>
<div>
<dt>Disponibles</dt>
<dd>4 de 5</dd>
</div>
<div>
<dt>Caídos</dt>
<dd>1</dd>
</div>
<div>
<dt>Respuesta promedio</dt>
<dd>465 ms</dd>
</div>
</dl>
</section>
<section id="services">
<h2>Servicios</h2>
<div class="controls">
<p class="field">
<label for="search">Buscar servicio</label>
<input type="search" id="search" name="search">
</p>
<fieldset>
<legend>Mostrar</legend>
<label><input type="radio" name="filter" value="all" checked> Todos</label>
<label><input type="radio" name="filter" value="available"> Disponibles</label>
<label><input type="radio" name="filter" value="down"> Caídos</label>
</fieldset>
<p><button type="button">Revisar ahora</button></p>
</div>
<table>
<caption>Estado de los servicios en la última revisión</caption>
<thead>
<tr>
<th scope="col">Servicio</th>
<th scope="col">Estado</th>
<th scope="col">Tiempo de respuesta</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Catálogo</th>
<td><span class="status status-available">Disponible</span></td>
<td>120 ms</td>
</tr>
<tr>
<th scope="row">Pagos</th>
<td><span class="status status-available">Disponible</span></td>
<td>480 ms</td>
</tr>
<tr>
<th scope="row">Inventario</th>
<td><span class="status status-down">Caído</span></td>
<td>sin respuesta</td>
</tr>
<tr>
<th scope="row">Notificaciones</th>
<td><span class="status status-available">Disponible</span></td>
<td>310 ms</td>
</tr>
<tr>
<th scope="row">Búsqueda</th>
<td><span class="status status-available">Disponible</span></td>
<td>950 ms</td>
</tr>
</tbody>
</table>
</section>
</main>
<footer>
<p>Datos de ejemplo escritos a mano.</p>
</footer>
</body>
</html>
а стиловият му лист, fig05_01/styles.css, е fig04_06/styles.css без нито една промяна, освен коментарите отгоре:
/* fig05_01/styles.css */
/* La hoja del panel tal como la deja la Lección 4 (es fig04_06/styles.css, copiada
aquí para que esta carpeta funcione sola): la de la Lección 3 más Flexbox y Grid.
Todavía no se adapta a una pantalla angosta. */
/* El orden de las capas se declara una sola vez, arriba. La última gana. */
@layer reset, base, components;
@layer reset {
/* La caja: el ancho que escribes es el ancho que se ve. */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* El navegador no hace que los campos hereden la letra del texto. */
input,
button {
font: inherit;
}
}
@layer base {
/* Las decisiones del panel viven aquí, con nombres que dicen para qué sirven. */
:root {
--color-text: #1b1f24;
--color-muted: #57606a;
--color-page: #f6f8fa;
--color-surface: #ffffff;
--color-border: #d0d7de;
--color-control: #6e7781;
--color-accent: #0b5cad;
--color-available-text: #0f5132;
--color-available-bg: #d1e7dd;
--color-down-text: #842029;
--color-down-bg: #f8d7da;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 1rem;
--space-4: 1.5rem;
--space-5: 2.5rem;
--font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--line-height: 1.6;
--radius: 0.375rem;
}
body {
margin: 0;
background: var(--color-page);
color: var(--color-text);
font-family: var(--font-body);
line-height: var(--line-height);
}
/* Un ancho cómodo para leer, centrado: auto reparte el espacio sobrante. */
header,
main,
footer {
max-width: 60rem;
margin: 0 auto;
padding: var(--space-3);
}
h1 {
margin: 0 0 var(--space-2);
font-size: 2rem;
line-height: 1.2;
}
h2 {
margin: 0 0 var(--space-3);
font-size: 1.375rem;
line-height: 1.3;
}
a {
color: var(--color-accent);
}
/* Que el foco del teclado se vea siempre: nunca outline: none sin reemplazo. */
:focus-visible {
outline: 3px solid var(--color-accent);
outline-offset: 2px;
}
footer {
color: var(--color-muted);
}
}
@layer components {
/* Cada sección es una hoja blanca sobre el fondo de la página. */
section {
margin-bottom: var(--space-4);
padding: var(--space-3) var(--space-4);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
}
/* El resumen: cada par nombre-valor en su renglón. */
dl div {
padding: var(--space-2) 0;
border-bottom: 1px solid var(--color-border);
}
dl div:last-child {
border-bottom: 0;
}
dt {
color: var(--color-muted);
}
dd {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
/* Los controles. */
label {
margin-right: var(--space-3);
}
input[type="search"] {
min-height: 2.5rem;
padding: var(--space-1) var(--space-2);
border: 1px solid var(--color-control);
border-radius: var(--radius);
}
fieldset {
margin: var(--space-3) 0;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius);
}
button {
min-height: 2.5rem;
padding: var(--space-1) var(--space-3);
border: 0;
border-radius: var(--radius);
background: var(--color-accent);
color: var(--color-surface);
font-weight: 600;
cursor: pointer;
}
button:hover {
background: var(--color-text);
}
/* La tabla. */
table {
width: 100%;
margin-top: var(--space-3);
border-collapse: collapse;
}
caption {
padding-bottom: var(--space-2);
color: var(--color-muted);
text-align: left;
}
th,
td {
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--color-border);
text-align: left;
}
/* Los números a la derecha y con cifras del mismo ancho para que alineen. */
th:last-child,
td:last-child {
text-align: right;
font-variant-numeric: tabular-nums;
}
/* La insignia de estado: una sola regla, y las variantes solo cambian variables. */
.status {
display: inline-block;
padding: 0 var(--space-2);
border-radius: 999px;
background: var(--badge-bg);
color: var(--badge-text);
font-size: 0.875rem;
font-weight: 600;
}
.status-available {
--badge-bg: var(--color-available-bg);
--badge-text: var(--color-available-text);
}
.status-down {
--badge-bg: var(--color-down-bg);
--badge-text: var(--color-down-text);
}
/* ---- Lección 4: Flexbox y Grid ---- */
/* Flexbox, una dimensión: el encabezado se reparte en una fila y baja
de renglón cuando no cabe. */
.page-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: var(--space-1) var(--space-3);
}
.page-header p {
margin: 0;
}
/* La barra de controles: tres hermanos en una línea; solo el campo crece. */
.controls {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: var(--space-3);
}
.controls p,
.controls fieldset {
margin: 0;
}
.field {
display: flex;
flex-direction: column;
gap: var(--space-1);
flex: 1 1 14rem;
}
.field input {
width: 100%;
}
/* Grid, dos dimensiones: las cuatro cifras del resumen en cuatro columnas
iguales. En un teléfono no caben; la Lección 5 lo resuelve. */
.summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0 var(--space-3);
margin: 0;
}
}
Измери завършеното табло, както измери началното. Измерих fig05_06.html при 320, 375, 768, 1024 и 1440 px: ширината на страницата е равна на тази на прозореца при петте, а сравнението в конзолата връща false. При 1440 px обобщението е в колона от 20 rem отляво, а таблицата отдясно, с лентата с контроли над таблицата в един-единствен ред; при 320 px всичко е в една колона, с таблицата, която се превърта вътре в кутията си. Ширината на телефона, която беше проблемът, отворил урока, вече не е проблем.
5.2.7 Пренеси го в своя revisor
Дотук работеше със страниците на хранилището. Липсва стъпката, която превръща наученото в твой проект: твоят revisor да остане същият като fig05_06.html, измерен и запазен в Git. Те са четири стъпки и никоя не изисква да пишеш нещо ново.
1. Стиловият лист. Отвори fig05_06/styles.css (в хранилището или го копирай от пълния блок по-горе) и постави съдържанието му в твоя ~/revisor/css/styles.css, на мястото на онова, което е имало. Можеш да го замениш целия, защото е стиловият лист от Урок 4 с корекциите и правилата, които току-що прочете: нищо не се губи. Ако в предишните уроци си правил собствени промени в стиловия си лист (друг цвят, друг размер), тогава не го замествай: направи на ръка двете корекции (максималната ширина на header, main и footer се вдига на 80rem; section губи своя margin-bottom), смени правилото .summary с това от 5.1.4 и добави към блока от Урок 4 правилото .layout с неговата @media, @container на обобщението и .table-scroll.
2. Страницата. В твоя ~/revisor/index.html направи промените в HTML, които са тези от списъка в 5.2.6:
<main>става<main class="layout">.- Таблицата остава вътре в
<div class="table-scroll" role="region" aria-labelledby="table-caption" tabindex="0">, точно както в 5.2.2. - Което се забравя:
<caption>носиid="table-caption". Без тозиid,aria-labelledbyна обвивката сочи към нищо и областта остава без име.
Можеш също да копираш цялата fig05_06.html върху твоя index.html, с една предпазна мярка: в хранилището нейният <link> сочи към fig05_06/styles.css; в твоя проект трябва да казва href="css/styles.css", както от Урок 3. Ако не го смениш, страницата ще се види без стилове.
3. Измери. От папката на проекта пусни сървъра:
$ cd ~/revisor
$ python3 -m http.server 8000 --bind 127.0.0.1
Отвори http://127.0.0.1:8000/, включи адаптивния режим и повтори измерването от урока: редът в конзолата трябва да връща false при 320, 375, 768, 1024 и 1440 px, а страницата трябва да се вижда същата като fig05_06.html при същите ширини. Направих го с папка revisor, сглобена така: при 320 px страницата е 320, а кутията на таблицата 238 px, с таблицата, която се превърта вътре; при 1440 px, двете колони. Ако нещо не съвпада, сравни файла си с този от хранилището: почти винаги е клас, който не е написан, или div, който е затворен на друго място.
4. Запази го в Git. Спри сървъра с Ctrl+C (или отвори друг терминал) и питай Git какво се е променило, както в Урок 1:
$ git status
En la rama main
Cambios no rastreados para el commit:
(usa "git add <archivo>..." para actualizar lo que será confirmado)
(usa "git restore <archivo>..." para descartar los cambios en el directorio de trabajo)
modificados: css/styles.css
modificados: index.html
sin cambios agregados al commit (usa "git add" y/o "git commit -a")
Това са точно двата файла, които си пипал. (Ако се появи някой друг, това е твоя промяна отпреди, която не си запазил: прегледай я с git diff, преди да решиш дали влиза в този commit.) Провери с git diff, че промените са тези, които си искал, и ги запази:
$ git add index.html css/styles.css
$ git commit -m "Adapta el panel a cualquier ancho de pantalla"
$ git status
En la rama main
nada para hacer commit, el árbol de trabajo está limpio
git commit отпечатва ред с кода на commit и колко реда са се променили; числата зависят от твоите файлове. С това твоят revisor е готов за Урок 6, който започва точно оттук.
Грешката, която ще видиш
Грешките в подреждането почти никога не са съобщения: това е страница, която изглежда „зле“, без да каже защо. В този урок има две и двете се мерят. Първата току-що я видя в тага viewport: без него страницата не прелива, но всичко изглежда дребно, а единствената следа е числото на window.innerWidth. Втората е тази, която отвори урока.
Страницата, по-широка от прозореца. Отвори fig05_01.html при 320 px. Сигналът е хоризонталната лента за превъртане или конзолата:
document.documentElement.scrollWidth > document.documentElement.clientWidth
която отговаря true. За да намериш кой излиза, в инструментите премини с курсора през елементите в панела с елементите: браузърът засенчва областта на всеки и този, който излиза от десния ръб на прозореца, е виновникът. В fig05_01.html са два: таблицата, която стига до 414 px, и обобщението, което стига до 369. Двете решения вече ги имаш: обвивката с overflow-x: auto за таблицата и колоните, които се броят сами, за обобщението. Съвет за случая, когато виновниците са няколко: оправи единия, мери отново и търси следващия. Страницата е колкото е най-широкият, така че докато остава един, измерването продължава да дава true, дори да си оправил другия, и е лесно да повярваш, че поправката не е проработила.
И един вариант, който обърква: таблицата вече е в кутията си с overflow-x: auto и въпреки това страницата си остава по-широка от прозореца. Това е издутата решетка от точка 5.1.3: колоната на страницата е декларирана като 1fr и се разширява с ширината на таблицата. Оправя се с minmax(0, 1fr). Ако се натъкнеш на това в друг проект, вече знаеш къде да гледаш: не в таблицата, а в колоната, която я съдържа.
Какво се прави погрешно
Медийна заявка за устройство. @media (width: 390px) { ... } „за iPhone“. Покрива една-единствена ширина, на един-единствен модел, от една година. Сгъваем телефон, прозорец наполовина или екран с уголемен шрифт попадат между тези точки и там страницата не е обмислена. Цена: списък с устройства, който расте всяка година и никога не е актуален.
Да скриеш преливането, вместо да го оправиш. body { overflow-x: hidden } кара лентата за превъртане да изчезне и оставя съдържанието отрязано: онова, което е отвъд ръба, вече не може да се види, нито да се достигне. По-лошо е от дефекта, защото изглежда оправено. Критерият 1.4.10 изисква да не се губи информация при увеличение; да отрежеш страницата е да я загубиш.
Да превръщаш таблицата в друго нещо, за да „се побере“. Да смениш display на table, tr и td с block или grid, или да преправиш таблицата с div. Може да ѝ отнеме ролята на таблица и навигацията по клетки на този, който ползва екранен четец. В Chrome 154 таблицата с display: grid запази ролята си, но правилото на курса не зависи от това същото да се случва във всеки браузър и четец: таблицата се оставя таблица и се обвива.
Да махнеш мащабирането „за да не мърда“. user-scalable=no или maximum-scale=1 в тага viewport. Пречи на приближаването на страницата, което е онова, което слабовиждащият ползва, за да я чете.
Да дадеш на контейнер с несигурна ширина колона 1fr. Това е издутата решетка от 5.1.3: остава незабелязана, докато съдържанието е късо, и избухва в деня, в който пристигне широка таблица, дълъг идентификатор или URL без интервали. Струва един следобед, защото виновникът изглежда да е съдържанието, а е колоната.
Упражнения
Упражнение 1 — По-малко числа, същите колони
Копирай fig05_03.html и остави в обобщението само три числа: махни „Respuesta promedio“ (среден отговор). Без да пипаш CSS, измери ширината на всяка карта при 768, 1024 и 1440 px (инструментите на браузъра ти дават ширината на всеки елемент, като го избереш). После смени auto-fit с auto-fill, измери отново при тези три ширини и обясни при кои е имало разлика и защо (ползвай онова, което видя с две карти в 5.1.2).
Упражнение 2 — Счупи нарочно решетката на страницата
В копие на fig05_06.html смени minmax(0, 1fr) с 1fr в правилото на .layout. Измери ширината на страницата при 320 px с реда от конзолата. Колко дава? После потърси с инструментите на браузъра кой елемент кара решетката да расте и обясни защо, ако таблицата вече е била в кутията си с overflow-x: auto, страницата пак прелива.
Упражнение 3 — Точка на прекъсване, която се определя от съдържанието
В fig05_06.html лентата с контроли минава от три реда (при 320 px) на два и после на един, когато разширяваш прозореца. Без да поглеждаш нито един модел телефон, намери с адаптивния режим при каква ширина на прозореца минава от два реда на един и я запиши. После продължи да разширяваш: от 1024 px нагоре, и до малко над 1160, лентата отново заема два реда. Обясни двете открития със сбора от размерите на отделните части.
Решения
Решение 1
Измерих го с три карти с под от 11 rem. С auto-fit: при 768 px, три колони от 235 px; при 1024 и при 1440 px, три колони от 299 px. Последните две измервания са равни, защото стиловият лист ограничава съдържанието до 60 rem от Урок 3: от прозорец 960 px нагоре контейнерът вече не расте и е 928 px. С auto-fill резултатът е идентичен при 768 и различен при 1024 и при 1440: там се побират четири колони от 220 px, трите карти заемат първите три и остава празна колона отдясно.
Разликата се появява само когато има по-малко карти, отколкото възможни колони, а това става при 1024 и при 1440 px. При 768 px се побират точно три колони за три карти и не остава нито една. auto-fit свива колоните без съдържание и оставя тези, които го имат, да се разтегнат; auto-fill ги запазва, макар да са празни.
Решение 2
С 1fr в решетката на страницата измерването дава 439 при 320 px (сравнението връща true): това е колкото е таблицата, плюс вътрешният отстъп на нейната секция и този на страницата. Елементът, който разширява всичко, е колоната на решетката и секцията, която я съдържа. Таблицата наистина е вътре в кутията си, с overflow-x: auto, но кутията не може да е по-тясна от колоната си, а колоната 1fr има минимум auto, който е минималната ширина на съдържанието, таблицата включена. Тогава колоната се издува, кутията се издува с нея и кутията няма вече какво да превърта: сега всичко се побира в една широка кутия, която на свой ред прелива страницата. С minmax(0, 1fr) минимумът е нула, колоната е колкото позволява прозорецът, а таблицата, която си остава широка, се превърта вътре в кутия от 238 px.
Решение 3
Лентата се побира в един ред, когато кутията ѝ е поне колкото сборът на трите ѝ части плюс двете пролуки между тях. Измерих го в fig05_06.html: полето има основа 14rem (224 px), групата радио бутони е 345 px (всеки етикет носи десния външен отстъп от 1 rem, който му даде Урок 3), бутонът 135 px, а gap слага по 16 px два пъти. Сборът е 224 + 345 + 135 + 32 = 736 px. В тази зона на таблото страницата е една колона, а кутията на контролите е ширината на прозореца минус 82 px: 32 вътрешен отстъп на main, 48 вътрешен отстъп на секцията и 2 от нейната рамка. Така лентата минава на един ред при прозорец от 818 px, който беше точно първата ширина, която даде един-единствен ред при измерване пиксел по пиксел.
Второто откритие е това, което учи най-много: при 1024 px страницата минава на две колони (@media (width >= 64em)) и кутията на контролите остава на 598 px, което е по-малко от 736: лентата отново се разчупва на два реда. Едва от 1162 px кутията си връща 736 px и отново се побира в един. Ширината, която има значение за лентата, не е тази на прозореца, а тази на кутията ѝ, а ширината на кутията ѝ не расте равномерно с тази на прозореца, защото по средата страницата е сменила организацията си. Затова flex-wrap решава това без нито едно написано число: лентата слиза на нов ред, когато кутията ѝ е тясна, било заради прозореца, било заради странична колона. Ако беше написал медийна заявка с „818 px“, щеше да улучи при 820 и да сгреши при 1024.
Как разбирам, че съм успял
-
fig05_01.htmlпри 320 px връщаtrueс реда от конзолата (ширината на страницата е 414 px), аfig05_06.htmlвръщаfalseпри 320, 375, 768, 1024 и 1440 px. -
fig05_02.htmlна емулиран телефон от 390 px показваwindow.innerWidth980;fig05_03.htmlна същия телефон показва 390. - В
fig05_06.htmlпри 1440 px обобщението е отляво в една колона, а таблицата отдясно; при 320 px всичко върви в една колона и само таблицата се превърта встрани, вътре в кутията си. - С Tab фокусът минава в този ред: връзка „Resumen“, връзка „Servicios“, поле за търсене, група радио бутони, бутон „Revisar ahora“ и кутията на таблицата (която се обявява като област). С фокус върху кутията стрелките превъртат таблицата.
- Бутоните и полето за търсене са високи поне 40 px (
min-height: 2.5rem, от Урок 3) и инструментът за инспекция на браузъра го потвърждава. - Можеш със свои думи да обясниш какво прави всяка част от
repeat(auto-fit, minmax(min(100%, 11rem), 1fr)), защо1frсамо по себе си не стига в колона, която ще съдържа нещо широко, и каква е разликата между медийна и контейнерна заявка, с пример от таблото за всяка. - Твоят
~/revisor(сервиран от папката си наhttp://127.0.0.1:8000/) изглежда същият катоfig05_06.html, връщаfalseпри петте ширини, аgit log --onelineпоказва commit-а с адаптивното табло.
Обобщение
За да затвърдиш видяното, отговори, без да поглеждаш урока:
- Какво прави браузърът на телефон със страница, която не носи тага
viewport, и какво число измерихме? - Откъде идва 320 и кого защитава, освен този, който ползва телефон?
- Защо
repeat(4, 1fr)прелива при 320 px, щом1frе „дял от мястото“? - С какво се различават
auto-fillиauto-fitи кое от двете си използвал за обобщението? - Защо една широка таблица се обвива, а не се преправя с друг
display? Какви три атрибута носи обвивката ѝ и за какво служи всеки? - Защо точката на прекъсване се пише в
emи се избира, като се гледа съдържанието, не модел телефон? - Защо
@containerне може да променя самия контейнер?
Ако се запънеш на някой отговор, върни се към съответната точка: това е знак, че там е останала разхлабена част, не че не ставаш за това.
За допълнително четене
- MDN, „Адаптивен уеб дизайн“ — учебният модул на Mozilla за
viewport, mobile first и точки на прекъсване. Посетено на 7 октомври 2026 г. - W3C, „Разбиране на критерий 1.4.10: Reflow“ — откъде идва 320 и коя част от съдържанието може да се превърта. Посетено на 7 октомври 2026 г.
- MDN, „Контейнерни заявки“ — как се декларира контейнер, за който може да се пита, синтаксисът на
@containerи защо една заявка не може да променя собствения си контейнер. Посетено на 7 октомври 2026 г.
Предпочитате имейл? Пишете ни на hola@habil.mx