Пошук уроків, статей та іншого контенту
Зберемо галерею з автоматичними колонками, minmax(), auto-fit і різними розмірами зображень.
У цьому уроці зберемо адаптивну галерею на CSS Grid, яка:
автоматично підбирає кількість колонок;
використовує minmax() для мінімальної та максимальної ширини карток;
використовує auto-fit для заповнення доступного простору;
підтримує картки різної ширини та висоти;
коректно працює на вузьких екранах;
не потребує медіазапитів для базового перемикання кількості колонок.
Базова конструкція галереї виглядає так:
.gallery {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(100%, 15rem), 1fr)
);
gap: 1rem;
}Розглянемо її частинами:
display: grid перетворює контейнер на CSS Grid;
repeat() повторює опис колонки;
auto-fit автоматично створює стільки колонок, скільки поміщається;
minmax() задає мінімальний і максимальний розмір колонки;
15rem — бажана мінімальна ширина картки;
1fr — картка може розтягуватися, щоб поділити вільний простір;
min(100%, 15rem) не дозволяє картці бути ширшою за контейнер на дуже вузькому екрані.
Наприклад, у широкому контейнері можуть розміститися чотири картки:
[ картка ][ картка ][ картка ][ картка ]У вужчому контейнері Grid автоматично скоротить кількість колонок:
[ картка ][ картка ]
[ картка ][ картка ]На мобільному екрані всі картки можуть перейти в один стовпчик:
[ картка ]
[ картка ]
[ картка ]auto-fit і minmax()Значення auto-fit намагається розтягнути наявні колонки, щоб вони заповнили весь простір контейнера.
Порівняно з ним, auto-fill зберігає потенційні порожні колонки. Для галереї зазвичай потрібен саме auto-fit, тому що картки мають займати весь доступний простір без порожніх місць праворуч.
.gallery {
grid-template-columns: repeat(
auto-fit,
minmax(15rem, 1fr)
);
}У цьому прикладі картка не буде меншою за 15rem, але може розтягуватися завдяки 1fr.
На практиці краще використовувати:
minmax(min(100%, 15rem), 1fr)Якщо ширина контейнера менша за 15rem, звичайний minmax(15rem, 1fr) може спричинити горизонтальне переповнення. Функція min() обмежує мінімальний розмір шириною самого контейнера.
CSS Grid дозволяє картці займати кілька колонок або рядків.
.gallery__item--wide {
grid-column: span 2;
}
.gallery__item--tall {
grid-row: span 2;
}Клас gallery__item--wide розтягує картку на дві колонки, а gallery__item--tall — на два рядки.
Для цього потрібно встановити висоту автоматичних рядків:
.gallery {
grid-auto-rows: 12rem;
}Тепер:
звичайна картка займає один рядок;
висока картка займає два рядки;
широка картка займає дві колонки;
картка з обома класами займає область 2 × 2.
Приклад є самодостатнім: SVG-ілюстрації вбудовані безпосередньо в HTML, тому документ можна відкрити у браузері без додаткових файлів.
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Адаптивна галерея</title>
<style>
:root {
color-scheme: dark;
--page-background: #111827;
--surface: #1f2937;
--surface-hover: #374151;
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
--accent: #a78bfa;
--gap: 1rem;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
background: var(--page-background);
color: var(--text-primary);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.page {
width: min(100% - 2rem, 90rem);
margin-inline: auto;
padding-block: 3rem;
}
.page__header {
max-width: 42rem;
margin-block-end: 2rem;
}
.page__title {
margin: 0 0 0.75rem;
font-size: clamp(2rem, 5vw, 4rem);
line-height: 1;
}
.page__description {
margin: 0;
color: var(--text-secondary);
line-height: 1.6;
}
.gallery {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(100%, 15rem), 1fr)
);
grid-auto-rows: 12rem;
gap: var(--gap);
grid-auto-flow: dense;
}
.gallery__item {
min-width: 0;
overflow: hidden;
border-radius: 1rem;
background: var(--surface);
transition:
transform 180ms ease,
background-color 180ms ease;
}
.gallery__item:hover {
background: var(--surface-hover);
transform: translateY(-0.25rem);
}
.gallery__item--wide {
grid-column: span 2;
}
.gallery__item--tall {
grid-row: span 2;
}
.gallery__item--featured {
grid-column: span 2;
grid-row: span 2;
}
.gallery__link {
display: grid;
height: 100%;
color: inherit;
text-decoration: none;
}
.gallery__art {
display: block;
width: 100%;
height: 100%;
min-height: 0;
}
.gallery__caption {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.9rem 1rem;
background: rgb(17 24 39 / 75%);
}
.gallery__name {
font-weight: 700;
}
.gallery__category {
color: var(--text-secondary);
font-size: 0.875rem;
}
@media (max-width: 42rem) {
.gallery__item--wide,
.gallery__item--featured {
grid-column: span 1;
}
}
@media (max-width: 31rem) {
.gallery {
grid-auto-rows: 11rem;
}
.gallery__item--tall,
.gallery__item--featured {
grid-row: span 1;
}
}
@media (prefers-reduced-motion: reduce) {
.gallery__item {
transition: none;
}
}
</style>
</head>
<body>
<main class="page">
<header class="page__header">
<h1 class="page__title">Міські пейзажі</h1>
<p class="page__description">
Галерея автоматично змінює кількість колонок, а окремі картки
можуть займати більше місця у сітці.
</p>
</header>
<section class="gallery" aria-label="Галерея міських пейзажів">
<article class="gallery__item gallery__item--featured">
<a class="gallery__link" href="#featured">
<svg class="gallery__art" viewBox="0 0 800 500" role="img"
aria-labelledby="featured-title">
<title id="featured-title">Фіолетове місто на заході сонця</title>
<defs>
<linearGradient id="featured-gradient" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#312e81"/>
<stop offset="0.5" stop-color="#7c3aed"/>
<stop offset="1" stop-color="#f97316"/>
</linearGradient>
</defs>
<rect width="800" height="500" fill="url(#featured-gradient)"/>
<circle cx="610" cy="145" r="72" fill="#fde68a"/>
<path d="M0 390 140 250 250 370 390 190 560 390 680 270 800 390V500H0Z"
fill="#111827" opacity=".85"/>
<path d="M0 430 180 330 320 420 480 300 640 420 800 340V500H0Z"
fill="#030712" opacity=".9"/>
</svg>
<span class="gallery__caption">
<span class="gallery__name">Захід сонця</span>
<span class="gallery__category">Місто</span>
</span>
</a>
</article>
<article class="gallery__item gallery__item--tall">
<a class="gallery__link" href="#forest">
<svg class="gallery__art" viewBox="0 0 400 600" role="img"
aria-labelledby="forest-title">
<title id="forest-title">Темний хвойний ліс</title>
<defs>
<linearGradient id="forest-gradient" x1="0" x2="0" y1="0" y2="1">
<stop offset="0" stop-color="#164e63"/>
<stop offset="1" stop-color="#14532d"/>
</linearGradient>
</defs>
<rect width="400" height="600" fill="url(#forest-gradient)"/>
<circle cx="290" cy="130" r="55" fill="#bae6fd" opacity=".8"/>
<path d="M30 600 145 170 260 600Z" fill="#052e16"/>
<path d="M160 600 280 110 400 600Z" fill="#064e3b"/>
<path d="M0 600 75 270 155 600Z" fill="#022c22"/>
</svg>
<span class="gallery__caption">
<span class="gallery__name">Тихий ліс</span>
<span class="gallery__category">Природа</span>
</span>
</a>
</article>
<article class="gallery__item">
<a class="gallery__link" href="#sea">
<svg class="gallery__art" viewBox="0 0 400 300" role="img"
aria-labelledby="sea-title">
<title id="sea-title">Хвилі на морі</title>
<rect width="400" height="300" fill="#0e7490"/>
<circle cx="300" cy="75" r="42" fill="#fef3c7"/>
<path d="M0 170C75 120 125 220 205 165S320 120 400 170V300H0Z"
fill="#67e8f9"/>
<path d="M0 220C90 170 150 260 235 210S330 180 400 225V300H0Z"
fill="#164e63"/>
</svg>
<span class="gallery__caption">
<span class="gallery__name">Хвилі</span>
<span class="gallery__category">Море</span>
</span>
</a>
</article>
<article class="gallery__item gallery__item--wide">
<a class="gallery__link" href="#desert">
<svg class="gallery__art" viewBox="0 0 800 300" role="img"
aria-labelledby="desert-title">
<title id="desert-title">Пустеля з дюнами</title>
<defs>
<linearGradient id="desert-gradient" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#fef3c7"/>
<stop offset="1" stop-color="#f59e0b"/>
</linearGradient>
</defs>
<rect width="800" height="300" fill="url(#desert-gradient)"/>
<circle cx="620" cy="72" r="40" fill="#fff7ed"/>
<path d="M0 235C170 120 260 280 430 190S650 170 800 230V300H0Z"
fill="#d97706"/>
<path d="M0 275C190 190 300 310 490 225S680 220 800 270V300H0Z"
fill="#92400e"/>
</svg>
<span class="gallery__caption">
<span class="gallery__name">Дюни</span>
<span class="gallery__category">Подорожі</span>
</span>
</a>
</article>
<article class="gallery__item">
<a class="gallery__link" href="#architecture">
<svg class="gallery__art" viewBox="0 0 400 300" role="img"
aria-labelledby="architecture-title">
<title id="architecture-title">Сучасна архітектура</title>
<rect width="400" height="300" fill="#334155"/>
<rect x="60" y="80" width="90" height="220" fill="#bae6fd"/>
<rect x="165" y="35" width="95" height="265" fill="#e0f2fe"/>
<rect x="275" y="110" width="75" height="190" fill="#7dd3fc"/>
<path d="M0 300 200 185 400 300Z" fill="#1e293b"/>
</svg>
<span class="gallery__caption">
<span class="gallery__name">Геометрія</span>
<span class="gallery__category">Архітектура</span>
</span>
</a>
</article>
<article class="gallery__item">
<a class="gallery__link" href="#mountains">
<svg class="gallery__art" viewBox="0 0 400 300" role="img"
aria-labelledby="mountains-title">
<title id="mountains-title">Гірський пейзаж</title>
<rect width="400" height="300" fill="#1d4ed8"/>
<circle cx="85" cy="72" r="35" fill="#fef08a"/>
<path d="M0 300 130 95 205 205 270 120 400 300Z" fill="#dbeafe"/>
<path d="M0 300 145 170 225 245 295 185 400 300Z" fill="#1e3a8a"/>
</svg>
<span class="gallery__caption">
<span class="gallery__name">Вершини</span>
<span class="gallery__category">Гори</span>
</span>
</a>
</article>
<article class="gallery__item gallery__item--wide">
<a class="gallery__link" href="#night">
<svg class="gallery__art" viewBox="0 0 800 300" role="img"
aria-labelledby="night-title">
<title id="night-title">Нічне небо</title>
<rect width="800" height="300" fill="#111827"/>
<circle cx="590" cy="95" r="58" fill="#fef3c7"/>
<circle cx="615" cy="75" r="58" fill="#111827"/>
<g fill="#fef08a">
<circle cx="130" cy="75" r="4"/>
<circle cx="235" cy="145" r="3"/>
<circle cx="370" cy="65" r="5"/>
<circle cx="710" cy="145" r="4"/>
<circle cx="470" cy="180" r="3"/>
</g>
<path d="M0 250 130 180 250 230 390 160 520 240 650 190 800 235V300H0Z"
fill="#030712"/>
</svg>
<span class="gallery__caption">
<span class="gallery__name">Нічне небо</span>
<span class="gallery__category">Атмосфера</span>
</span>
</a>
</article>
</section>
</main>
</body>
</html>.gallery {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(100%, 15rem), 1fr)
);
grid-auto-rows: 12rem;
gap: 1rem;
}grid-auto-rows: 12rem задає висоту неявних рядків. Рядки створюються автоматично, коли браузер розміщує елементи галереї.
Завдяки цьому можна керувати висотою карток через grid-row:
.gallery__item--tall {
grid-row: span 2;
}Якщо картка займає два рядки, її висота буде приблизно 24rem, не враховуючи проміжок між рядками.
.gallery__item--wide {
grid-column: span 2;
}Цей клас не задає конкретну колонку, наприклад grid-column: 1 / 3. Він лише говорить: «зайняти дві доступні колонки».
Такий підхід зручний у динамічних галереях, де кількість колонок залежить від ширини екрана.
.gallery__item--featured {
grid-column: span 2;
grid-row: span 2;
}Картка займає чотири звичайні комірки сітки. Це підходить для головного зображення або рекомендованого матеріалу.
У прикладі використано:
.gallery {
grid-auto-flow: dense;
}Коли одна картка займає кілька колонок або рядків, у сітці можуть виникати вільні місця. Режим dense дозволяє Grid заповнювати такі місця наступними елементами.
Без dense сітка може виглядати так:
[ велика картка ][ велика картка ]
[ порожнє місце ][ звичайна картка ]З dense браузер намагається заповнити вільну комірку:
[ велика картка ][ велика картка ]
[ звичайна картка ][ інша картка ]Важливо: dense може змінити візуальний порядок елементів, хоча порядок у DOM залишиться незмінним. Для галереї це зазвичай прийнятно, але для послідовного контенту потрібно перевірити, чи не погіршується сприйняття сторінки.
Картка, яка займає дві колонки, не завжди може поміститися на вузькому екрані. Тому в прикладі її розмір скасовується:
@media (max-width: 42rem) {
.gallery__item--wide,
.gallery__item--featured {
grid-column: span 1;
}
}Те саме потрібно зробити для висоти:
@media (max-width: 31rem) {
.gallery__item--tall,
.gallery__item--featured {
grid-row: span 1;
}
}Автоматична кількість колонок не означає, що всі span-значення автоматично стануть безпечними для будь-якої ширини. Якщо картка має займати дві колонки, а доступна лише одна, потрібно окремо передбачити цей сценарій.
У прикладі використано вбудовані SVG, але для звичайних фотографій структура буде такою:
<article class="gallery__item gallery__item--wide">
<a class="gallery__link" href="#photo">
<img
class="gallery__image"
src="images/photo.jpg"
alt="Опис фотографії"
>
<span class="gallery__caption">
<span class="gallery__name">Назва фотографії</span>
<span class="gallery__category">Категорія</span>
</span>
</a>
</article>Для зображення потрібно додати:
.gallery__image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}object-fit: cover заповнює всю область картки, зберігаючи пропорції зображення. Частина зображення може бути обрізана.
Якщо важливо показати фотографію повністю, можна використати:
.gallery__image {
object-fit: contain;
background: #111827;
}У цьому випадку зображення не обрізається, але навколо нього можуть з'явитися порожні області.
Для змістовних зображень потрібно використовувати описовий alt:
<img
src="images/mountains.jpg"
alt="Засніжені гори на тлі блакитного неба"
>Якщо зображення вже має підпис поруч і цей підпис повністю описує його, alt все одно повинен бути присутнім. Він може бути коротшим, але не повинен бути пропущений.
Для декоративних зображень використовуйте порожній атрибут:
<img src="images/texture.jpg" alt="">У прикладі SVG має role="img" і доступну назву через aria-labelledby:
<svg role="img" aria-labelledby="image-title">
<title id="image-title">Опис ілюстрації</title>
</svg>Невдалий варіант:
.gallery {
grid-template-columns: repeat(4, 300px);
}Така сітка не адаптується до ширини контейнера і може спричинити горизонтальне прокручування.
Краще:
.gallery {
grid-template-columns: repeat(
auto-fit,
minmax(min(100%, 15rem), 1fr)
);
}.gallery {
grid-template-columns: repeat(
auto-fit,
minmax(30rem, 1fr)
);
}На екрані шириною 320px картка мінімум 30rem не поміститься.
Використовуйте реалістичний мінімальний розмір і обмежуйте його через min():
minmax(min(100%, 15rem), 1fr)min-width: 0Довгий текст або внутрішній елемент може змусити Grid-картку бути ширшою за колонку.
.gallery__item {
min-width: 0;
}Це дозволяє вмісту стискатися в межах комірки.
Якщо зображення має власний розмір або невірно задану висоту, воно може переповнювати картку.
.gallery__item {
overflow: hidden;
}
.gallery__image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}span 2 на мобільних екранах.gallery__item--wide {
grid-column: span 2;
}Якщо на мобільному екрані фактично доступна лише одна колонка, таке правило може створити неочікуване розміщення або переповнення.
Передбачте скасування розміру:
@media (max-width: 42rem) {
.gallery__item--wide {
grid-column: span 1;
}
}grid-template-areasДля кожної картки можна вручну створити області, але адаптивна галерея з динамічною кількістю елементів швидко стане складною в підтримці. Для нерегулярних карток зазвичай достатньо:
grid-column: span 2;
grid-row: span 2;Розширте галерею:
Додайте ще чотири картки.
Для однієї картки використайте grid-column: span 2.
Для іншої використайте grid-row: span 2.
Перевірте галерею на ширині приблизно 320px, 768px і 1440px.
Переконайтеся, що зображення не створюють горизонтального прокручування.
Замініть SVG на власні фотографії та додайте коректні alt.
display: grid створює основу адаптивної галереї.
repeat(auto-fit, ...) автоматично визначає кількість колонок.
minmax() задає мінімальний і максимальний розмір колонки.
min(100%, 15rem) захищає від переповнення на вузьких екранах.
grid-column: span 2 робить картку ширшою.
grid-row: span 2 робить картку вищою.
grid-auto-rows визначає базову висоту рядків.
grid-auto-flow: dense допомагає заповнювати вільні місця.
Для растрових зображень зазвичай потрібні width: 100%, height: 100% і object-fit: cover.
Великі картки потрібно перевіряти на мобільних розмірах і за потреби повертати до одного рядка або колонки.