Создание макетов, которые работают на экранах от 320px до 2560px — главная задача верстальщика. В этом уроке разберём готовые паттерны: адаптивные сетки, карточки, сайдбар-макеты, Holy Grail и адаптивные таблицы. Все решения построены на CSS Grid и Flexbox — без хаков и JavaScript.
Самый мощный паттерн адаптивности — сетка, которая сама решает, сколько колонок помещается.
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
}Как это работает:
auto-fit — «впихни столько колонок, сколько влезает»minmax(280px, 1fr) — каждая колонка не уже 280px, но может растягиваться/* auto-fit: оставшееся пространство распределяется между элементами */
.grid-fit {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
/* auto-fill: создаёт пустые колонки, элементы не растягиваются */
.grid-fill {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}Если элементов мало:
auto-fit — растянет карточки на всю ширинуauto-fill — оставит пустое место справаДля карточек товаров обычно нужен auto-fill (карточки одного размера), для контентных блоков — auto-fit.
.card {
display: flex;
flex-direction: column;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.2s;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.card img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.card-body {
padding: 16px;
flex: 1; /* Растягивается — кнопка всегда внизу */
display: flex;
flex-direction: column;
}
.card-body .btn {
margin-top: auto; /* Прижимается к низу карточки */
}Ключевые приёмы:
aspect-ratio — одинаковые пропорции у всех картинокflex: 1 + margin-top: auto — кнопка всегда прижата к низуoverflow: hidden — картинка не вылезает за скругления.layout {
display: grid;
grid-template-columns: minmax(200px, 300px) 1fr;
gap: 24px;
}
@media (max-width: 768px) {
.layout {
grid-template-columns: 1fr;
}
}.layout {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.sidebar {
flex: 1 1 250px; /* Растёт от 250px, но может быть шире */
max-width: 300px;
}
.main {
flex: 1 1 600px; /* Растёт от 600px — если не влезает, переносится */
}
@media (max-width: 768px) {
.sidebar {
max-width: 100%;
}
}Когда контейнер уже, чем 250 + 600 = 850px, .main переносится на новую строку → становится на 100% ширины.
Классический макет: шапка, подвал, основной контент, левый и правый сайдбар.
.page {
display: grid;
min-height: 100dvh;
grid-template:
"header header header" auto
"left main right" 1fr
"footer footer footer" auto
/ 200px 1fr 200px;
}
.header { grid-area: header; }
.left { grid-area: left; }
.main { grid-area: main; }
.right { grid-area: right; }
.footer { grid-area: footer; }
@media (max-width: 1024px) {
.page {
grid-template:
"header" auto
"main" 1fr
"left" auto
"right" auto
"footer" auto
/ 1fr;
}
}На мобильном все области выстраиваются в одну колонку, а основной контент идёт первым (до сайдбаров).
Паттерн «featured + grid»: первый элемент занимает всю ширину или две колонки.
.featured-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.featured-grid .featured {
grid-column: 1 / -1; /* Вся ширина */
}
@media (min-width: 768px) {
.featured-grid .featured {
grid-column: span 2; /* Две колонки на планшете+ */
grid-row: span 2;
}
}Таблицы — одна из самых сложных задач адаптивности. Вот три стратегии.
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table {
min-width: 600px; /* Таблица не сжимается, скроллится */
width: 100%;
border-collapse: collapse;
}Простейшее решение. Пользователь свайпает таблицу горизонтально.
@media (max-width: 768px) {
table, thead, tbody, th, td, tr {
display: block;
}
thead {
display: none; /* Скрываем заголовки */
}
tr {
margin-bottom: 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
}
td {
display: flex;
justify-content: space-between;
padding: 6px 0;
border: none;
}
td::before {
content: attr(data-label); /* Подставляем заголовок из data-атрибута */
font-weight: 600;
margin-right: 16px;
}
}<table>
<thead>
<tr>
<th>Имя</th><th>Email</th><th>Роль</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Имя">Анна</td>
<td data-label="Email">anna@mail.ru</td>
<td data-label="Роль">Админ</td>
</tr>
</tbody>
</table>.col-secondary {
display: table-cell;
}
@media (max-width: 768px) {
.col-secondary {
display: none; /* Менее важные колонки скрываются */
}
}.products {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: clamp(12px, 2vw, 24px); /* Fluid отступы */
padding: clamp(16px, 3vw, 48px);
}
.product-card {
display: flex;
flex-direction: column;
border-radius: 12px;
overflow: hidden;
background: white;
border: 1px solid #e2e8f0;
}
.product-card img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}
.product-card .info {
padding: clamp(12px, 2vw, 20px);
flex: 1;
display: flex;
flex-direction: column;
}
.product-card .price {
font-size: clamp(1.125rem, 1rem + 0.5vw, 1.5rem);
font-weight: 700;
margin-top: auto;
}Эта сетка:
| Задача | Решение |
|--------|---------|
| Адаптивная сетка карточек | repeat(auto-fit, minmax(280px, 1fr)) |
| Сайдбар + контент | Grid + 1fr или Flex + flex-wrap |
| Holy Grail | grid-template-areas |
| Кнопка внизу карточки | flex: 1 + margin-top: auto |
| Адаптивная таблица | Горизонтальный скролл или карточки |
| Fluid отступы | clamp() в gap/padding |
Адаптивная сетка карточек с auto-fit и сайдбар-макет
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { margin: 0; box-sizing: border-box; }
body {
font-family: system-ui, sans-serif;
background: #f1f5f9;
color: #1e293b;
}
/* Макет с сайдбаром */
.page {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
max-width: 1280px;
margin: 0 auto;
padding: clamp(16px, 3vw, 48px);
}
@media (min-width: 960px) {
.page {
grid-template-columns: 240px 1fr;
}
}
.sidebar {
background: white;
border-radius: 12px;
padding: 20px;
border: 1px solid #e2e8f0;
align-self: start;
}
.sidebar h3 {
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
margin-bottom: 12px;
}
.sidebar ul {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar li {
padding: 8px 0;
border-bottom: 1px solid #f1f5f9;
}
/* Адаптивная сетка карточек */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: clamp(12px, 2vw, 20px);
}
.card {
background: white;
border-radius: 12px;
overflow: hidden;
border: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
transition: box-shadow 0.2s;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.card-img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
background: linear-gradient(135deg, #dbeafe, #c7d2fe);
}
.card-body {
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
}
.card-body h3 {
font-size: 1.1rem;
margin-bottom: 8px;
}
.card-body p {
color: #64748b;
font-size: 0.9rem;
margin-bottom: 16px;
}
.card-body .btn {
margin-top: auto;
display: inline-block;
padding: 8px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
font-size: 0.875rem;
cursor: pointer;
text-align: center;
}
</style>
</head>
<body>
<div class="page">
<aside class="sidebar">
<h3>Категории</h3>
<ul>
<li>Все товары</li>
<li>Электроника</li>
<li>Одежда</li>
<li>Книги</li>
</ul>
</aside>
<main class="cards">
<div class="card">
<div class="card-img"></div>
<div class="card-body">
<h3>Товар 1</h3>
<p>Описание товара с основными характеристиками</p>
<button class="btn">Подробнее</button>
</div>
</div>
<div class="card">
<div class="card-img"></div>
<div class="card-body">
<h3>Товар 2</h3>
<p>Ещё один товар с кратким описанием</p>
<button class="btn">Подробнее</button>
</div>
</div>
<div class="card">
<div class="card-img"></div>
<div class="card-body">
<h3>Товар 3</h3>
<p>Третий товар каталога</p>
<button class="btn">Подробнее</button>
</div>
</div>
<div class="card">
<div class="card-img"></div>
<div class="card-body">
<h3>Товар 4</h3>
<p>Описание четвёртого товара</p>
<button class="btn">Подробнее</button>
</div>
</div>
<div class="card">
<div class="card-img"></div>
<div class="card-body">
<h3>Товар 5</h3>
<p>Пятый товар в сетке</p>
<button class="btn">Подробнее</button>
</div>
</div>
<div class="card">
<div class="card-img"></div>
<div class="card-body">
<h3>Товар 6</h3>
<p>Шестой товар каталога</p>
<button class="btn">Подробнее</button>
</div>
</div>
</main>
</div>
</body>
</html>Создание макетов, которые работают на экранах от 320px до 2560px — главная задача верстальщика. В этом уроке разберём готовые паттерны: адаптивные сетки, карточки, сайдбар-макеты, Holy Grail и адаптивные таблицы. Все решения построены на CSS Grid и Flexbox — без хаков и JavaScript.
Самый мощный паттерн адаптивности — сетка, которая сама решает, сколько колонок помещается.
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
}Как это работает:
auto-fit — «впихни столько колонок, сколько влезает»minmax(280px, 1fr) — каждая колонка не уже 280px, но может растягиваться/* auto-fit: оставшееся пространство распределяется между элементами */
.grid-fit {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
/* auto-fill: создаёт пустые колонки, элементы не растягиваются */
.grid-fill {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}Если элементов мало:
auto-fit — растянет карточки на всю ширинуauto-fill — оставит пустое место справаДля карточек товаров обычно нужен auto-fill (карточки одного размера), для контентных блоков — auto-fit.
.card {
display: flex;
flex-direction: column;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.2s;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.card img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.card-body {
padding: 16px;
flex: 1; /* Растягивается — кнопка всегда внизу */
display: flex;
flex-direction: column;
}
.card-body .btn {
margin-top: auto; /* Прижимается к низу карточки */
}Ключевые приёмы:
aspect-ratio — одинаковые пропорции у всех картинокflex: 1 + margin-top: auto — кнопка всегда прижата к низуoverflow: hidden — картинка не вылезает за скругления.layout {
display: grid;
grid-template-columns: minmax(200px, 300px) 1fr;
gap: 24px;
}
@media (max-width: 768px) {
.layout {
grid-template-columns: 1fr;
}
}.layout {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.sidebar {
flex: 1 1 250px; /* Растёт от 250px, но может быть шире */
max-width: 300px;
}
.main {
flex: 1 1 600px; /* Растёт от 600px — если не влезает, переносится */
}
@media (max-width: 768px) {
.sidebar {
max-width: 100%;
}
}Когда контейнер уже, чем 250 + 600 = 850px, .main переносится на новую строку → становится на 100% ширины.
Классический макет: шапка, подвал, основной контент, левый и правый сайдбар.
.page {
display: grid;
min-height: 100dvh;
grid-template:
"header header header" auto
"left main right" 1fr
"footer footer footer" auto
/ 200px 1fr 200px;
}
.header { grid-area: header; }
.left { grid-area: left; }
.main { grid-area: main; }
.right { grid-area: right; }
.footer { grid-area: footer; }
@media (max-width: 1024px) {
.page {
grid-template:
"header" auto
"main" 1fr
"left" auto
"right" auto
"footer" auto
/ 1fr;
}
}На мобильном все области выстраиваются в одну колонку, а основной контент идёт первым (до сайдбаров).
Паттерн «featured + grid»: первый элемент занимает всю ширину или две колонки.
.featured-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.featured-grid .featured {
grid-column: 1 / -1; /* Вся ширина */
}
@media (min-width: 768px) {
.featured-grid .featured {
grid-column: span 2; /* Две колонки на планшете+ */
grid-row: span 2;
}
}Таблицы — одна из самых сложных задач адаптивности. Вот три стратегии.
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table {
min-width: 600px; /* Таблица не сжимается, скроллится */
width: 100%;
border-collapse: collapse;
}Простейшее решение. Пользователь свайпает таблицу горизонтально.
@media (max-width: 768px) {
table, thead, tbody, th, td, tr {
display: block;
}
thead {
display: none; /* Скрываем заголовки */
}
tr {
margin-bottom: 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
}
td {
display: flex;
justify-content: space-between;
padding: 6px 0;
border: none;
}
td::before {
content: attr(data-label); /* Подставляем заголовок из data-атрибута */
font-weight: 600;
margin-right: 16px;
}
}<table>
<thead>
<tr>
<th>Имя</th><th>Email</th><th>Роль</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Имя">Анна</td>
<td data-label="Email">anna@mail.ru</td>
<td data-label="Роль">Админ</td>
</tr>
</tbody>
</table>.col-secondary {
display: table-cell;
}
@media (max-width: 768px) {
.col-secondary {
display: none; /* Менее важные колонки скрываются */
}
}.products {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: clamp(12px, 2vw, 24px); /* Fluid отступы */
padding: clamp(16px, 3vw, 48px);
}
.product-card {
display: flex;
flex-direction: column;
border-radius: 12px;
overflow: hidden;
background: white;
border: 1px solid #e2e8f0;
}
.product-card img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}
.product-card .info {
padding: clamp(12px, 2vw, 20px);
flex: 1;
display: flex;
flex-direction: column;
}
.product-card .price {
font-size: clamp(1.125rem, 1rem + 0.5vw, 1.5rem);
font-weight: 700;
margin-top: auto;
}Эта сетка:
| Задача | Решение |
|--------|---------|
| Адаптивная сетка карточек | repeat(auto-fit, minmax(280px, 1fr)) |
| Сайдбар + контент | Grid + 1fr или Flex + flex-wrap |
| Holy Grail | grid-template-areas |
| Кнопка внизу карточки | flex: 1 + margin-top: auto |
| Адаптивная таблица | Горизонтальный скролл или карточки |
| Fluid отступы | clamp() в gap/padding |
Адаптивная сетка карточек с auto-fit и сайдбар-макет
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { margin: 0; box-sizing: border-box; }
body {
font-family: system-ui, sans-serif;
background: #f1f5f9;
color: #1e293b;
}
/* Макет с сайдбаром */
.page {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
max-width: 1280px;
margin: 0 auto;
padding: clamp(16px, 3vw, 48px);
}
@media (min-width: 960px) {
.page {
grid-template-columns: 240px 1fr;
}
}
.sidebar {
background: white;
border-radius: 12px;
padding: 20px;
border: 1px solid #e2e8f0;
align-self: start;
}
.sidebar h3 {
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
margin-bottom: 12px;
}
.sidebar ul {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar li {
padding: 8px 0;
border-bottom: 1px solid #f1f5f9;
}
/* Адаптивная сетка карточек */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: clamp(12px, 2vw, 20px);
}
.card {
background: white;
border-radius: 12px;
overflow: hidden;
border: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
transition: box-shadow 0.2s;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.card-img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
background: linear-gradient(135deg, #dbeafe, #c7d2fe);
}
.card-body {
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
}
.card-body h3 {
font-size: 1.1rem;
margin-bottom: 8px;
}
.card-body p {
color: #64748b;
font-size: 0.9rem;
margin-bottom: 16px;
}
.card-body .btn {
margin-top: auto;
display: inline-block;
padding: 8px 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
font-size: 0.875rem;
cursor: pointer;
text-align: center;
}
</style>
</head>
<body>
<div class="page">
<aside class="sidebar">
<h3>Категории</h3>
<ul>
<li>Все товары</li>
<li>Электроника</li>
<li>Одежда</li>
<li>Книги</li>
</ul>
</aside>
<main class="cards">
<div class="card">
<div class="card-img"></div>
<div class="card-body">
<h3>Товар 1</h3>
<p>Описание товара с основными характеристиками</p>
<button class="btn">Подробнее</button>
</div>
</div>
<div class="card">
<div class="card-img"></div>
<div class="card-body">
<h3>Товар 2</h3>
<p>Ещё один товар с кратким описанием</p>
<button class="btn">Подробнее</button>
</div>
</div>
<div class="card">
<div class="card-img"></div>
<div class="card-body">
<h3>Товар 3</h3>
<p>Третий товар каталога</p>
<button class="btn">Подробнее</button>
</div>
</div>
<div class="card">
<div class="card-img"></div>
<div class="card-body">
<h3>Товар 4</h3>
<p>Описание четвёртого товара</p>
<button class="btn">Подробнее</button>
</div>
</div>
<div class="card">
<div class="card-img"></div>
<div class="card-body">
<h3>Товар 5</h3>
<p>Пятый товар в сетке</p>
<button class="btn">Подробнее</button>
</div>
</div>
<div class="card">
<div class="card-img"></div>
<div class="card-body">
<h3>Товар 6</h3>
<p>Шестой товар каталога</p>
<button class="btn">Подробнее</button>
</div>
</div>
</main>
</div>
</body>
</html>Создай адаптивную сетку карточек, которая автоматически подстраивается под ширину экрана без медиа-запросов. Каждая карточка должна быть не уже 260px и растягиваться равномерно. Используй `auto-fit` и `minmax()`. Отступы между карточками должны быть fluid (от 12px до 24px).
Для сетки: `display: grid`, `repeat(auto-fit, minmax(260px, 1fr))`, `gap: clamp(12px, 2vw, 24px)`. Карточки — `flex-direction: column`. Для картинки `aspect-ratio: 16 / 9`. Для body карточки `flex: 1`, а для цены `margin-top: auto` чтобы прижать её к низу.