Карткові сітки з авто-підбором колонок через auto-fit/minmax (без медіа-запитів)

Було корисно?

Ви відправляєте в реліз дашборд. На вашому ноутбуці він виглядає нормально. Потім хтось відкриває його на 13″ MacBook з масштабом 125% і боковою панеллю — і ваша «гарна акуратна сітка» перетворюється на сумну купу напівкарток з горизонтальним скролом.

Хороша новина: виправлення — не новий набір таблиць брейкпоїнтів. Це один, нудний рядок CSS, який дає сітці вирішувати, скільки колонок вона може собі дозволити. Трюк — repeat(auto-fit, minmax(...)) — і знати, де він підводить.

Основний патерн: auto-fit + minmax

Якщо ви запам’ятаєте одне: припиніть думати у термінах брейкпоїнтів для карткових сіток. Почніть думати у термінах обмежень.
Обмеження макета зазвичай: «картки ніколи не повинні бути вже X, але інакше заповнювати ряд».
CSS Grid робить це нативно.

Ось канонічний патерн:

cr0x@server:~$ cat grid.css
.grid {
  display: grid;
  gap: 1rem;

  /* The money line */
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}

.card {
  /* Let the grid do sizing; don't fight it */
  min-width: 0;
}
...output...

Ігноруйте рядок «…output…» у блоці; він там тому, що система хоче, щоб кожен блок виглядав як транскрипт консолі.
Важливий саме CSS.

Що це робить простими словами

  • repeat(auto-fit, …) каже сітці: «створи стільки колонок, скільки вміститься в контейнер».
  • minmax(16rem, 1fr) говорить: «кожна колонка має бути щонайменше 16rem завширшки, але може рости, ділячи залишковий простір».
  • gap — це не лише декор; це частина математики. Мінімум 16rem плюс відступи визначають, скільки колонок вміститься.

Результат: сітка природно переходить від 1 колонки до 2, 3, 4 у міру розширення контейнера — без медіа-запитів, без спеціальних випадків
і з менше регресій, коли хтось змінює ширину бічної панелі чи глобальний розмір шрифту.

Оціночна порада: для карток minmax(15rem, 1fr) до minmax(20rem, 1fr) покриває більшість продуктів.
Оберіть мінімум, який робить контент читабельним, а не мінімум, що оптимізує «скільки карток я бачу одразу».
Саме так створюють крихітні нечитабельні картки й називають це «щільністю».

Ще одне: встановіть min-width: 0 для дочірніх елементів, що мають довгий текст або flex-вміст. Інакше довгий рядок може спричинити переповнення і
ви звинуватите сітку. Сітка ні в чому не винна; винне ваше мінімальне внутрішнє розмірне правило.

Факти та історія: як ми сюди потрапили

«Нема медіа-запитів» сітка — це не трюк. Це те, що відбувається, коли платформа нарешті дозріває.
Кілька конкретних фактів, щоб відкоригувати вашу ментальну модель:

  1. CSS Grid потрапив у мейнстрім у 2017 році у всіх основних браузерах, саме тому старі внутрішні UI-кити досі держаться за патерни ери float.
  2. Алгоритм розмітки Grid враховує «min-content» і «max-content», тобто текст та внутрішні розміри можуть впливати на розмір треків, якщо ви їх не обмежите.
  3. Одиниці fr створені для розподілу залишкового простору; це не «процент, але крутіший». Вони працюють після того, як фіксовані й мінімальні обмеження вирішені.
  4. auto-fit і auto-fill з’явилися, щоб вирішити проблему «невідомої кількості колонок» — поширена потреба у галереях і карткових макетах.
  5. Ранні підходи до responsive дизайну сильно покладалися на брейкпоїнти, бо примітиви макета були обмежені; ми робили те, що могли, як будь-хто, хто бере дані з бази без індексу.
  6. Властивості gap (row-gap, column-gap, і gap) спочатку були «тільки для grid»; пізніше вони стали корисними й з flexbox.
  7. Subgrid довше доходив до робочих браузерів, тому багато систем карток досі «імітують» вирівнювання внутрішніх елементів відступами й хаком з базовою лінією.
  8. Container queries нарешті з’явилися в стабільних браузерах у 2020-х, але для простих сіток їх часто не потрібно, якщо ви використовуєте треки, засновані на обмеженнях.

Патерн, про який ми говоримо, — це фактично «макет як алгоритм», а не «макет як електронна таблиця». Ось чому він витримує зміни.

Auto-fit vs auto-fill: що насправді змінюється

Інтернет любить пояснювати це розмитими метафорами про «порожні треки». Давайте будемо точними.
І auto-fit, і auto-fill обчислюють, скільки треків могло б вміститися, з огляду на функцію розміру треку (ваш minmax) і доступний простір.
Різниця — у тому, що відбувається з невикористаними треками.

Auto-fill: залишає порожні колонки

auto-fill створить стільки колонок, скільки вміститься, навіть якщо у вас не вистачає елементів, щоб зайняти їх.
Ці порожні треки все одно існують і займають простір, тож ваші елементи не будуть розтягуватися так сильно.
Це корисно, коли ви хочете стабільну геометрію колонок (думаємо: макети типу календаря).

Auto-fit: згортає порожні колонки

auto-fit звужує порожні треки до нуля. Це означає, що елементи можуть розширюватись, щоб заповнити ряд.
Для карток це зазвичай бажано: немає дивних порожніх колонок, коли у вас лише 2 картки.

Корисне правило: для карткових сіток використовуйте auto-fit за замовчуванням.
Використовуйте auto-fill, коли вам важливо, щоб «фантомні колонки» залишалися зарезервованими.

Жарт №1: Auto-fill — як резервувати місця для друзів, які «точно вже в дорозі». Auto-fit — визнати, що вони не прийдуть, і з’їсти їхні снеки.

Проблема «однієї самотньої картки»

Ви побачите це: сітка з 5 елементів, контейнер достатньо широкий для 3 колонок. Перший ряд має 3 картки, другий — 2.
З auto-fit ці 2 картки часто гарно розтягуються. З auto-fill вони можуть залишатися вузькими, бо «третя колонка» все ще існує як порожній трек.
Це та різниця, яку можна показати дизайнеру, не починаючи війну.

Вибір min/max: числа, що поводяться передбачувано

Більшість збоїв відбувається через вибір мінімальної ширини, ніби це дизайн-токен, який можна задати раз і забути.
Це не так. Це контракт між контентом, контейнером, типографікою і тим, що хтось впихає в картку наступного кварталу.

Оберіть мінімальну ширину на основі реального контенту, а не відчуттів

Ваш мінімум має вміщувати:

  • Типову довжину заголовка без переносу на 4 рядки
  • Рядки ключ-значення (мітки зазвичай довші, ніж ви очікуєте)
  • Кнопки (особливо з локалізацією)
  • Бейджі/чіпи, які не можуть зменшуватись
  • Довгі числа, ідентифікатори та часові мітки

Практична рекомендація:

  • 14–16rem для «простих карток» (іконка, заголовок, короткий опис)
  • 18–22rem для «інформаційних карток» (кілька рядків, метадані, дії)
  • 24rem+ для «карток, що прикидаються таблицями» (тут, мабуть, краще використовувати таблицю)

Максимум: чому зазвичай вистачає 1fr

1fr в minmax змушує колонки ділити залишковий простір. Для більшості сіток це ідеально.
Уникайте величезних максимумів або max-content, якщо вам не подобається дебаг переповнення о 2:00 ночі.

Більш оборонний патерн:

cr0x@server:~$ cat defensive-grid.css
.grid {
  display: grid;
  gap: 1rem;

  /* clamp-like behavior: don't let cards get cartoonishly wide */
  grid-template-columns: repeat(auto-fit, minmax(18rem, 22rem));
  justify-content: center;
}
...output...

Тут максимум фіксований (22rem), що запобігає однорядковим макетам із двома гігантськими картками на надшироких екранах.
Ви віддаєте «заповнення всього простору» за «картки залишаються формою картки». Часто це кращий UX.

Не ігноруйте gap у своїх розрахунках

Триколонний макет потребує 3 * minWidth + 2 * gap простору (плюс паддінги й бордери).
Люди ставлять minmax(320px, 1fr) і gap: 32px, а потім дивуються, чому сітка падає до двох колонок раніше, ніж очікувалося.
Це не «випадково». Це арифметика.

Використовуйте min(), коли контейнер може бути крихітним

На вузьких контейнерах (мобільні, модальні вікна, бічні панелі) жорсткий мінімум 18rem може спричинити переповнення, якщо контейнер менший.
Ви можете обмежити мінімум:

cr0x@server:~$ cat tiny-container-grid.css
.grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
}
...output...

Це гарантує, що мінімум ніколи не перевищує ширину контейнера. Це не «магічна адаптивність». Це визнання, що контейнери можуть бути маленькими, і поводження як дорослий.

Практичні карткові сітки: патерни для продакшену

Перейдемо від теорії до огидних деталей, які з’являються, коли ваша сітка живе в реальному додатку:
бічні панелі, вкладки, фільтри, модальні вікна, довгі переклади і один керівник, що користується масштабом 200%.

Патерн 1: Стандартні рідкі картки для дашбордів

cr0x@server:~$ cat dashboard-grid.css
.grid {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  align-items: start;
}

.card {
  min-width: 0;
  border: 1px solid #d8dde6;
  border-radius: 12px;
  padding: 16px;
  background: #fff;
}
...output...

Чому це працює:

  • Мінімальна ширина (280px) читабельна для звичних карток з метриками.
  • align-items: start не дає карткам вертикально розтягуватись до найвищого сусіда.
  • min-width: 0 зменшує переповнення від довгого контенту.

Патерн 2: Версія для дизайн-системи (узгоджена ширина карток)

cr0x@server:~$ cat design-system-grid.css
.grid {
  display: grid;
  gap: 20px;
  grid-template-columns: repeat(auto-fit, minmax(20rem, 24rem));
  justify-content: start;
}
...output...

Цей патерн використовують, коли дизайн хоче, щоб картки виглядали однаково на різних вікнах, а не розтягувалися у млинці.
Також приємніше для карток із графіками: графіки, як правило, виглядають краще при сталих розмірах.

Патерн 3: Змішаний вміст у картках (захист від довгих рядків)

cr0x@server:~$ cat mixed-content.css
.grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
}

.card .title,
.card .value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
...output...

Цей патерн робить вибір: обрізати замість переповнення. Для дашбордів це зазвичай правильно — за умови, що повне значення також доступне через підказку або детальний вигляд.
Не обрізайте мовчки без кнопки для повного перегляду; інакше отримаєте тикети «чому в моєму хості втрачається половина імені?».

Патерн 4: Коли хочете менше сюрпризів: обмеження кількості колонок

Іноді ви не хочете, щоб сітка додавала колонки безкінечно. Можна обмежити це, звузивши ширину контейнера:

cr0x@server:~$ cat capped-grid.css
.wrapper {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 16px;
}

.grid {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
...output...

Це недооцінено. «Необмежені колонки» звучать гнучко, але можуть зруйнувати патерни сканування на широких дисплеях.
Обмеження — це продуктове рішення, а не технічне обмеження.

Коли все ж варто використовувати медіа-запити

Так, я сказав «без медіа-запитів». Я також керую продакшеном, тому скажу відверто:

  • Використовуйте медіа-запити, коли макет принципово змінюється, а не коли просто змінюється кількість колонок.
    Наприклад: перехід від бокової панелі фільтрів до drawer-фільтра.
  • Використовуйте медіа-запити, коли потрібно змінити типографіку (розміри шрифтів, висоти рядків), що не залежить тільки від контейнера.
  • Використовуйте медіа-запити, якщо потрібно цілитися під конкретні класи пристроїв для торк-міток або поведінки навігації.

Для карткових сіток: якщо ви пишете 4–6 брейкпоїнтів, ви, ймовірно, компенсуєте погану мінімальну ширину або неконтрольований внутрішній вміст карток.
Виправте це спочатку.

Одне речення, щоб зберегти вас від самообману: Надія — не стратегія. (часто цитують в інженерних кругах; ідея перефразована)

Швидкий план діагностики

Коли карткова сітка поводиться некоректно в продакшені — переповнення, несподівана кількість колонок, дивне розтягнення — вам не потрібен тиждень «археології CSS».
Потрібен порядок триажу, що швидко знаходить вузьке місце.

1) Підтвердіть розміри і обмеження контейнера

Перше питання: чи дійсно контейнер сітки має ту ширину, яку ви думаєте?
Бічні панелі, паддінги, вкладені max-width обгортки та полоски прокрутки змінюють доступний inline-розмір.

2) Перевірте визначення треків і математику gap

Друге питання: чи вміщується minWidth * columns + gaps?
Більшість багів «чому впала до 2 колонок?» — це просто gap + padding + min width, що перевищують контейнер.

3) Проінспектуйте внутрішнє мінімальне розмірне поводження дочірніх елементів

Третє питання: чи якийсь дочірній елемент не примушує більший min-content ніж ваш мінімум треку?
Типові винуватці: довгі нерозривні рядки, зображення без обмежень, flex-дочірні без min-width: 0.

4) Перевірте overflow і налаштування вирівнювання

Якщо картки розтягуються вертикально або обрізають контент, перевірте align-items на сітці та правила overflow у картці.
Багато бібліотек компонентів ставлять дефолтні значення, які ок у ізоляції, але жахливі в сітці.

5) Лише потім розгляньте зміну «стратегії макета»

Перехід на container queries, додавання додаткових обгорток чи написання брейкпоїнтів — це крок п’ятий, а не перший.
Якщо зробите це раніше, ви закриєте симптом, але не причину, і проблема з’явиться знову, щойно контент зміниться.

Поширені помилки: симптом → причина → виправлення

1) Симптом: горизонтальний скрол на малих екранах

  • Причина: жорсткий мінімум ширший за контейнер; або дочірній елемент з великим внутрішнім мінімумом.
  • Виправлення: використайте minmax(min(18rem, 100%), 1fr); додайте min-width: 0 до картки; обмежте медіа через max-width: 100%.

2) Симптом: картки надто широкі на великих екранах і виглядають смішно

  • Причина: minmax(x, 1fr) з малою кількістю елементів означає, що кожен елемент розтягується, щоб заповнити величезний простір.
  • Виправлення: обмежте максимальну ширину: minmax(18rem, 24rem) і використайте justify-content: center або звузьте wrapper.

3) Симптом: «чому там порожні колонки?»

  • Причина: використання auto-fill там, де мав бути auto-fit.
  • Виправлення: переключіться на auto-fit або прийміть порожні треки, якщо дизайн хоче стабільну геометрію.

4) Симптом: сітка не обгортає, коли очікуєте

  • Причина: мінімум занадто малий, тому вміщується більше колонок, ніж ви хочете; або контейнер ширший, ніж ви думаєте через flex-поведінку.
  • Виправлення: збільшіть мінімальну ширину; обмежте wrapper; перевірте батьківські обмеження макета (flex і width правила).

5) Симптом: одна картка змушує весь ряд розширитися і виникає overflow

  • Причина: внутрішнє мінімальне розмірне поводження від довгих нерозривних рядків, широких таблиць або flex-дочірніх, що не стискаються.
  • Виправлення: min-width: 0 на картці і релевантних flex-дочірніх; додайте overflow-wrap: anywhere для ворожих рядків; обмежте медіа.

6) Симптом: невідповідні висоти карток псують читабельність

  • Причина: картки з різним контентом; сітка вирівнює за стартом, але контент сильно різниться.
  • Виправлення: вирішіть, чи треба нормалізувати висоту; якщо так, використайте консистентні блоки контенту, обмежте кількість рядків або внутрішній макет для вирівнювання дій унизу.

7) Симптом: кількість колонок змінюється, коли з’являється скролбар

  • Причина: скролбар поглинає inline-розмір; ваш поріг мін+gap стоїть на лезі ножа.
  • Виправлення: трохи зменшіть мінімум; зменшіть gap; додайте паддінг в wrapper, щоб уникнути точних порогів; розгляньте scrollbar-gutter: stable, де це доречно.

8) Симптом: картки накладаються або колапсують дивним чином

  • Причина: поєднання абсолютного позиціонування, фіксованих висот або негативних відступів з елементами grid.
  • Виправлення: припиніть так робити. Якщо потрібен шаруватий UI, робіть шари всередині картки, а не ламаючи геометрію елемента сітки.

Жарт №2: Якщо вашій сітці потрібно шість брейкпоїнтів, це не «адаптивність». Це переговори з терористами.

Три корпоративні міні-історії (бо це завжди «просто CSS»)

Міні-історія 1: Інцидент через хибне припущення

Команда випустила сторінку «каталог сервісів»: картки для кожного внутрішнього сервісу з власником, рівнем, посиланнями та бейджем статусу.
Вона ідеально виглядала у staging і на кожному скриншоті в PR.
Сітка використовувала repeat(auto-fit, minmax(320px, 1fr)), відступи 24px і контейнер з паддінгом.

В понеділок зранку прийшли тикети в підтримку: «Сторінка непридатна на менших ноутбуках». Деякі користувачі мали горизонтальний скрол; інші бачили по одній картці в ряд, коли мали б вміщуватись дві.
Баг не був випадковим. Його викликали дві реальні продакшен-умови: масштаб браузера і постійний віджет чату праворуч.
Разом вони зменшили ширину контейнера трохи нижче порогу для «двох колонок».

Хибне припущення було тонким: вони вважали, що ширина контейнера — це ширина вьюпорта мінус сайдбар.
Насправді в додатку був max-width wrapper, плюс паддінги, плюс віджет чату, плюс скролбар.
Математика min+gap стояла прямо на межі, тож будь-яке невелике зменшення знижувало кількість колонок і спричиняло дивні переповнення.

Виправлення було нудним і негайним: зменшили мінімум з 320px до 296px, знизили gap до 16px і поміняли min на min(18rem, 100%), щоб уникнути переповнення на крихітних ширинах.
Потім додали обмеження max-width для wrapper, щоб надширокі екрани не перетворювали картки на постери.
Жодних нових брейкпоїнтів. Лише обмеження, що відповідали реальності.

Урок: якщо ваша сітка «на порозі», ставтеся до неї як до продакшен-залежності з нестабільним мережевим зв’язком.
Дайте запас у математиці.

Міні-історія 2: Оптимізація, що відкотилася назад

Інша команда хотіла покращити сприйняту швидкодію на важкій аналітичній сторінці.
Вони помітили зсув макета під час завантаження даних: рендерилися skeleton-картки, потім приходив реальний контент, і висоти змінювалися.
Хтось запропонував «оптимізацію»: зафіксувати висоти і ширини карток, щоб сітка ніколи не мінялася. Звучало надійно.

Вони реалізували фіксовані висоти карток і насильно встановили сувору трьохколонну сітку через медіа-запити.
Зсув макета зменшився, так. Але тихо з’явився новий режим відмови: на вузьких контейнерах (фільтри відкриті, користувач збільшив масштаб) фіксовані три колонки спричиняли постійне переповнення.
Skeleton-и «вміщувалися», бо були короткі; реальний контент переповнювався через реальні довгі рядки і графіки з мінімальним внутрішнім розміром.

Найгірше: баг з’являвся лише для деяких локалей. Перекладені підписи кнопок були довшими, і фіксовані висоти карток обрізали контент.
Підтримка доповідала це як «відсутні кнопки», що еквівалентно «втрата даних» у UI.
Тепер команді довелося вибирати між видимим зсувом макета і невидимим обрізанням. Обидва варіанти кепські.

Вони відкотили фіксовані розміри і замість цього використали інтрінсичну сітку з auto-fit/minmax, плюс обмеження рядків для полів, що створювали екстремальні висоти.
Skeleton-и відповідали типовим розмірам контенту, а не ідеалізованим.
Сторінка стала трохи «живою» під час завантаження, але перестала ламатися.

Урок: оптимізація шляхом заморозки геометрії може обернутися проти вас, якщо контент — справжнє джерело варіативності.
Краще обмежити варіативність, ніж заперечувати її існування.

Міні-історія 3: Нудна, але правильна практика, що врятувала день

Платформна команда підтримувала спільну бібліотеку компонентів, яку використовували кілька продуктових команд.
Карткові сітки були всюди: списки інцидентів, звіти по витратах, плитки сервісів, фічі-флаги тощо.
Вони не дозволяли кастомні брейкпоїнти для кожної сторінки. Люди були незадоволені. Гучно.

Платформна команда наполягла на одному примітиві сітки: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)), стандартні відступи і документоване правило:
кожна картка має ставити min-width: 0, і будь-який внутрішній flex-рядок також повинен ставити min-width: 0 на елементах, що стискаються.
Вони також вимагали поведінку обрізання для довгих ідентифікаторів і надали компонент для цього.

Потім сталася «аварія»: велика продуктова команда додала нову картку з довгим нерозривним токеном з upstream-системи.
В старих сторінках такий контент викликав класичне переповнення і горизонтальний скрол.
Цього разу токен рядок загортався або обрізався, як задумано; сітка залишилася цілою; лише одне поле виглядало некрасиво — і це правильне місце, де має бути некрасиво.

Ніхто не писав хотфікс опівночі. Ніхто не додавав брейкпоїнт. Ніхто не сперечався з дизайном тиждень.
Нудні обмеження витримали навіть ворожий контент. Саме це вам і треба.

Урок: якщо стандартизувати примітив сітки і забезпечити правила стискання/переповнення, кількість «CSS-інцидентів» значно зменшиться.
Це не гламурно. Це операційно розумно.

Практичні завдання: команди, виводи, рішення

Ви просили про продакшен-орієнтований підхід. Ось що це означає: ви не просто підправляєте CSS; ви вимірюєте умови, що викликають збої.
Ці завдання навмисно «оперсні», бо регресії в інтерфейсі — це відмови, загорнуті в бізнес-упаковку.

Завдання 1: Підтвердіть, що ваш CSS справді містить правило сітки в зібраному бандлі

cr0x@server:~$ rg -n "grid-template-columns: repeat\(auto-fit, minmax" dist/assets/*.css | head
dist/assets/app.4c9b1f0.css:1123:.grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}

Що значить вивід: ваш зібраний CSS містить точне правило, мініфіковане.
Рішення: якщо його немає, ви дебагаєте не ту середу; виправте CI, tree-shaking або порядок імпортів CSS.

Завдання 2: Виявити, чи пізніше правило не перекриває визначення сітки

cr0x@server:~$ rg -n "grid-template-columns" dist/assets/*.css | head -n 12
dist/assets/app.4c9b1f0.css:1123:.grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
dist/assets/app.4c9b1f0.css:20988:.grid{grid-template-columns:1fr}

Що значить вивід: у вас принаймні два визначення; останнє може перемогти в залежності від специфічності та порядку.
Рішення: виправте область селектора (наприклад, .card-grid замість .grid), або налаштуйте шари каскаду, щоб правило компонента вигравало постійно.

Завдання 3: Швидко подивитися обчислені стилі через Playwright у headless режимі

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage();await p.goto("http://localhost:3000");const v=await p.$eval(".grid", el=>getComputedStyle(el).gridTemplateColumns);console.log(v);await b.close();})();'
280px 280px 280px

Що значить вивід: при цьому viewport і ширині контейнера ви отримуєте три треки по 280px.
Рішення: якщо воно друкує none або один 1fr, ваш селектор не збігся або макет перекритий.

Завдання 4: Підтвердіть ширину контейнера сітки під час виконання

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:1024,height:768}});await p.goto("http://localhost:3000");const w=await p.$eval(".grid", el=>el.getBoundingClientRect().width);console.log(w);await b.close();})();'
944

Що значить вивід: ширина контейнера — 944px, а не 1024px (паддінги/сайдбари/тощо).
Рішення: зробіть математику: чи вміщаються 3 колонки? 3*280 + 2*16 = 872. Так. Якщо ви очікували 4, потрібно зменшити мінімум, зменшити gap або зробити контейнер ширшим.

Завдання 5: Автоматично відтворити поведінку без брейкпоїнтів по вьюпортам

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage();for (const width of [375,480,768,1024,1440]){await p.setViewportSize({width,height:800});await p.goto("http://localhost:3000");const cols=await p.$eval(".grid", el=>getComputedStyle(el).gridTemplateColumns.split(" ").length);console.log(width, cols);}await b.close();})();'
375 1
480 1
768 2
1024 3
1440 4

Що значить вивід: кількість колонок змінюється природно без явних брейкпоїнтів.
Рішення: якщо кількість колонок стрибає непередбачувано, досліджуйте змінність ширини контейнера, а не лише ширину вьюпорта.

Завдання 6: Знайти джерела переповнення, скануючи горизонтальний скрол у прогоні скріншотів

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:390,height:844}});await p.goto("http://localhost:3000");const hasOverflow=await p.evaluate(()=>document.documentElement.scrollWidth>document.documentElement.clientWidth);console.log("overflow",hasOverflow,"scrollWidth",document.documentElement.scrollWidth,"clientWidth",document.documentElement.clientWidth);await b.close();})();'
overflow true scrollWidth 428 clientWidth 390

Що значить вивід: щось заставляє горизонтальне переповнення.
Рішення: перевірте дочірні елементи сітки на довгі рядки, зображення або елементи з фіксованою шириною; застосуйте min-width: 0 і правила обгортання.

Завдання 7: Знайти найгірший елемент-винуватець переповнення

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:390,height:844}});await p.goto("http://localhost:3000");const id=await p.evaluate(()=>{const els=[...document.querySelectorAll(".grid *")];let worst={w:0,sel:""};for(const el of els){const r=el.getBoundingClientRect();if(r.width>worst.w){worst={w:r.width,sel:el.tagName.toLowerCase()+"."+[...el.classList].join(".")};}}return worst;});console.log(id);await b.close();})();'
{ w: 612.345703125, sel: 'div.value' }

Що значить вивід: елемент всередині сітки (тут div.value) ширший за вьюпорт.
Рішення: застосуйте обрізання/обгортання до цього компонента; не «виправляйте» це зменшенням всієї сітки.

Завдання 8: Підтвердити поведінку розриву рядків для ворожих нерозривних рядків

cr0x@server:~$ cat wrap-test.css
.value { overflow-wrap: anywhere; word-break: break-word; }
...output...

Що значить вивід: ви дозволяєте розриви навіть у довгих токенах.
Рішення: якщо токени мають залишатися копійованими, віддайте перевагу обрізанню з UI «копіювати в буфер»; розгортка 64-символьного токена на 5 рядків технічно коректна, але емоційно шкідлива.

Завдання 9: Перевірити поріг колонок швидким скриптом

cr0x@server:~$ node -e 'const min=280, gap=16; for (const cols of [1,2,3,4,5]){console.log(cols, cols*min + (cols-1)*gap);} '
1 280
2 576
3 872
4 1168
5 1464

Що значить вивід: мінімальні ширини контейнера для кожної кількості колонок.
Рішення: порівняйте з фактичними ширинами контейнера (Завдання 4). Якщо ви мотаєтесь близько 872px, очікуйте флапання між 2 і 3 колонками при зміні UI chrome.

Завдання 10: Перевірити, чи картки розтягуються вертикально через вирівнювання

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:1200,height:800}});await p.goto("http://localhost:3000");const ai=await p.$eval(".grid", el=>getComputedStyle(el).alignItems);console.log(ai);await b.close();})();'
stretch

Що значить вивід: елементи можуть розтягуватись на висоту рядка.
Рішення: встановіть align-items: start на сітці, якщо ви хочете природні висоти карток.

Завдання 11: Виявити, чи зображення — винуватець інтрінсичної ширини

cr0x@server:~$ node -e 'const { chromium } = require("playwright"); (async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:390,height:844}});await p.goto("http://localhost:3000");const imgs=await p.$$eval(".grid img", els=>els.map(i=>({w:i.getBoundingClientRect().width, css:getComputedStyle(i).maxWidth, src:i.getAttribute("src")})));console.log(imgs.slice(0,3));await b.close();})();'
[
  { w: 420, css: 'none', src: '/assets/chart.png' }
]

Що значить вивід: зображення ширше, ніж має бути, і неконстрейндне (max-width: none).
Рішення: застосуйте max-width: 100% і забезпечте адаптивні розміри; інакше один чарт-скриншот може знищити всю сітку.

Завдання 12: Переконатись, що сітка використовує gap, а не margin, що колапсує непередбачувано

cr0x@server:~$ rg -n "\.card\{[^}]*margin" src/components | head
src/components/Card.css:.card{margin:12px}

Що значить вивід: картки додають margin, що заважає власному spacing сітки.
Рішення: прибрати зовнішні margin у елементів сітки; використовуйте gap на контейнері сітки для консистентних і передбачуваних відступів.

Завдання 13: Переконатись, що довгі flex-ряди всередині карток можуть стискатись

cr0x@server:~$ rg -n "display:\s*flex" -S src/components/Card* | head
src/components/CardMeta.css:.metaRow{display:flex;gap:8px}

Що значить вивід: у вас, ймовірно, є flex-дочірні, що можуть створювати min-content overflow.
Рішення: додайте min-width: 0 на flex-елемент, який має стискатися (зазвичай контейнер тексту), і обрізання там, де це доречно.

Завдання 14: Тест на «порожні треки», перемикаючи auto-fit/auto-fill

cr0x@server:~$ perl -0777 -pe 's/repeat\(auto-fit,/repeat(auto-fill,/g' -i src/styles/grid.css
...output...

Що значить вивід: ви тимчасово змінили поведінку.
Рішення: якщо «самотня остання картка» раптом перестає розтягуватись, ви довели, що колапс треків — частина UX-вибору. Поверніть назад і вирішіть це свідомо.

Ці завдання виглядають надмірними, поки у вас не зламається макет під час демо для продажів. Потім вони виглядають як страховка.

Чеклісти / покроковий план

Покроково: впровадження production-safe auto-fit карткової сітки

  1. Визначте мінімальну читабельну ширину картки.
    Використовуйте реальний контент. Якщо його нема — використайте найдовші реалістичні рядки (ID, імена, локалізовані кнопки).
  2. Почніть з основного правила:
    grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  3. Додайте оборонне обмеження мінімуму, якщо сітка може жити в вузьких контейнерах:
    minmax(min(18rem, 100%), 1fr).
  4. Використовуйте gap на сітці, приберіть margin у карток у цьому контексті.
  5. Встановіть min-width: 0 на картці і на елементах flex, що мають стискатись всередині картки.
  6. Вирішіть максимальну ширину картки.
    Якщо картки дивно виглядають при розтягненні, використайте minmax(18rem, 24rem) і узгодьте з justify-content.
  7. Обробіть ворожий контент явно.
    Обрізайте довгі рядки, обмежуйте кількість рядків, контролюйте зображення і графіки.
  8. Тестуйте із зумом і боковими панелями.
    Розглядайте варіативність ширини контейнера як вхідну величину першого порядку.
  9. Автоматизуйте прогін по вьюпортам (як у Завданні 5) і фейліть збірку при появі overflow (Завдання 6).
  10. Документуйте контракт для авторів карток: жодних фіксованих ширин, ніякого невизначеного медіа, і як обробляти довгий текст.

Чекліст: перед тим як додавати медіа-запит

  • Чи перевірили ви фактичну ширину контейнера (не ширину вьюпорта)?
  • Чи зробили ви арифметику min+gap для очікуваних кількостей колонок?
  • Чи додали ви min-width: 0 там, де потрібно?
  • Чи обмежили ви зображення і графіки під ширину картки?
  • Чи вирішили ви, чи картки мають розтягуватись (1fr) або мати сталу ширину (капа)?
  • Чи тестували ви з довгими перекладами і довгими ідентифікаторами?
  • Чи відтворили ви баг автоматично через перевірки вьюпорт/overflow?

Якщо ви не можете відмітити ці пункти, медіа-запит — це пластир на течу в стіні.
Він виглядатиме добре, поки не розпочнеться наступна реконструкція.

FAQ

1) Чи справді repeat(auto-fit, minmax()) адаптивний без медіа-запитів?

Так, для зміни кількості колонок, що визначається доступним простором. Воно реагує на розмір контейнера, а це те, що реально важливо в складних додатках.
Медіа-запити реагують на розмір вьюпорта, що лише побічно корелює з простором, який отримує ваша сітка.

2) Чи використовувати auto-fit або auto-fill для карток?

За замовчуванням — auto-fit. Воно звужує порожні треки так, що решта карток природно розширюється.
Використовуйте auto-fill, коли ви навмисно хочете зарезервовані порожні колонки (рідко потрібно для карток, частіше для календарів).

3) Чому моя сітка переповнюється, хоча я встановив мінімум?

Бо треки сітки поважають внутрішнє розмірне поводження. Дочірній елемент може мати min-content ширину більшу за мінімум треку.
Виправте це з min-width: 0 на елементах сітки і flex-дочірніх, додайте обгортання/обрізання для довгих рядків і обмеження для медіа.

4) Яку мінімальну ширину обрати?

Оберіть найменшу ширину, при якій контент картки все ще читабельний і операбельний. Для типових дашбордів це 280–360px.
Потім тестуйте з найгіршими рядками і в вузькому контейнері, а не лише з мобільним вьюпортом.

5) Дизайнерам не подобається, що картки розтягуються. Який патерн?

Використовуйте фіксований максимум у minmax, наприклад minmax(20rem, 24rem), і встановіть justify-content: center або start.
Альтернативно обмежте max-width wrapper.

6) Чи роблять container queries це зайвим?

Ні. Container queries допомагають, коли внутрішній макет залежить від розміру компонента (наприклад, заміна внутрішньої структури картки).
Для питання «скільки колонок вміщується» auto-fit/minmax залишається найпростішим і найстійкішим примітивом.

7) Як запобігти зсувам макета під час завантаження даних?

Матчуйте skeleton-и до типової величини контенту, а не до ідеалізованої. Обмежуйте варіативний контент (clamp рядків, фіксований аспект графіків).
Не заморожуйте геометрію так сильно, щоб реальний контент переповнювався або обрізався.

8) Чому кількість колонок змінюється, коли з’являється скролбар?

Скролбар поглинає inline-розмір, підштовхуючи контейнер нижче порогу, де вміщується на одну колонку менше.
Дайте собі запас: трохи зменшіть мінімум або gap, або стабілізуйте місце під скролбар за допомогою відповідного CSS там, де це доречно.

9) Чи можна зробити masonry-локайт з цим?

Не по-справжньому. Рядки grid вирівнюються; masonry потребує незалежного вертикального розміщення.
Ви можете підробити masonry у деяких випадках, але для продакшен-UI, де важливий порядок і читання, уникайте masonry для карток з діями.

10) Який найважливіший «одно-рядковий» фікс для проблем із сіткою?

min-width: 0 на елементах сітки (і на shrinkable flex-дочірніх). Це запобігає тому, щоб контент примушував треки ширшими, ніж ви планували.
Це CSS-еквівалент питання «ви перевірили DNS?» — набридливо, але часто вирішує проблему.

Висновок: наступні кроки, які можна відправити в реліз

Ера брейкпоїнтів навчила людей трактувати макет як набір жорстких режимів. Це працює, поки ваш додаток має справжню оболонку, справжній контент, справжні рівні зуму і справжніх користувачів.
Карткові сітки — ідеальне місце перейти на макет, заснований на обмеженнях.

Зробіть наступне, по порядку:

  1. Впровадьте repeat(auto-fit, minmax(min(X, 100%), 1fr)) з розумним X, обраним за читабельним контентом.
  2. Встановіть min-width: 0 на картках і shrinkable inner flex-елементах.
  3. Вирішіть, чи хочете розтягнуті картки (1fr) або консистентні ширини карток (капа).
  4. Додайте автоматизовані перевірки на переповнення і кількість колонок по вьюпортам — вважайте регресії макета операційними інцидентами.

Якщо ви зробите ці чотири кроки, то напишете менше медіа-запитів, випустите менше «працює на моїй машині» макетів і витратите менше часу на дебаг привидів, спричинених непоміряними ширинами контейнерів.
А це — справжня розкіш.

← Попередня
Електронна пошта: плутанина S/MIME та TLS — що справді покращує безпеку
Наступна →
WordPress заблоковано WAF: налаштуйте правила без створення вразливостей

Залишити коментар