Съдържание на курса

Урок 4 — Подреждане с Flexbox и Grid

От Dorian Chávez · основател на Hábil и архитект на интеграции ·

Време: две сесии от около 90 минути. Разпределение, което работи: в първата — „Защо, преди как“ и Flexbox (4.1), с лентата с контроли; във втората — Grid (4.2), подреденото табло, пренасянето му в твоя revisor (4.2.5) и упражненията. Всяка сесия завършва със страница, която можеш да отвориш и да измериш.

Какво изграждаш: таблото, подредено на широк екран

Какво научаваш: едно измерение с Flexbox и две с Grid; двете оси, gap, flex и flex-wrap; колони с fr и repeat(); да мериш с инструментите на браузъра вместо на око

Откъде идваш. Носиш таблото от Урок 2, написано на ръка с HTML, който казва какво е всяко нещо, и стиловия лист от Урок 3, с неговите цветови променливи, предвидимата му кутия и значките за състояние. Този стилов лист още не подрежда нищо: всеки елемент стои под предишния, което е онова, което браузърът прави, когато никой не му иска друго. Ако по някаква причина твоето копие не съвпада с това от тези уроци, няма значение: страницата fig04_01.html и стиловият ѝ лист fig04_01/styles.css са точно тази изходна точка и я имаш цялата по-долу. Всички страници на този урок са в programas/04-flexbox-grid/ на хранилището на курса. Обърни внимание на една подробност, преди да ги отвориш: всички, освен подреденото табло, зареждат изходния стилов лист с <link rel="stylesheet" href="fig04_01/styles.css">, относителен път, който браузърът търси в папката fig04_01 до страницата. Затова работят, ако изтеглиш (или клонираш с Git) цялото хранилище и пуснеш сървъра от папката му programas/, или ако копираш всяка страница заедно с папката fig04_01. Ако копираш само .html, страницата ще се види без стилове: браузърът е поискал fig04_01/styles.css, сървърът е отговорил с 404 и разделът „Мрежа“ на инструментите ти го показва в червено. fig04_06.html, подреденото табло, с което завършва урокът, по същия начин ползва собствения си стилов лист, fig04_06/styles.css. За този урок работиш с два файла от папката си revisor, index.html и css/styles.css, а за самостоятелните експерименти — със страници за изпробване до тях. От папката на проекта локалният сървър си остава python3 -m http.server 8000 --bind 127.0.0.1.

Какво не прави този урок. Още не решава телефона: страницата да върши работа при ширина 320 пиксела е цялата тема на Урок 5, който започва точно оттам, където този свършва. Нито пипа JavaScript. Числата на обобщението си остават написани на ръка; една програма ще ги преброи в Урок 6, а Урок 7 ще ги начертае в таблицата. Днес се променя само къде стои всяко нещо и колко е голямо, не какво казва.

След урока ще можеш да

  • Обясняваш какво са flex контейнер, неговите елементи и двете му оси и предвиждаш какво правят justify-content и align-items, преди да презаредиш.
  • Пишеш лента с контроли с display: flex, flex-wrap, gap, align-items и flex и казваш кой получава мястото, което остава.
  • Четеш свойството flex като основа, растене и свиване и разпознаваш кога един елемент отказва да се свие и как му се позволява.
  • Избираш Flexbox, когато разпределението е в едно измерение, и Grid, когато са две, и обосноваваш избора с таблото пред себе си.
  • Декларираш решетка с grid-template-columns, единицата fr и repeat() и обясняваш откъде идват редовете, които не си декларирал.
  • Мериш с инструментите на браузъра къде започва и къде свършва всяка кутия, вместо да съдиш на око.
  • Пазиш реда на HTML като ред на четене, без да преподреждаш с order онова, което клавиатурата обхожда.

Защо, преди как

Отвори fig04_01.html на локалния си сървър, с прозореца на браузъра много широк, като на настолен компютър. Което виждаш, е таблото такова, каквото го остави Урок 3: четливо, със своите цветове и значки, но подредено като списък за пазар. Заглавието, датата и навигационните връзки стоят една под друга, макар да има място да са на един ред. Четирите числа на обобщението са четири реда, един под друг, всеки на цяла ширина. Полето за търсене, филтрите и бутонът също са натрупани. А таблицата е най-долу, след всичко останало.

Измери го, вместо да го предполагаш. Измерих го с Chrome 154, автоматизирано и без прозорец, с прозорец от 1440 пиксела: съдържанието е колона от 60 rem (960 px), центрирана, и вътре в нея всяка кутия заема цялата ширина, която има: заглавието, датата и навигацията са по 928 px всяко, а вътре в секциите, с вътрешния им отстъп, числото „Caídos“ (паднали) е 878 px, като таблицата и абзацът, в който живее бутонът „Revisar ahora“ (провери сега). Бутонът — не: той е около 135 px, защото бутонът е елемент в ред и заема само онова, което му трябва за текста; онова, което се разтяга, е абзацът, който го съдържа. Нищо не прелива и нищо не е написано погрешно, но мястото се пилее: за да стигнеш до таблицата, която е онова, за което човекът отваря таблото, трябва да минеш през три реда заглавна част, четири числа и три контрола, всяко на собствен ред. Четирите числа биха се събрали със запас на един ред, а трите контрола — също.

Причината е една. Досега таблото не решава как да подреди онова, което носи вътре. Един блок стои под друг, защото това прави нормалният поток на страницата, и всяка кутия е на цяла ширина, защото никой не ѝ е казал друго. Този урок го учи да решава с двата инструмента, които CSS има за това: Flexbox, за да разпределя неща по една линия, и Grid, за да ги подрежда едновременно в редове и колони.

Едно честно предупреждение от самото начало, за да не те свари неподготвен: в края на този урок таблото ще изглежда добре на широк екран и все още не на телефон. Ако смалиш прозореца до 320 пиксела, страницата продължава да прелива. Това не е пропуск: това е различен проблем, със свои инструменти, и заема целия следващ урок. Тук се учиш да подреждаш; в Урок 5 — подреждането да върши работа при всякаква ширина.

Как ще проверяваш

Едно подреждане се оценява зле на око: две кутии, които „изглеждат“ еднакво широки, се различават с двайсет пиксела, а една пролука, която „изглежда“ равна, не е. Затова в този урок ще мериш и инструментът вече го имаш: инструментите на браузъра, които срещна в Урок 1.

Отвори ги с F12 и влез в раздела за елементите (в Chrome и Edge се казва „Елементи“; във Firefox — „Инспектор“). Премини с курсора над който и да е таг от дървото на документа: браузърът засенчва тази кутия върху страницата и показва малък етикет с името ѝ и размерите ѝ, например в fig04_01.html при 1440 px, dl 878 × 297.38 за списъка на обобщението. Първото число е ширината, второто височината, в CSS пиксели. Ако щракнеш върху тага, панелът отдясно ти показва в раздела „Изчислено“ (Computed) цялата ѝ кутия: съдържание, вътрешен отстъп, рамка и външен отстъп. А когато един елемент е flex или grid контейнер, дървото му слага до него малка значка, която казва flex или grid; като щракнеш върху нея, браузърът чертае върху страницата линиите на решетката или контура на всеки flex елемент. Тази значка е най-бързият начин да разбереш дали едно свойство за подреждане действа, или не.

За да разбереш къде започва и къде свършва една кутия, което е онова, с което ще проверяваш повечето резултати от този урок, служи адаптивният режим (иконата на телефон и таблет в Chrome и Edge; Ctrl+Shift+M във Firefox): там въвеждаш точната ширина на прозореца, а линийката отгоре ти дава позициите. Когато в този урок прочетеш „заглавието е от 256 до 553 px“, значи че левият му ръб е на 256 пиксела от левия ръб на прозореца, а десният му — на 553: същото, което би видял, като преминеш с курсора над него при прозорец с тази ширина.

Понятията

Две идеи, в този ред: Flexbox, който подрежда неща в една линия, и Grid, който подрежда неща едновременно в редове и колони. Всяка се обяснява първо с минимален пример, в страница за изпробване, която можеш да отвориш и да измериш, а после се прилага към таблото.

4.1 Flexbox: подреждане в едно измерение

4.1.1 Контейнер, елементи и двете оси

По подразбиране децата на един елемент се подреждат според нормалния поток: блоковите елементи (<p>, <div>, <section>) стоят един под друг и заемат цялата налична ширина, а редовите (<span>, <a>, <label>) текат като думите на един ред. Нормалният поток е причината таблото от предишния урок да е дълга колона.

Flexbox е алтернативен режим на подреждане, който се включва с една-единствена декларация върху родителя:

.container {
  display: flex;
}

С нея родителят става flex контейнер, а непосредствените му деца стават flex елементи. Само непосредствените деца: внуците не разбират. Първото, което се забелязва, е че децата, които преди стояха едно под друго, сега стоят едно до друго, в ред. Но важното не е редът: важното е, че контейнерът вече разпределя мястото между децата си и ти му казваш как.

Flexbox работи с две оси и почти всички грешки на начинаещия идват от смесването им. Главната ос е посоката, в която се подреждат елементите; напречната ос е перпендикулярната. Със стойността по подразбиране, flex-direction: row, главната ос е хоризонтална (отляво надясно на български) и напречната е вертикална. Ако напишеш flex-direction: column, те се разменят: главната става вертикална. Две свойства се опират на това разграничение и затова си струва да го научиш преди имената:

  • justify-content разпределя елементите по главната ос.
  • align-items ги подравнява по ширината на напречната ос.

Отвори fig04_02.html, за да го видиш. Това е страница за изпробване с две кутии с пунктирани линии, всяка с по три сини елемента с различни височини:

<!-- fig04_02.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Los dos ejes de Flexbox</title>
  <link rel="stylesheet" href="fig04_01/styles.css">
  <style>
    .box {
      display: flex;
      flex-direction: row;
      justify-content: space-between;
      align-items: center;
      gap: 0.5rem;
      height: 9rem;
      padding: 0.5rem;
      background: var(--color-surface);
      border: 2px dashed var(--color-border);
    }

    .box > div {
      padding: 0.5rem 1rem;
      color: #ffffff;
      background: var(--color-accent);
      border-radius: var(--radius);
    }

    .box > div:nth-child(2) { height: 5rem; }
    .box > div:nth-child(3) { height: 2.5rem; }

    .box-column {
      flex-direction: column;
      align-items: flex-start;
      height: 14rem;
    }
  </style>
</head>
<body>
  <main>
    <h1>Los dos ejes</h1>

    <h2>flex-direction: row (el eje principal es horizontal)</h2>
    <div class="box">
      <div>Uno</div>
      <div>Dos</div>
      <div>Tres</div>
    </div>

    <h2>flex-direction: column (el eje principal es vertical)</h2>
    <div class="box box-column">
      <div>Uno</div>
      <div>Dos</div>
      <div>Tres</div>
    </div>
  </main>
</body>
</html>

В първата кутия (row) justify-content: space-between залепя „Uno“ (едно) за левия ръб, „Tres“ (три) за десния и оставя „Dos“ (две) по средата, с оставащото място, разпределено по пролуките. align-items: center центрира трите вертикално, затова, макар да имат различни височини, всички споделят една и съща централна линия. Във втората кутия се променя само посоката: главната ос вече е вертикална, така че justify-content: space-between разпределя надолу („Uno“ горе, „Tres“ долу), а align-items: flex-start залепя всичко за левия ръб, който сега е ръбът на напречната ос. Свойствата не са променили значението си: променили са посоката си. Това означава „осите“.

Стойностите, които ще ползваш най-много, са малко:

Свойство Стойност Какво прави
justify-content flex-start (по подразбиране) Залепя елементите в началото на главната ос
justify-content center Събира ги в средата
justify-content space-between Първият в началото, последният в края, останалите разпределени между тях
justify-content flex-end Залепя ги в края
align-items stretch (по подразбиране) Всеки елемент се разтяга, докато запълни напречната ос
align-items flex-start / center / flex-end Подравнява в началото, в средата или в края на напречната ос
align-items baseline Подравнява по базовата линия на текста, полезно когато размерите на буквите се различават

Една полезна подробност: стойността по подразбиране на align-items е stretch и затова, щом включиш display: flex, елементите на един ред, които нямат собствена височина, се разтягат до височината на реда и стават еднакво високи. Страничен ефект, който изненадва първия път. Условието има значение: stretch разтяга само този, чиято височина е auto; елемент с написана height запазва своята и остава залепен в началото на напречната ос. Измерих го, като махнах от първата кутия на fig04_02.html align-items: center, така че да остане stretch по подразбиране: кутията е 144 px отвън и ѝ остават 124 отвътре, след като се приспаднат вътрешният ѝ отстъп и рамката. „Uno“, което няма собствена височина, се разтегна до тези 124 px; „Dos“ и „Tres“, които я имат написана (5rem и 2.5rem), останаха на 80 и 40 px, залепени горе.

4.1.2 Мястото между елементите: gap

Между flex елементите трябва да оставиш въздух. Старият навик беше да се слага margin на всяко дете и се срещаха два проблема: последният елемент остава с излишен отстъп и когато елементите слизат на нов ред, трябва да налучкваш кои отстъпи са излишни. Свойството gap, което се пише в контейнера, решава и двете неща: слага мястото само между елементите, никога по краищата, и еднакво хоризонтално и вертикално. Можеш да дадеш една стойност (gap: 1rem) или две (gap: 0.5rem 1rem: първо мястото между редовете, после мястото между колоните).

gap работи във Flexbox и в Grid и днес е едно от нещата, които можеш да ползваш без страх, макар да е стигнало до двата на различни дати и е добре да го знаеш, защото ще видиш стар код с външни отстъпи на негово място. В Grid MDN го отбелязва като наличен във всички браузъри от октомври 2017 г.. Във Flexbox се забави повече: последният браузър, който го прие, беше Safari 14.1, през април 2021 г., а платформата го води като широко достъпен (widely available, което значи „наличен във всички от поне 30 месеца“) от октомври 2023 г. Правилото, което ще следваш в този курс, е просто: разстоянието между братята го слага родителят с gap; външните отстъпи се пазят за отделянето на блок, който не му е брат.

4.1.3 Колко е голям всеки елемент: flex

Дотук разпределяхме оставащото място. Остава да кажем какво става, когато елементите не се събират или когато остава и някой трябва да се възползва. Три числа контролират това и се пишат заедно в свойството flex:

flex: <flex-grow> <flex-shrink> <flex-basis>;
  • Основата (flex-basis) е изходният размер на елемента по главната ос. С auto е този, който би имал според свойството му за размер по тази ос —width в ред, height в колона (така го определя спецификацията)— или, ако няма, според съдържанието му.
  • Растене (flex-grow) казва колко от оставащото място получава елементът. С 0 не получава нищо. С 1 всички елементи със стойност 1 си поделят остатъка на равни части; такъв с 2 получава двойно повече от такъв с 1.
  • Свиване (flex-shrink) казва колко отстъпва елементът, когато мястото не стига. С 0 не отстъпва никога; с 1 отстъпва пропорционално.

Началните стойности са 0 1 auto: не расте, свива се, заема колкото съдържанието му. Затова, без да го искаш, един flex контейнер, пълен с елементи, се стяга, преди да прелее. Има съкращения, които е добре да разпознаваш: flex: 1 значи 1 1 0 („заемай всичко, което остава, от нула“), flex: auto е 1 1 auto и flex: none е 0 0 auto („фиксиран размер“).

Има капан, който струва един следобед първия път. Един flex елемент не се свива под минималния размер на съдържанието си: дълга дума без интервали, изображение, текстово поле с фиксирана ширина. MDN го описва така: един елемент може да се свие до размера си min-content и не повече. Когато това стане, елементът остава широк, а контейнерът прелива. Решението има две части: да кажеш на елемента, че може да се свие повече, с min-width: 0 (в ред; в колона, където главната ос е вертикална, еквивалентното свойство е min-height: 0), и да дадеш на съдържанието му начин да се побере на по-малко място (да го отрежеш с многоточие или да го оставиш да слезе на нов ред). Само с първата част елементът се свива, но текстът излиза извън него.

Виж го в fig04_03.html. Това са два еднакви реда, всеки с името на една услуга, нейния адрес (дълъг URL, който няма интервали, където да се прекъсне) и значката за състояние. Адресът носи три декларации, които искат „ако не се побираш, отрежи се с многоточие“: overflow: hidden, white-space: nowrap и text-overflow: ellipsis. Единствената разлика между двата реда е класът can-shrink, който добавя min-width: 0 към кутията с името:

<!-- fig04_03.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>El mínimo de un elemento flex</title>
  <link rel="stylesheet" href="fig04_01/styles.css">
  <style>
    .service {
      display: flex;
      align-items: center;
      gap: 1rem;
      padding: 0.5rem;
      border: 2px dashed var(--color-border);
    }

    .service-name {
      flex: 1 1 auto;
    }

    .service-name h2,
    .service-name p {
      margin: 0;
    }

    .address {
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
      color: var(--color-muted);
    }

    .can-shrink {
      min-width: 0;
    }
  </style>
</head>
<body>
  <main>
    <h1>El mínimo de un elemento flex</h1>

    <div class="service" id="one">
      <div class="service-name">
        <h2>Inventario</h2>
        <p class="address">https://inventario.example.com/salud/v2/estado-completo</p>
      </div>
      <span class="status status-down">Caído</span>
    </div>

    <div class="service" id="two">
      <div class="service-name can-shrink">
        <h2>Inventario</h2>
        <p class="address">https://inventario.example.com/salud/v2/estado-completo</p>
      </div>
      <span class="status status-down">Caído</span>
    </div>
  </main>
</body>
</html>

Измерих го при 320 px, ширината на малък телефон. В първия ред кутията с името отказва да е по-малка от целия URL (417 px), избутва значката извън екрана и страницата е 514 px: прелива, макар адресът да „умее“ да се реже. Причината е горната: автоматичният минимум на един flex елемент е ширината на съдържанието му, а целият URL е това съдържание. Във втория ред, с min-width: 0, кутията с името слиза на 197 px, URL се реже с „…“ и значката остава вътре, на 26 px от десния ръб на прозореца. При 768 px или повече двата реда изглеждат еднакво, защото там URL се побира цял: капанът се появява само когато мястото не стига и затова остава незабелязан на екрана на този, който програмира.

В таблото от този урок не е нужно: нито един flex елемент на таблото няма съдържание, което да отказва да се свие. Но идеята се връща в Урок 5 под друго име: колона 1fr на Grid има същия автоматичен минимум, а решението, minmax(0, 1fr), е същата идея да напишеш нулевия минимум нарочно.

4.1.4 flex-wrap: да слезеш на нов ред вместо да се стягаш

По подразбиране един flex контейнер има flex-wrap: nowrap: всички елементи вървят в един-единствен ред и ако не се побират, се свиват (а ако вече не могат да се свият, преливат). С flex-wrap: wrap елементите, които не се побират, слизат на следващия ред. MDN го казва с полезно изречение: когато има няколко реда, всеки се държи като отделен flex контейнер. Това значи, че justify-content и flex-grow действат вътре във всеки ред, не върху цялото.

Решението кой в кой ред се побира се взема по основата на всеки елемент, преди растене или свиване, но коригирана според границите му: ако основата е по-малка от min-width (или по-голяма от max-width), брои се границата, а също се броят и външните му отстъпи. Спецификацията го нарича хипотетичен главен размер. Измерих го в Chrome 154: два елемента с основа 200 px се побират заедно в ред от 500 px; ако на втория му сложиш min-width: 320px, слиза на следващия ред, макар основата му да не се е променила. Ето трикът, който държи почти всичко „адаптивно“, без да пишеш нито една медийна заявка и който Урок 5 ще използва в дълбочина: ако дадеш на един елемент разумна основа и му позволиш да расте, браузърът се грижи да го подреди. Основа от 14rem казва „предпочитам да съм около 224 px; сложи ме в един ред с този, който се побира до мен; ако остава място, раздели го“. На широк екран в реда се побират няколко; на тесен всеки слиза и заема цял ред. Никой не е написал „при 600 px направи еди-какво“: съдържанието решава.

4.1.5 Решен пример: лентата с контроли

Секцията „Servicios“ (услуги) на таблото има днес три контрола един под друг: полето за търсене с етикета му, групата филтри (трите радио бутона) и бутонът „Revisar ahora“. На широк екран естественото е един ред; на телефон — три реда. Това е идеалният случай за Flexbox, защото са братя, които си поделят една линия.

Първо трябва да подготвиш HTML. Трите контрола вече ги имаш; трябва да ги обгърнеш в контейнер с име, за да можеш да сочиш към него от CSS. А полето с етикета му се групира, за да пътуват заедно. Това е единствената промяна в HTML на тази стъпка:

<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>

Забележи какво не беше пипнато: елементите си остават тези от Урок 2, със свързаните си етикети. Единственото ново е един контейнер div и един клас. Значението не се промени; просто се добави място, към което да се сочи. Сега CSS:

<!-- fig04_04.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Barra de controles con Flexbox</title>
  <link rel="stylesheet" href="fig04_01/styles.css">
  <style>
    .controls {
      display: flex;
      flex-wrap: wrap;
      align-items: flex-end;
      gap: 1rem;
    }

    .controls p,
    .controls fieldset {
      margin: 0;
    }

    .field {
      display: flex;
      flex-direction: column;
      gap: 0.25rem;
      flex: 1 1 14rem;
    }

    .field input {
      width: 100%;
    }
  </style>
</head>
<body>
  <main>
    <h1>Barra de controles</h1>
    <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>
  </main>
</body>
</html>

Ред по ред:

  • .controls { display: flex; flex-wrap: wrap; ... } включва Flexbox и позволява слизане на нов ред. Трите непосредствени деца (полето, групата и бутонът) са сега flex елементи.
  • align-items: flex-end ги подравнява по долния им ръб. Така полето, групата филтри и бутонът споделят една и съща основа, макар височините им да са различни, и изглеждат като една-единствена лента. Със стойността по подразбиране, stretch, бутонът би се разтегнал до височината на групата.
  • gap: 1rem слага мястото между тях, без разпръснати външни отстъпи.
  • .controls p, .controls fieldset { margin: 0 } маха външните отстъпи, които носеха по подразбиране; сега мястото го слага родителят.
  • В .field, който от своя страна е flex контейнер, flex-direction: column подрежда етикета върху полето, а flex: 1 1 14rem казва на полето: „имай основа 14rem, можеш да растеш, можеш да се свиваш“. То е единственото от трите, което расте, и затова поема оставащото място на реда.
  • .field input { width: 100% } кара полето да запълни контейнера си. Без този ред абзацът .field би растял, но полето би останало с ширината, която браузърът му дава по подразбиране на текстово поле, и спечеленото място би останало празно отдясно.

Заредих тази страница при пет ширини (320, 375, 768, 1024 и 1440 px) и при всичките пет ширината на страницата е равна на тази на прозореца: лентата не прелива в нито една. При 320 и при 375 px контролите образуват три реда; от 768 px нагоре — един-единствен ред, с полето, заемащо всичко, което остава. При 1440 px го измерих част по част: полето е от 256 до 672.5 px (416.5 широко, много повече от основата му от 224), групата радио бутони от 688.5 до 1033, а бутонът от 1049 до 1184, залепен за десния ръб на съдържанието. Между една част и друга са 16-те px на gap.

Има едно свойство на Flexbox, за което трябва да знаеш, точно за да не го ползваш лекомислено: order. Позволява да смениш визуалния ред на елементите, без да местиш HTML. Проблемът е, че клавиатурата и екранният четец следват реда на кода, не този на екрана: който навигира с Tab, би скачал от единия край на лентата към другия, без да разбира защо. Това нарушава критерий 1.3.2 („Смислена последователност“) и 2.4.3 („Ред на фокуса“) от указанията. Правило на курса: редът на HTML е редът на четене; ако една кутия трябва да е първа, пише се първа.

4.2 Grid: подреждане в две измерения

4.2.1 Flexbox или Grid: въпросът, който решава

Flexbox подрежда в една линия (с редове, които слизат, ако му позволиш). Grid подрежда едновременно в редове и колони: определя решетка и поставя всеки елемент в клетка. Приличат си по това, че и двата започват с декларация display в родителя и двата приемат gap. Различават се по това кой командва.

Във Flexbox командва съдържанието: елементите са тези, които искат място, а контейнерът го разпределя. Затова е перфектен за лента с контроли, заглавна част със заглавие и дата, ред от бутони: групи неща, чийто размер зависи от онова, което казват. В Grid командва решетката: колоните съществуват първо, а елементите се подреждат в тях, така че остават подравнени и хоризонтално, и вертикално. Затова е перфектен за карти с обобщение, група от числа или скелета на страницата.

Един практичен въпрос за решаване: искам ли нещата от втория ред да са подравнени с тези от първия? Ако да, е Grid. Ако всеки ред се подрежда сам, е Flexbox. И не се изключват: в таблото ще ползваш и двата, единия вътре в другия.

4.2.2 Колони, fr и repeat()

За да включиш Grid, пишеш display: grid в родителя и му казваш колко колони има с grid-template-columns:

.summary {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 0.75rem;
}

Новата единица е fr, от fraction, дял: „един дял от наличното място“. Три колони от 1fr разпределят ширината на три равни части; 1fr 2fr би дало на втората двойно повече от първата; 200px 1fr фиксира първата колона и оставя на втората всичко останало. Да повтаряш три пъти едно и също е уморително и затова има repeat(): repeat(3, 1fr) е същото като 1fr 1fr 1fr.

Не е нужно да му казваш колко реда има. Ако има шест елемента и три колони, браузърът създава два реда сам. MDN ги нарича неявна решетка: тази, която се разширява, когато има съдържание извън онова, което си декларирал (декларираната е явната). Височината им се контролира с grid-auto-rows; по подразбиране всеки ред е колкото иска съдържанието му.

Един начин да се увериш, че си разбрал неявната решетка: с четири елемента и repeat(2, 1fr) колко реда има? Два, с по два елемента, и никой не ги е декларирал. Упражнение 2 те моли да го провериш с числата на обобщението.

4.2.3 Решен пример: обобщението в четири колони

Обобщението на таблото е учебникарският случай за Grid: четири числа, които е добре да се виждат заедно, всяко с името си отгоре и стойността си отдолу, и подравнени помежду си. Преди CSS, HTML. Обобщението от Урок 2 е списък с описания (<dl>) с четири групи от термин и стойност и няма какво да се променя в значението му. Само му се добавя клас, към който да се сочи:

<dl class="summary">

Нито е нужно да пипаш четирите кутии (div), които вече обгръщаха всяка двойка от термин и стойност: всяка става елемент на решетката. Това става, когато HTML вече е имал правилната структура: Grid само чете онова, което вече е било написано. Страницата за изпробване fig04_05.html декларира четири равни колони:

<!-- fig04_05.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Resumen en cuatro columnas fijas</title>
  <link rel="stylesheet" href="fig04_01/styles.css">
  <style>
    .summary {
      display: grid;
      grid-template-columns: repeat(4, 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>

Ред по ред: display: grid превръща списъка в grid контейнер, а четирите му div — в елементи на решетката; grid-template-columns: repeat(4, 1fr) иска четири колони, които си поделят ширината на равни части; gap: 0 1rem не оставя място между редовете (има само един) и оставя 16 px между колоните; а margin: 0 маха външния отстъп, който списъкът с описания носи по подразбиране, така че мястото да го слага този, който го съдържа. Измерих го при 1440 и при 1024 px: четирите числа остават на един ред, всяка колона точно 220 px, с 16 px между две съседни. Сметките излизат: контейнерът е 928 px, трите пролуки правят 48, а останалите 880, разделени на четири, дават 220.

Един разумен въпрос, преди да продължим: като променим display на елемент със собствено значение, не му ли отнемаме значението? Проверих го в Chrome 154 с дървото на достъпността: списъкът с описания с display: grid запазва термините и определенията си и дори таблица с display: grid запазва ролята си на таблица. В други браузъри и с други екранни четци няма гаранция: Адриан Розели, който от години мери това, предупреждава, че смяната на display на таблица може да отнеме навигацията по клетки на този, който ползва четец. Тъй като няма нужда да рискуваш, правилото на курса е: не се сменя display на таблица; сменя се този на обвивката ѝ. Урок 5 го прилага.

А сега честната част от примера. Смали прозореца до 320 px и мери отново: четирите числа се стягат, всяко се разчупва на няколко реда и въпреки това страницата е 344 px широка, повече от прозореца. Четири фиксирани колони са добро решение, когато има място, и лошо, когато няма, а 1fr има скрит минимум, който не позволява на колоната да се свие повече от най-дългата си дума. Защо става така и редът CSS, който кара решетката да брои сама колко колони се побират, са първото понятие на Урок 5. Засега запази въпроса, на който ще умееш да отговориш, когато го завършиш: колко колони се побират при 320 px и кой трябва да го реши?

4.2.4 Подреденото табло

С горното вече имаш всички части, за да подредиш цялото табло на широк екран. Таблото на урока е fig04_06.html. Промените спрямо Урок 2 са малко и никоя не променя онова, което HTML казва: класът page-header върху заглавната част, класът summary върху списъка с описания и div.controls с неговия p.field около трите контрола, точно както в 4.1.5. Таблицата си остава абсолютно същата.

<!-- fig04_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="fig04_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>
    <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>

Едно предупреждение, преди да погледнеш стиловия лист: заглавната част в ред (.page-header) е стъпка 1 на Упражнение 1. Ако искаш да го решиш сам, направи го, преди да четеш CSS. Стиловият лист е този от Урок 3 без да е пипнато нито едно от правилата му, с нов блок в края на слоя components; коментарите сочат къде започва:

/* fig04_06/styles.css */
/* La hoja del panel al terminar la Lección 4: la de la Lección 3, sin tocar una
   sola de sus reglas, más un bloque al final con Flexbox y Grid. */

/* 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;
  }
}

Новият блок има три части и познаваш всяка. Заглавната част (.page-header) е flex контейнер, който разпределя трите си деца със space-between, подравнява ги по базовата линия на текста и им позволява да слязат на нов ред; абзацът ѝ губи външния отстъп, така че датата да не остава по-ниско от заглавието. Лентата с контроли (.controls, .field) е тази от 4.1.5, с числата, заменени с променливите за пространство от Урок 3 (var(--space-3) е същото 1rem). А обобщението (.summary) е решетката с четири колони от 4.2.3. Онова, което не се е променило, има същото значение като онова, което се е променило: слоевете, променливите, фокусът и значките са тези от Урок 3. Подреждането се добавя към онова, което е имало; не го пренаписва.

Измери го. При 1440 px, с инструмента за инспекция, заглавието на заглавната част е от 256 до 553 px, датата от 617 до 938, а навигацията от 1002 до 1184, залепена за десния ръб на съдържанието: един-единствен ред. Четирите числа на обобщението остават в ред от четири колони по 207.5 px (по-малки са от 220-те на страницата за изпробване, защото сега живеят вътре в секция, с нейния вътрешен отстъп и рамка). А лентата с контроли е един-единствен ред, с полето от 281 до 647.5 px, радио бутоните от 663.5 до 1008 и бутона от 1024 до 1159. Което в изходната страница бяха десет натрупани реда, сега са три ивици: заглавната част, обобщението и лентата, с таблицата точно отдолу. Таблицата, която е онова, за което се идва, се качва с повече от 400 пиксела: в fig04_01.html започваше на 859 px от горната част на страницата, а в fig04_06.html започва на 419.

При 768 px всичко продължава да не прелива, а заглавната част и лентата вече се разпределят на повече от един ред от само себе си, благодарение на flex-wrap. При 375 и 320 px, напротив, страницата е 414 px: таблицата продължава да е по-широка от прозореца, както от Урок 3, а сега я придружава обобщението, чиито четири фиксирани колони стигат до 369 px. Това е точната точка, от която започва Урок 5.

4.2.5 Пренеси го в своя revisor

Дотук работеше със страниците на хранилището. Липсва стъпката, която превръща наученото в твоя проект: твоят revisor да остане същият като fig04_06.html, измерен и запазен в Git. Те са четири стъпки и никоя не изисква да пишеш нещо ново.

1. Стиловият лист. Отвори ~/revisor/css/styles.css и постави в края на слоя components, точно преди последната затваряща скоба }, блока, който започва с коментара /* ---- Lección 4: Flexbox y Grid ---- */ (имаш го цял по-горе или в fig04_06/styles.css на хранилището). Не трий нищо от онова, което е имало: блокът само добавя. Ако предпочиташ, можеш да замениш целия стилов лист с fig04_06/styles.css, който е този от Урок 3 с този блок; но ако в Урок 3 си направил собствени промени в стиловия си лист (друг цвят, друг размер), заменянето ще ги изтрие и в такъв случай е по-добре да поставиш само блока.

2. Страницата. В твоя ~/revisor/index.html направи трите промени в HTML:

  • <header> става <header class="page-header">.
  • <dl> става <dl class="summary">.
  • Полето за търсене, <fieldset> и абзацът на бутона остават вътре в <div class="controls">, а абзацът на полето носи class="field", точно както в 4.1.5. Внимавай къде затваряш div: след абзаца на бутона и преди таблицата.

Можеш също да копираш цялата fig04_06.html върху твоя index.html, с една предпазна мярка: в хранилището нейният <link> сочи към fig04_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/, включи адаптивния режим с прозорец от 1440 px и сравни твоето табло с fig04_06.html при същата ширина: заглавната част в един ред, четирите числа в един ред и лентата с контроли в един ред. Премини с курсора над .summary в раздела за елементите: трябва да има значката grid, а .controls и .page-header — значката flex. Ако някоя я няма, е клас, който не е написан, или div, който е затворен на друго място. Не се тревожи, ако при 320 px твоето табло още прелива: това на урока също, и по същата причина.

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 "Acomoda el panel con Flexbox y Grid"
$ git status
En la rama main
nada para hacer commit, el árbol de trabajo está limpio

git commit отпечатва ред с кода на commit и колко реда са се променили; числата зависят от твоите файлове. С това твоят revisor е готов за Урок 5, който започва точно оттук.

Изходният стилов лист, fig04_01/styles.css, е този от Урок 3 такъв, какъвто е; имаш го цял по-долу, в случай че искаш да сравниш или нямаш този от предишния урок:

/* fig04_01/styles.css */
/* La hoja del panel tal como la deja la Lección 3, sin un solo cambio: todavía
   no acomoda nada, y cada cosa va debajo de la anterior. */

/* 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);
  }
}

а изходната страница, fig04_01.html, е таблото от Урок 2 с един <link> към този стилов лист:

<!-- fig04_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="fig04_01/styles.css">
</head>
<body>
  <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>
        <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>
        <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>

      <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>

Грешката, която ще видиш

Грешките в подреждането почти никога не са съобщения: това е страница, която изглежда „зле“, без да каже защо. Двете, които най-често ще срещнеш с Flexbox, се учат най-добре, като ги предизвикаш нарочно. Първата не оставя никаква следа в конзолата, само дискретна подсказка в инструментите на браузъра; втората се вижда, щом прозорецът се стесни.

Грешка в подреждането, която не предупреждава: свойството, което не прави нищо. Отвори fig04_07.html. Това е лента от три бутона с justify-content: space-between, а бутоните остават залепени отляво, един след друг, без да се разпределят:

<!-- fig04_07.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Una propiedad de Flexbox sin contenedor</title>
  <link rel="stylesheet" href="fig04_01/styles.css">
  <style>
    .controls {
      justify-content: space-between;
      gap: 0.75rem;
    }
  </style>
</head>
<body>
  <main>
    <h1>Los botones no se reparten</h1>
    <div class="controls">
      <button type="button">Todos</button>
      <button type="button">Disponibles</button>
      <button type="button">Caídos</button>
    </div>
  </main>
</body>
</html>

В конзолата няма нищо. Браузърът не счита това за грешка: просто игнорира свойството, защото justify-content няма ефект върху обикновен блок (действа само върху flex, grid или многоколонни контейнери), а .controls е обикновен блок (липсва display: flex). Което има, е подсказка в инструментите на браузъра: отвори раздела за елементите, избери .controls и погледни панела му със стилове; декларацията justify-content: space-between се появява приглушена, с иконка за предупреждение до нея, която, като преминеш с курсора, обяснява, че свойството няма ефект, защото елементът не е нито flex, нито grid контейнер. (Точният текст се променя според браузъра и езика му; иконата и приглушената декларация са сигналът.) Поправката е един ред: display: flex. Когато свойство за подреждане „не прави нищо“, първото, което се проверява, е дали родителят му е контейнер.

Елементът, който не се свива. Вторият случай е този от 4.1.3 и си струва да го разпознаваш отдалече, защото се маскира: отваряш fig04_03.html при 320 px и се появява хоризонтална лента за превъртане, макар дългият текст да има всичко необходимо, за да се отреже с многоточие. Сигналът е, че елементът, който излиза от ръба, е контейнерът на текста, не текстът: като преминеш с курсора през раздела за елементите, кутията .service-name на първия ред е 417 px в прозорец от 320. Един flex елемент не се свива под минималното си съдържание, а URL без интервали е огромно минимално съдържание. Поправката са двете части, които вече познаваш: min-width: 0 върху flex елемента и начин на съдържанието му да се побере. Ако сложиш само втората, текстът „умее“ да се реже, но кутията му никога не го моли за това.

Какво се прави погрешно

Да разделяш братята с външни отстъпи във всяко дете. margin-right: 1rem на всеки бутон от една лента. Последният остава с излишен отстъп и когато лентата слезе на нов ред, отстъпите вече не съвпадат с пролуките. Цена: корекции с :last-child и магически числа, които никой не разбира след месец. Поправка: мястото между братята го слага родителят с gap.

Да слагаш свойството за подреждане в детето, а не в родителя. justify-content или grid-template-columns във всяка карта, вместо в контейнера, който ги разпределя. Не правят нищо и няма грешка, която да го каже: това е случаят на fig04_07.html в друга премяна. Преди да напишеш свойство за подреждане, попитай се кой разпределя; там отива.

Да ползваш order, за да преподредиш онова, което се чете. Да преподредиш с CSS нещо, което клавиатурата обхожда в друг ред. Цена: Tab скача от единия край към другия и екранният четец чете в ред, който не съвпада с онова, което се вижда. Оправя се, като напишеш HTML в реда, в който трябва да се чете.

Фиксирани ширини в пиксели за всичко. width: 640px в колона, input { width: 20rem } в поле. Изглеждат добре на екрана на този, който ги е написал, и пречат на всеки друг. Таблото се е спасило от това, защото стиловият лист от Урок 3 не е дал на нищо фиксирана ширина, а и лентата с контроли от този урок също: дала е на полето една основа (14rem), която може да расте и да се свива, а това не е същото като ширина. Алтернативата на написаното число: width: 100%, max-width или да оставиш ширината на flex и на grid.

Упражнения

Упражнение 1 — Заглавната част в един ред

<header> на таблото има три деца: заглавието <h1>, абзаца „Última revisión“ (последна проверка) и <nav> с двете връзки. В fig04_01.html стоят едно под друго. Ползвай Flexbox, без да пипаш HTML, на две стъпки:

  1. Направи ги ред, със заглавието залепено отляво, <nav> залепен за десния ръб и датата, разпределена между двете, така че на тесен телефон да слизат на нов ред, вместо да се стягат.
  2. Сега промени решението си: заглавието отляво и другите две заедно за десния ръб, едно до друго.

Измери двете стъпки при 1440 и при 320 px с инструментите, като гледаш къде започва и къде свършва всяко дете, и провери, че при 320 px заглавната част не излиза от прозореца.

Упражнение 2 — Две колони, два реда

Копирай fig04_05.html и смени repeat(4, 1fr) с repeat(2, 1fr). Преди да презаредиш, предскажи: колко реда ще има, кой ги е декларирал и колко ще е голяма всяка карта при 1440 px? После мери при 1440, 768 и 320 px. Прелива ли страницата при 320 px, както преливаше тази с четири колони? Обясни защо да или защо не.

Упражнение 3 — Кой остава с остатъка

В fig04_04.html, при 1440 px, полето за търсене е много по-голямо от основата си от 14 rem. Смени flex: 1 1 14rem с flex: 0 1 14rem (само първото число). Предскажи какво става с полето, с бутона и с оставащото място; после мери ширината на полето и позицията на бутона и обясни разликата с това, което казва всяко от трите числа на flex.

Решения

Решение 1

Стъпка 1. Заглавната част има нужда от три неща: да е flex контейнер, да слиза на нов ред и децата да се разпределят. Със justify-content: space-between първото дете остава отляво, последното отдясно, а това в средата — между двете. Това е същото, което прави правилото .page-header в стиловия лист на таблото, което вместо името на елемента ползва клас, а вместо числата — променливите за пространство:

header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.25rem 1rem;
}

С flex-wrap: wrap, когато трите не се побират в един ред, последното слиза само, без медийна заявка. Двете стойности на gap отделят редовете (0.25 rem) по-малко от колоните (1 rem). Подравняването baseline поставя заглавието и малките текстове на една и съща текстова линия, което изглежда по-добре от подравняването по долния ръб на кутиите. Измерих го в fig04_06.html: при 1440 px заглавието е от 256 до 553 px, датата от 617 до 938, а <nav> от 1002 до 1184, залепен за десния ръб на съдържанието; при 320 px и трите слизат, по едно на ред, а заглавната част е точно 320-те px на прозореца.

Стъпка 2. space-between не върши работа, за да събере двете отдясно: разпределя остатъка във всички пролуки и затова датата остава в средата. Което трябва, е остатъкът да отиде в една-единствена пролука, тази след заглавието. Това го прави автоматичен отстъп: във flex контейнер отстъп auto взима цялото оставащо място от своята страна и избутва следващите до края. Променят се два реда:

header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: flex-start;
  gap: 0.25rem 1rem;
}

header h1 {
  margin-right: auto;
}

justify-content се връща към изходната си стойност, която не разпределя нищо, а margin-right: auto на заглавието поглъща цялото свободно място на реда. Измерено по същия начин, при 1440 px: заглавието си остава от 256 до 553, датата минава от 665 до 986, а <nav> от 1002 до 1184; между двете остават точно 16-те px на gap. При 320 px резултатът е същият като в стъпка 1 (по едно на ред). Другият изход, да групираш датата и <nav> в собствен div, също работи, но променя HTML, за да реши нещо, което е само за подреждане.

Таблото на урока остава със стъпка 1: датата по средата разделя визуално заглавието от навигацията. И двете са правилни; важното е да знаеш коя си поискал и с какъв инструмент се постига всяка.

Решение 2

Има два реда по две карти и никой не ги е декларирал: grid-template-columns говори само за колони, а когато четирите елемента не се побират в двете декларирани колони, браузърът създава втория ред по своя инициатива. Това е неявната решетка от 4.2.2. Измерих го: при 1440 px всяка карта е 456 px (928 минус пролука от 16, между две); при 768 px — 360; а при 320 px — 136, без да прелива: страницата е 320.

Защо тази не прелива, а онази с четири колони прелива? Защото скритият минимум на 1fr си е там, но сега в нито един ред не трябва да се побират повече от половината дълги думи: две колони със своя минимум, плюс една пролука, се побират в 288-те px, които страницата оставя. Обърни внимание какво учи това: броят колони, който е подходящ, зависи от ширината, а да го напишеш на ръка те принуждава да избереш един-единствен за всички ширини. Решетката да го брои сама е точно онова, което ще научиш в Урок 5.

Решение 3

С flex: 0 1 14rem полето престава да расте: е колкото основата си, 224 px, вместо 416.5-те, които беше. Групата радио бутони и бутонът не променят размера си (никой не растеше), така че се изместват наляво: бутонът, който свършваше на 1184 px, залепен за десния ръб на съдържанието, сега свършва на 991.5. Оставащите 192.5 px остават празни в края на реда.

Трите числа го обясняват. Първото, растене, казва колко от остатъка получава елементът; с 1 полето беше единственото, което искаше остатък, и си го вземаше целия, а с 0 никой не го иска и то остава там, където падне (в края, защото justify-content е flex-start). Второто, свиване, остава 1: ако лентата се стесни, полето още отстъпва. Третото, основата, не се промени: 14 rem. Затова в лентата на таблото полето носи 1 в началото: това е частта, която е добре да се възползва от мястото, защото по-широко поле за търсене позволява да се види повече от написаното.

Как разбирам, че съм успял

  • Можеш да кажеш, преди да презаредиш, какво правят justify-content: space-between и align-items: center в ред и какво се променя с flex-direction: column; fig04_02.html потвърждава, че си прав.
  • fig04_04.html при 1440 px е един-единствен ред с полето, заемащо онова, което остава, а при 320 px са три реда, без да прелива.
  • Можеш да обясниш защо първият ред на fig04_03.html прелива при 320 px, а вторият не.
  • В fig04_06.html при 1440 px заглавната част, обобщението и лентата са три ивици по един ред всяка, а инструментът за инспекция показва значката grid върху .summary и flex върху .page-header и .controls.
  • С Tab фокусът минава в същия ред като в Урок 3: връзка „Resumen“, връзка „Servicios“, поле за търсене, група радио бутони и бутон „Revisar ahora“. Подреждането не е променило реда на четене.
  • Можеш да кажеш с един въпрос кога да ползваш Flexbox и кога Grid и да дадеш пример от таблото за всеки.
  • Твоят ~/revisor (сервиран от папката си на http://127.0.0.1:8000/) изглежда същият като fig04_06.html при 1440 px, а git log --oneline показва commit-а с подреждането.

Обобщение

За да затвърдиш видяното, отговори, без да поглеждаш урока:

  1. Коя ос контролира justify-content и коя align-items? Какво се променя, когато сложиш flex-direction: column?
  2. Защо gap е по-добър от външен отстъп във всяко дете?
  3. Какво значи flex: 1 1 14rem в един елемент, казано със свои думи?
  4. Защо един flex елемент с дълъг URL вътре може да накара страницата да прелее и кои две неща го оправят?
  5. Кой е въпросът, който решава между Flexbox и Grid?
  6. При шест елемента и repeat(3, 1fr) колко реда има и кой ги е декларирал?
  7. Защо не се преподрежда с order онова, което клавиатурата обхожда?

Ако се запънеш на някой отговор, върни се към съответната точка: това е знак, че там е останала разхлабена част, не че не ставаш за това.

За допълнително четене

Да обсъдим вашия проект

Предпочитате имейл? Пишете ни на hola@habil.mx