Пошук уроків, статей та іншого контенту
Створіть dashboard із бічним меню, панеллю статистики, таблицею даних і картками показників на основі CSS Grid та Flexbox.
Dashboard — це інтерфейс для швидкого перегляду ключової інформації: статистики, показників, списків і останніх операцій.
У цьому уроці створимо адаптивну панель керування з такими частинами:
бічне меню;
верхня панель із заголовком і профілем користувача;
картки статистики;
картки з додатковими показниками;
таблиця останніх замовлень.
Для основного компонування використаємо CSS Grid, а для розташування елементів усередині компонентів — Flexbox.
Загальна структура сторінки матиме два основні стовпці:
.dashboard
├── .sidebar
└── .main
├── .topbar
├── .stats-grid
├── .content-grid
│ ├── .chart-card
│ └── .activity-card
└── .orders-cardБічне меню займає перший стовпець, а основний вміст — другий. На вузьких екранах ці області розташовуються одна під одною.
Створіть файл index.html і додайте до нього такий код:
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard</title>
<style>
:root {
--color-primary: #4f46e5;
--color-primary-dark: #3730a3;
--color-background: #f5f7fb;
--color-surface: #ffffff;
--color-sidebar: #111827;
--color-text: #1f2937;
--color-muted: #6b7280;
--color-border: #e5e7eb;
--color-success: #15803d;
--color-danger: #dc2626;
--radius: 14px;
--shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
font-family: Arial, sans-serif;
line-height: 1.5;
}
button,
input {
font: inherit;
}
button {
border: 0;
cursor: pointer;
}
.dashboard {
display: grid;
grid-template-columns: 250px minmax(0, 1fr);
min-height: 100vh;
}
.sidebar {
display: flex;
flex-direction: column;
padding: 24px 16px;
color: #ffffff;
background: var(--color-sidebar);
}
.logo {
margin-bottom: 40px;
padding: 0 12px;
font-size: 22px;
font-weight: 700;
}
.navigation {
display: flex;
flex-direction: column;
gap: 6px;
}
.navigation a {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 8px;
color: #9ca3af;
text-decoration: none;
transition: background-color 0.2s, color 0.2s;
}
.navigation a:hover,
.navigation a.active {
color: #ffffff;
background: var(--color-primary);
}
.sidebar-footer {
margin-top: auto;
padding: 16px 12px 0;
color: #9ca3af;
font-size: 13px;
}
.main {
min-width: 0;
padding: 28px;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 28px;
}
.page-title {
font-size: 28px;
line-height: 1.2;
}
.page-subtitle {
margin-top: 6px;
color: var(--color-muted);
}
.user-panel {
display: flex;
align-items: center;
gap: 12px;
}
.user-info {
text-align: right;
}
.user-name {
font-weight: 700;
}
.user-role {
color: var(--color-muted);
font-size: 13px;
}
.avatar {
display: grid;
width: 44px;
height: 44px;
place-items: center;
border-radius: 50%;
color: #ffffff;
background: var(--color-primary);
font-weight: 700;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 18px;
margin-bottom: 24px;
}
.card {
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
box-shadow: var(--shadow);
}
.stat-card {
padding: 20px;
}
.stat-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 18px;
}
.stat-label {
color: var(--color-muted);
font-size: 14px;
}
.stat-icon {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border-radius: 10px;
color: var(--color-primary);
background: #eef2ff;
font-size: 18px;
}
.stat-value {
display: block;
margin-bottom: 8px;
font-size: 26px;
}
.stat-change {
font-size: 13px;
font-weight: 700;
}
.stat-change.positive {
color: var(--color-success);
}
.stat-change.negative {
color: var(--color-danger);
}
.content-grid {
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(280px, 1fr);
gap: 24px;
margin-bottom: 24px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 20px 22px;
border-bottom: 1px solid var(--color-border);
}
.card-title {
font-size: 18px;
}
.card-link {
color: var(--color-primary);
font-size: 14px;
text-decoration: none;
}
.chart-card {
min-height: 300px;
}
.chart {
display: flex;
align-items: end;
gap: 14px;
height: 220px;
padding: 24px 22px;
}
.bar-wrapper {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: end;
gap: 8px;
height: 100%;
color: var(--color-muted);
font-size: 12px;
}
.bar {
width: 100%;
max-width: 42px;
min-height: 12px;
border-radius: 6px 6px 0 0;
background: var(--color-primary);
}
.activity-list {
display: flex;
flex-direction: column;
gap: 18px;
padding: 20px 22px;
list-style: none;
}
.activity-item {
display: flex;
align-items: flex-start;
gap: 12px;
}
.activity-icon {
display: grid;
flex: 0 0 34px;
width: 34px;
height: 34px;
place-items: center;
border-radius: 50%;
color: var(--color-primary);
background: #eef2ff;
}
.activity-text {
font-size: 14px;
}
.activity-time {
display: block;
margin-top: 3px;
color: var(--color-muted);
font-size: 12px;
}
.orders-card {
overflow: hidden;
}
.table-container {
overflow-x: auto;
}
.orders-table {
width: 100%;
min-width: 680px;
border-collapse: collapse;
text-align: left;
}
.orders-table th,
.orders-table td {
padding: 16px 22px;
border-bottom: 1px solid var(--color-border);
white-space: nowrap;
}
.orders-table th {
color: var(--color-muted);
font-size: 13px;
font-weight: 600;
}
.orders-table td {
font-size: 14px;
}
.orders-table tbody tr:last-child td {
border-bottom: 0;
}
.status {
display: inline-block;
padding: 5px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
.status.completed {
color: var(--color-success);
background: #dcfce7;
}
.status.pending {
color: #a16207;
background: #fef9c3;
}
.status.cancelled {
color: var(--color-danger);
background: #fee2e2;
}
@media (max-width: 1100px) {
.stats-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.content-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 700px) {
.dashboard {
grid-template-columns: 1fr;
}
.sidebar {
padding: 16px;
}
.logo {
margin-bottom: 18px;
}
.navigation {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
.sidebar-footer {
display: none;
}
.main {
padding: 20px 16px;
}
.topbar {
align-items: flex-start;
flex-direction: column;
}
.user-panel {
align-self: stretch;
justify-content: space-between;
}
.user-info {
text-align: left;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="dashboard">
<aside class="sidebar">
<div class="logo">InsightBoard</div>
<nav class="navigation" aria-label="Основна навігація">
<a class="active" href="#">
<span aria-hidden="true">▦</span>
<span>Огляд</span>
</a>
<a href="#">
<span aria-hidden="true">▣</span>
<span>Замовлення</span>
</a>
<a href="#">
<span aria-hidden="true">♙</span>
<span>Клієнти</span>
</a>
<a href="#">
<span aria-hidden="true">⚙</span>
<span>Налаштування</span>
</a>
</nav>
<p class="sidebar-footer">Версія 1.0.0</p>
</aside>
<main class="main">
<header class="topbar">
<div>
<h1 class="page-title">Огляд</h1>
<p class="page-subtitle">Основні показники вашого магазину</p>
</div>
<div class="user-panel">
<div class="user-info">
<p class="user-name">Олена Коваль</p>
<p class="user-role">Адміністратор</p>
</div>
<div class="avatar" aria-label="Аватар Олени Коваль">ОК</div>
</div>
</header>
<section class="stats-grid" aria-label="Основна статистика">
<article class="card stat-card">
<div class="stat-header">
<span class="stat-label">Загальний дохід</span>
<span class="stat-icon" aria-hidden="true">₴</span>
</div>
<strong class="stat-value">₴128 430</strong>
<span class="stat-change positive">↑ 12,5% за місяць</span>
</article>
<article class="card stat-card">
<div class="stat-header">
<span class="stat-label">Замовлення</span>
<span class="stat-icon" aria-hidden="true">▣</span>
</div>
<strong class="stat-value">1 248</strong>
<span class="stat-change positive">↑ 8,2% за місяць</span>
</article>
<article class="card stat-card">
<div class="stat-header">
<span class="stat-label">Нові клієнти</span>
<span class="stat-icon" aria-hidden="true">♙</span>
</div>
<strong class="stat-value">342</strong>
<span class="stat-change positive">↑ 5,4% за місяць</span>
</article>
<article class="card stat-card">
<div class="stat-header">
<span class="stat-label">Повернення</span>
<span class="stat-icon" aria-hidden="true">↩</span>
</div>
<strong class="stat-value">2,4%</strong>
<span class="stat-change negative">↓ 1,1% за місяць</span>
</article>
</section>
<section class="content-grid">
<article class="card chart-card">
<div class="card-header">
<h2 class="card-title">Продажі за тиждень</h2>
<a class="card-link" href="#">Детальніше</a>
</div>
<div class="chart" aria-label="Графік продажів за тиждень">
<div class="bar-wrapper">
<div class="bar" style="height: 45%;"></div>
<span>Пн</span>
</div>
<div class="bar-wrapper">
<div class="bar" style="height: 65%;"></div>
<span>Вт</span>
</div>
<div class="bar-wrapper">
<div class="bar" style="height: 52%;"></div>
<span>Ср</span>
</div>
<div class="bar-wrapper">
<div class="bar" style="height: 80%;"></div>
<span>Чт</span>
</div>
<div class="bar-wrapper">
<div class="bar" style="height: 68%;"></div>
<span>Пт</span>
</div>
<div class="bar-wrapper">
<div class="bar" style="height: 92%;"></div>
<span>Сб</span>
</div>
<div class="bar-wrapper">
<div class="bar" style="height: 74%;"></div>
<span>Нд</span>
</div>
</div>
</article>
<article class="card activity-card">
<div class="card-header">
<h2 class="card-title">Остання активність</h2>
</div>
<ul class="activity-list">
<li class="activity-item">
<span class="activity-icon" aria-hidden="true">✓</span>
<p class="activity-text">
Замовлення #1048 успішно виконано
<span class="activity-time">10 хвилин тому</span>
</p>
</li>
<li class="activity-item">
<span class="activity-icon" aria-hidden="true">+</span>
<p class="activity-text">
Зареєстровано нового клієнта
<span class="activity-time">35 хвилин тому</span>
</p>
</li>
<li class="activity-item">
<span class="activity-icon" aria-hidden="true">₴</span>
<p class="activity-text">
Отримано новий платіж
<span class="activity-time">1 годину тому</span>
</p>
</li>
</ul>
</article>
</section>
<section class="card orders-card">
<div class="card-header">
<h2 class="card-title">Останні замовлення</h2>
<a class="card-link" href="#">Переглянути всі</a>
</div>
<div class="table-container">
<table class="orders-table">
<thead>
<tr>
<th>Замовлення</th>
<th>Клієнт</th>
<th>Дата</th>
<th>Сума</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
<tr>
<td>#1048</td>
<td>Марія Бондар</td>
<td>12 червня 2025</td>
<td>₴2 450</td>
<td><span class="status completed">Виконано</span></td>
</tr>
<tr>
<td>#1047</td>
<td>Андрій Мельник</td>
<td>12 червня 2025</td>
<td>₴1 890</td>
<td><span class="status pending">В обробці</span></td>
</tr>
<tr>
<td>#1046</td>
<td>Ірина Шевченко</td>
<td>11 червня 2025</td>
<td>₴3 200</td>
<td><span class="status completed">Виконано</span></td>
</tr>
<tr>
<td>#1045</td>
<td>Олег Ткаченко</td>
<td>11 червня 2025</td>
<td>₴760</td>
<td><span class="status cancelled">Скасовано</span></td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
</div>
</body>
</html>Головний контейнер сторінки:
.dashboard {
display: grid;
grid-template-columns: 250px minmax(0, 1fr);
}Властивість grid-template-columns створює два стовпці:
250px — фіксована ширина бічного меню;
minmax(0, 1fr) — основна область, яка займає весь доступний простір.
minmax(0, 1fr) корисний для dashboard, оскільки він не дозволяє довгому вмісту неконтрольовано розширювати основний стовпець.
Внутрішні блоки також побудовані за допомогою Grid:
.stats-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 18px;
}repeat(4, minmax(0, 1fr)) створює чотири однакові стовпці для карток статистики.
Для центральної частини використано різні пропорції:
.content-grid {
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(280px, 1fr);
gap: 24px;
}Графік отримує більше місця, а блок активності — щонайменше 280px.
Grid відповідає за розташування великих областей сторінки, а Flexbox — за внутрішнє вирівнювання.
Наприклад, верхня панель:
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}Тут:
display: flex розміщує заголовок і профіль в одному рядку;
justify-content: space-between розсовує їх до протилежних країв;
align-items: center вирівнює елементи по вертикалі;
gap додає відстань між елементами.
Бічне меню використовує вертикальний Flexbox:
.sidebar {
display: flex;
flex-direction: column;
}
.sidebar-footer {
margin-top: auto;
}Властивість margin-top: auto відсуває підвал меню до нижнього краю бічної панелі. Це зручний спосіб розподілити вільний простір без абсолютного позиціонування.
Кожна картка має однакову структуру:
<article class="card stat-card">
<div class="stat-header">
<span class="stat-label">Загальний дохід</span>
<span class="stat-icon">₴</span>
</div>
<strong class="stat-value">₴128 430</strong>
<span class="stat-change positive">↑ 12,5% за місяць</span>
</article>Для спільного вигляду карток використовується клас .card:
.card {
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
box-shadow: var(--shadow);
}Цей підхід називається повторним використанням класів: загальні стилі зберігаються в одному місці, а специфічні стилі додаються окремими класами, наприклад .stat-card.
Значення кольорів і відступів винесені в CSS-змінні:
:root {
--color-primary: #4f46e5;
--color-background: #f5f7fb;
--color-surface: #ffffff;
}Якщо потрібно змінити основний колір dashboard, достатньо змінити значення --color-primary.
Таблиця міститься в окремому контейнері:
.table-container {
overflow-x: auto;
}Самій таблиці задано мінімальну ширину:
.orders-table {
width: 100%;
min-width: 680px;
}На малих екранах таблиця може бути ширшою за видиму область. Завдяки overflow-x: auto користувач зможе прокручувати її горизонтально, а не побачить зламаний макет.
Це практичніше, ніж примусово стискати всі стовпці або приховувати частину даних.
Статус замовлення оформлений як окремий компонент:
<span class="status completed">Виконано</span>Класи .completed, .pending і .cancelled визначають лише колір конкретного стану:
.status.completed {
color: var(--color-success);
background: #dcfce7;
}
.status.pending {
color: #a16207;
background: #fef9c3;
}Для dashboard використано два контрольні розміри.
На екранах шириною до 1100px:
@media (max-width: 1100px) {
.stats-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.content-grid {
grid-template-columns: 1fr;
}
}Відбуваються такі зміни:
чотири картки статистики перетворюються на два стовпці;
графік і активність розміщуються один під одним.
На екранах до 700px:
@media (max-width: 700px) {
.dashboard {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
}Бічне меню більше не займає окремий постійний стовпець. Воно розташовується над основним вмістом, а картки статистики переходять в один стовпець.
Адаптивні правила не змінюють HTML-структуру. Вони лише змінюють спосіб компонування вже наявних елементів.
У прикладі використано семантичні елементи:
<aside> — додатковий вміст і бічна навігація;
<nav> — навігаційні посилання;
<main> — основний вміст сторінки;
<section> — логічні групи даних;
<article> — самостійні картки;
<table> — табличні дані.
Для навігації додано атрибут:
<nav aria-label="Основна навігація">Він допомагає програмам читання з екрана зрозуміти призначення блоку.
Декоративні символи приховано від таких програм за допомогою:
<span aria-hidden="true">▦</span>Важливе текстове значення при цьому залишається доступним в іншому <span>.
Не варто будувати весь dashboard за допомогою position: absolute. Такий макет складно підтримувати й адаптувати.
Для основних областей використовуйте Grid, для внутрішнього вирівнювання — Flexbox.
min-width: 0Grid-елемент за замовчуванням може не стискатися через довгий текст або широку таблицю. Тому для основного контейнера додано:
.main {
min-width: 0;
}Це дозволяє вмісту коректно залишатися в межах доступного стовпця.
Якщо кожній картці задати жорстку ширину, на менших екранах з’явиться горизонтальна прокрутка всієї сторінки.
Краще використовувати:
minmax();
одиниці fr;
width: 100%;
медіазапити.
Не приховуйте стовпці таблиці без потреби. Для широких таблиць краще створити внутрішню горизонтальну прокрутку через overflow-x: auto.
Якщо всі стилі записати в одному великому селекторі, компонент буде складно повторно використати.
Краще розділяти:
.card — спільний вигляд карток;
.stat-card — особливості картки статистики;
.orders-card — особливості таблиці замовлень.
Розширте dashboard:
Додайте п’яту картку з показником «Середній чек».
Додайте ще два посилання до бічного меню.
Створіть новий рядок у таблиці замовлень.
Змініть кількість стовпців карток на екранах середнього розміру.
Додайте окремий колір для нового статусу замовлення.
Перевірте результат у браузері на широкому та вузькому екранах.
У цьому уроці ви:
створили структуру dashboard із бічним меню та основним вмістом;
використали CSS Grid для великих областей сторінки;
використали Flexbox для вирівнювання елементів усередині компонентів;
створили картки статистики;
оформили таблицю даних;
додали горизонтальну прокрутку для таблиці;
зробили макет адаптивним за допомогою медіазапитів;
застосували CSS-змінні та повторно використовувані класи;
використали семантичну HTML-структуру для кращої доступності.