48 Модуль 10 Реальные проекты 20 мин урок 23 из 25

Проект: дашборд

Плотный интерфейс: сайдбар, таблица с фиксированной шапкой, графики на чистом CSS, состояния загрузки и тёмная тема из коробки.

Что ты научишься делать

  • Соберёшь раскладку с сайдбаром, которая не ломается на планшете
  • Сделаешь таблицу с фиксированной шапкой и горизонтальной прокруткой
  • Нарисуешь три вида графиков без единой библиотеки

#Чем дашборд отличается от лендинга

Лендинг человек пролистывает один раз. Дашборд открывают каждый день и проводят в нём часы. Это меняет всё.

  • Плотность вместо воздуха. Пустое место, красивое на лендинге, здесь означает меньше данных на экране.
  • Никаких анимаций «для настроения». На третий день они начинают раздражать.
  • Тёмная тема обязательна. Люди сидят в интерфейсе вечерами.
  • Состояния важнее вида. Загрузка, пустой список, ошибка запроса встречаются постоянно.
  • Плотный текст. Мелкий шрифт и малые отступы, но контраст — строго по норме.

#Шаг 1. Раскладка

Классическая схема: сайдбар слева, шапка сверху, содержимое в остальном. Собирается одной сеткой на именованных областях — читается почти как картинка.

CSSlayout.css
@layer layout {  .app {    display: grid;    grid-template-areas:      "side head"      "side main";    grid-template-columns: 240px minmax(0, 1fr);    grid-template-rows: auto 1fr;    min-block-size: 100dvh;  }  .app__side { grid-area: side; }  .app__head { grid-area: head; }  .app__main { grid-area: main; overflow: auto; }  /* Планшет: сайдбар ужимается до иконок */  @media (max-width: 1100px) {    .app { grid-template-columns: 64px minmax(0, 1fr); }    .app__side .nav__label { display: none; }  }  /* Телефон: сайдбар уезжает вниз лентой */  @media (max-width: 720px) {    .app {      grid-template-areas: "head" "main" "side";      grid-template-columns: minmax(0, 1fr);      grid-template-rows: auto 1fr auto;    }    .app__side {      position: sticky;      inset-block-end: 0;      display: flex;      overflow-x: auto;    }  }}

#Шаг 2. Карточки показателей

Строка метрик
В песочницу

#Шаг 3. Графики на чистом CSS

Для трёх базовых видов графиков библиотека не нужна. Столбики — это сетка, кольцо — conic-gradient, линия — SVG в несколько строк.

Ключевой приём для всех трёх: значение приходит переменной, а CSS сам считает высоту и угол. Тогда данные можно подставлять на сервере, и никакой скрипт не нужен.

Три графика без библиотек
В песочницу

#Шаг 4. Таблица с фиксированной шапкой

Главная таблица дашборда: шапка не уезжает при вертикальной прокрутке, первая колонка — при горизонтальной. Раньше это делали двумя синхронизированными таблицами; теперь достаточно липкого позиционирования.

Таблица заказов
В песочницу

#Шаг 5. Состояния загрузки

Данные приходят с задержкой всегда. Скелет вместо крутящегося колёсика удерживает раскладку и не даёт странице прыгать, когда данные приехали.

CSSskeleton.css
@layer components {  .skeleton {    /* Размер тот же, что у настоящего содержимого,       иначе при загрузке страница дёрнется */    min-block-size: 1em;    border-radius: 6px;    background: linear-gradient(      90deg,      var(--color-surface) 0%,      color-mix(in oklab, var(--color-text) 8%, var(--color-surface)) 50%,      var(--color-surface) 100%    );    background-size: 200% 100%;    animation: shimmer 1.4s linear infinite;  }  @keyframes shimmer {    to { background-position: -200% 0; }  }  /* Без движения — просто ровная заливка */  @media (prefers-reduced-motion: reduce) {    .skeleton { animation: none; opacity: .6; }  }}

Скелет надо и озвучить: область с данными помечается aria-busy="true", пока идёт загрузка. Тогда программа чтения с экрана скажет «занято» вместо того, чтобы читать пустые блоки.

#Шаг 6. Тёмная тема

Если компоненты с самого начала ссылаются на семантические токены, тема добавляется одним блоком. Это ровно то, что разбирали в тридцать седьмом уроке, — здесь оно окупается.

Особенность дашборда: в тёмной теме нужно пересматривать не только цвета текста, но и цвета данных. Зелёный и красный, хорошо читавшиеся на белом, на тёмном фоне теряют контраст и требуют осветления.

CSSТема добавляется, а не переписывается
@layer tokens {  [data-theme="dark"] {    --color-bg: #0f1117;    --color-surface: #171a24;    --color-text: #e8eaf2;    --color-muted: #a7adc2;    --color-border: rgb(255 255 255 / .08);    /* Цвета данных осветлены: на тёмном фоне       насыщенные оттенки визуально темнеют */    --color-ok: #4bd1a0;    --color-warn: #e5b567;    --color-danger: #f2777a;  }}

#Плотность и читаемость

Дашборд живёт в противоречии: данных надо показать много, но текст обязан оставаться читаемым. Разрешается оно не уменьшением шрифта, а другими средствами.

Практические ориентиры, проверенные на живых интерфейсах.

ПараметрЛендингДашборд
Основной текст17–18px13–14px, но не меньше 13
Межстрочный интервал1.6–1.71.4–1.5
Отступ внутри карточки20–24px12–16px
Высота строки таблицы36–44px: меньше — тяжело попасть курсором
Контраст4.5 : 14.5 : 1, без исключений

#Пустые состояния

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

Хорошее пустое состояние отвечает на три вопроса: почему тут пусто, это нормально или ошибка, и что сделать дальше.

HTMLТри разных «пусто»
<!-- Данных ещё нет: подсказываем первый шаг --><div class="empty">  <p class="empty__title">Заказов пока нет</p>  <p class="empty__text">Они появятся здесь после первой продажи.</p>  <a class="btn" href="/catalog">Открыть каталог</a></div><!-- Фильтр ничего не нашёл: предлагаем ослабить --><div class="empty">  <p class="empty__title">Ничего не нашлось</p>  <p class="empty__text">Попробуй убрать фильтр по дате.</p>  <button class="btn btn--ghost" type="button">Сбросить фильтры</button></div><!-- Ошибка запроса: честно говорим и даём повторить --><div class="empty" role="alert">  <p class="empty__title">Не удалось загрузить</p>  <p class="empty__text">Проверь соединение и попробуй ещё раз.</p>  <button class="btn" type="button">Повторить</button></div>

#Грабли этого проекта

Что случилосьПочемуРешение
Таблица растянула весь дашбордКолонка сетки шире содержимого не бывает при 1frminmax(0, 1fr) у колонки содержимого
У липкой шапки пропали границыborder-collapse: collapse отдаёт границы таблицеborder-separate и границы у ячеек
Угол таблицы перекрываетсяЛипкие шапка и колонка на одном уровнеЯчейке угла больший z-index
Числа дёргаются при обновленииЦифры разной шириныfont-variant-numeric: tabular-nums
Страница прыгает после загрузкиСкелет другого размера, чем данныеСкелету те же размеры, что и содержимому
График не читается голосомНабор цветных блоков без текстаrole="img" с итогом или скрытая таблица

Где спотыкаются почти все

Колонка содержимого задана как <code>1fr</code>

Почему Минимальный размер такой колонки равен ширине содержимого. Широкая таблица растягивает всю сетку за экран.

Как надо minmax(0, 1fr) — колонка получает право быть уже содержимого.

Изменение показано только цветом

Почему Рост и падение, различимые лишь по зелёному и красному, недоступны части людей.

Как надо Добавь знак или стрелку рядом с числом.

Вместо скелета крутящееся колёсико

Почему Раскладка не зарезервирована — при появлении данных страница прыгает.

Как надо Скелет размером с будущее содержимое плюс aria-busy.

Цвета статусов не пересмотрены в тёмной теме

Почему Насыщенный зелёный на тёмном фоне теряет контраст и читается хуже нормы.

Как надо Отдельные значения токенов для тёмной темы, проверенные на контраст.

Скелет загрузки skeleton screen
Заглушка формой и размером с будущее содержимое.
Липкая шапка sticky header
Шапка таблицы, остающаяся на месте при прокрутке тела.
Табличные цифры tabular-nums
Начертание, где все цифры одной ширины.
Конический градиент conic-gradient
Градиент по кругу. Основа для кольцевых диаграмм на чистом CSS.

Проверь себя

  1. Почему колонку содержимого задают как <code>minmax(0, 1fr)</code>, а не <code>1fr</code>?

    Минимальный размер <code>1fr</code> равен размеру содержимого. Прокрутка должна появляться внутри таблицы, а не у страницы.

  2. Что ломает границы у липкой шапки таблицы?

    При схлопывании границы принадлежат таблице, а не ячейкам, и при прокрутке уезжают вместе с ней.

  3. Зачем в дашборде <code>font-variant-numeric: tabular-nums</code>?

    В большинстве шрифтов единица уже восьмёрки — на колонке чисел это хорошо заметно.

  4. Как сделать график доступным?

    Набор цветных прямоугольников для читалки — пустое место. Данные нужно дать текстом.

  5. Чем скелет лучше крутящегося индикатора?

    Скелет тех же размеров, что и будущее содержимое, убирает сдвиг вёрстки.

Задание

Таблица дашборда

Собери таблицу заказов: шапка не уезжает при вертикальной прокрутке, первая колонка — при горизонтальной, суммы выровнены по правому краю табличными цифрами.

Обёртке задай ограничение по высоте и прокрутку. Не забудь: при липкой шапке нельзя использовать border-collapse: collapse.

Открыть в песочнице
    Подсказка 1

    Обёртке: max-height: 240px; overflow: auto;

    Подсказка 2

    Таблице: border-collapse: separate; border-spacing: 0;

    Подсказка 3

    Шапке: position: sticky; top: 0; z-index: 2; Первой колонке: position: sticky; left: 0;

    Подсказка 4

    Суммам: .num { text-align: right; font-variant-numeric: tabular-nums; }

    Показать решение
    HTMLindex.html
    <div class="table-wrap">  <table class="table">    <thead>      <tr>        <th scope="col">Номер</th>        <th scope="col">Клиент</th>        <th scope="col">Статус</th>        <th scope="col" class="num">Сумма</th>      </tr>    </thead>    <tbody>      <tr><th scope="row">#4821</th><td>Марина К.</td><td>Оплачен</td><td class="num">1 240 ₽</td></tr>      <tr><th scope="row">#4820</th><td>Константин П.</td><td>Ждёт оплаты</td><td class="num">890 ₽</td></tr>      <tr><th scope="row">#4819</th><td>Анна С.</td><td>Отменён</td><td class="num">2 105 ₽</td></tr>      <tr><th scope="row">#4818</th><td>Пётр В.</td><td>Оплачен</td><td class="num">640 ₽</td></tr>      <tr><th scope="row">#4817</th><td>Ольга Д.</td><td>Оплачен</td><td class="num">3 480 ₽</td></tr>      <tr><th scope="row">#4816</th><td>Илья М.</td><td>Ждёт оплаты</td><td class="num">1 015 ₽</td></tr>    </tbody>  </table></div>
    CSSstyle.css
    .table-wrap {  max-height: 240px;  overflow: auto;  background: #fff;  border: 1px solid #e2e2ea;  border-radius: 12px;}.table {  width: 100%;  min-width: 480px;  font-size: 13px;  /* Схлопнутые границы уехали бы вместе с телом таблицы */  border-collapse: separate;  border-spacing: 0;}.table th,.table td {  padding: 9px 12px;  text-align: left;  border-bottom: 1px solid #eeedf4;  white-space: nowrap;}.table thead th {  position: sticky;  top: 0;  z-index: 2;  background: #f5f5fa;}.table tbody th {  position: sticky;  left: 0;  background: #fff;  font-weight: 600;}/* Угол выше обеих липких зон */.table thead th:first-child { z-index: 3; }.num {  text-align: right;  font-variant-numeric: tabular-nums;}

    Чек-лист сдачи дашборда