Как сверстать список задач

Эта статья основана на одном из практических заданий verstaem.online.

Вы можете читать материал как обычное руководство или сначала попробовать выполнить задание самостоятельно, а затем сравнить свое решение с подробным разбором.

Перейти к заданию

В этой статье мы создадим современный список задач с названием, описанием, тегами, участниками, сроком выполнения и кнопками управления. На его примере разберем, как использовать Flexbox для построения сложных интерфейсов, оформлять декоративные элементы через псевдоэлементы, создавать группы аватаров и делать универсальные карточки задач.

Создаем HTML-разметку

Сначала создадим базовую HTML-разметку с помощью Emmet. Для этого достаточно ввести ! и нажать Tab. Обратите внимание на атрибут lang. Поскольку основной контент страницы написан на русском языке, здесь используем значение ru.

<!doctype html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Список задач</title>
  </head>
  <body>
  </body>
</html>

Поскольку у нас несколько одинаковых задач, логично использовать обычный список <ul>. Это поможет скринридерам прочитывать количество элементов. Внутри списка каждая задача располагается в своем <li>. Однако сама задача — это самостоятельный интерфейсный компонент, который вполне может существовать отдельно от списка. Поэтому внутри каждого элемента списка создадим тег <article>. Такой подход часто используется в реальных проектах и делает разметку более осмысленной.

<ul class="tasks">
    <li class="tasks__item">
        <article class="task">

        </article>
    </li>
</ul>

Теперь наполним карточку содержимым. Сверху расположим блок с названием задачи и кнопкой дополнительного меню. Ниже разместим теги, затем нижнюю строку с участниками, датой завершения и кнопкой открытия задачи. Кстати, теги тоже вполне можно сделать списком, но это уже мелочи.

Помимо этого, добавим изображения пользователей (обязательно указываем alt), а также дату и кнопку.

Отдельно стоит обратить внимание на кнопку с тремя точками. У нее нет текстового содержимого, поэтому добавим атрибут aria-label. Благодаря этому скринридеры смогут корректно озвучить назначение кнопки, даже несмотря на то, что внутри находится только SVG-иконка.

<ul class="tasks">
  <li class="tasks__item">
    <article class="task">
      <div class="task__top">
        <div class="task__text">
          <h3 class="task__title">
            Подготовить новый интерфейс страницы обучения
          </h3>
          <p class="task__descr">
            Переработать структуру блоков, улучшить визуальную иерархию и обновить систему отступов внутри интерфейса.
          </p>
        </div>
        <button class="task__more" aria-label="Открыть меню">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M10 10.8333C10.4603 10.8333 10.8334 10.4602 10.8334 10C10.8334 9.53977 10.4603 9.16667 10 9.16667C9.53978 9.16667 9.16669 9.53977 9.16669 10C9.16669 10.4602 9.53978 10.8333 10 10.8333Z" stroke="#70809B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
            <path d="M15.8333 10.8333C16.2936 10.8333 16.6667 10.4602 16.6667 10C16.6667 9.53977 16.2936 9.16667 15.8333 9.16667C15.3731 9.16667 15 9.53977 15 10C15 10.4602 15.3731 10.8333 15.8333 10.8333Z" stroke="#70809B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
            <path d="M4.16665 10.8333C4.62688 10.8333 4.99998 10.4602 4.99998 10C4.99998 9.53977 4.62688 9.16667 4.16665 9.16667C3.70641 9.16667 3.33331 9.53977 3.33331 10C3.33331 10.4602 3.70641 10.8333 4.16665 10.8333Z" stroke="#70809B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
          </svg>
        </button>
      </div>
      <div class="task__tags">
        <div class="task__tag">
          Flexbox
        </div>
        <div class="task__tag">
          UI
        </div>
        <div class="task__tag">
          Dashboard
        </div>
      </div>
      <div class="task__bottom">
        <div class="task__users">
          <img src="img/1.png" alt="Аватарка пользователя" class="task__user">
          <img src="img/2.png" alt="Аватарка пользователя" class="task__user">
          <img src="img/3.png" alt="Аватарка пользователя" class="task__user">
        </div>
        <div class="task__date">До 18 мая</div>
        <button class="task__btn">Открыть задачу</button>
      </div>
    </article>
  </li>
  <li class="tasks__item">
    <article class="task">
      <div class="task__top">
        <div class="task__text">
          <h3 class="task__title">
            Обновить карточки профиля пользователей и систему достижений
          </h3>
          <p class="task__descr">
            Привести компоненты к единому стилю платформы, переработать spacing и улучшить композицию интерфейса.
          </p>
        </div>
        <button class="task__more" aria-label="Открыть меню">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M10 10.8333C10.4603 10.8333 10.8334 10.4602 10.8334 10C10.8334 9.53977 10.4603 9.16667 10 9.16667C9.53978 9.16667 9.16669 9.53977 9.16669 10C9.16669 10.4602 9.53978 10.8333 10 10.8333Z" stroke="#70809B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
            <path d="M15.8333 10.8333C16.2936 10.8333 16.6667 10.4602 16.6667 10C16.6667 9.53977 16.2936 9.16667 15.8333 9.16667C15.3731 9.16667 15 9.53977 15 10C15 10.4602 15.3731 10.8333 15.8333 10.8333Z" stroke="#70809B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
            <path d="M4.16665 10.8333C4.62688 10.8333 4.99998 10.4602 4.99998 10C4.99998 9.53977 4.62688 9.16667 4.16665 9.16667C3.70641 9.16667 3.33331 9.53977 3.33331 10C3.33331 10.4602 3.70641 10.8333 4.16665 10.8333Z" stroke="#70809B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
          </svg>
        </button>
      </div>
      <div class="task__tags">
        <div class="task__tag">
          Components
        </div>
        <div class="task__tag">
          Design System
        </div>
        <div class="task__tag">
          Profile
        </div>
        <div class="task__tag">
          UI Kit
        </div>
      </div>
      <div class="task__bottom">
        <div class="task__users">
          <img src="img/4.png" alt="Аватарка пользователя" class="task__user">
          <img src="img/5.png" alt="Аватарка пользователя" class="task__user">
        </div>
        <div class="task__date">До 18 мая</div>
        <button class="task__btn">Открыть задачу</button>
      </div>
    </article>
  </li>
</ul>

Добавляем стили

Подключим файл стилей через <link> и зададим несколько базовых правил. Для этого задания достаточно переопределить box-sizing, сбросить отступ у <body> и подключить шрифт Inter, который используется в макете.

* {
  box-sizing: border-box;
}

@font-face {
  font-family: "Inter";
  src: url("../fonts/InterRegular.woff2");
  font-display: swap;
  font-weight: 400;
}

@font-face {
  font-family: "Inter";
  src: url("../fonts/InterSemiBold.woff2");
  font-display: swap;
  font-weight: 600;
}

@font-face {
  font-family: "Inter";
  src: url("../fonts/InterBold.woff2");
  font-display: swap;
  font-weight: 700;
}

body {
  margin: 0;
  padding: 20px;
  font-family: "Inter", sans-serif;
  background: #e6e4e4;
}

Далее оформляем список. Для этого отлично подойдет flexbox. Используем направление column, чтобы карточки располагались друг под другом, а расстояние между ними зададим через gap. Такой подход значительно удобнее, чем задавать внешние отступы каждой карточке по отдельности.

.tasks {
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  max-width: 800px;
  padding: 20px;
  list-style: none;
  gap: 32px;
}

Карточка представляет собой обычный блок со скруглением, внутренними отступами и светлым фоном. Но здесь есть один интересный момент. В макете возле каждой задачи расположен небольшой оранжевый кружок. Добавлять для него отдельный элемент в HTML совершенно не хочется — это чисто декоративная деталь. Поэтому воспользуемся псевдоэлементом ::before. Зададим карточке position: relative, а самому кружку — position: absolute.

.task {
  position: relative;
  border-radius: 30px;
  padding: 24px 24px 24px 60px;
  background: #fffdf9;
}

.task::before {
  content: "";
  position: absolute;
  left: 24px;
  top: 28px;
  border-radius: 100px;
  width: 16px;
  height: 16px;
  background: #f59e0b;
}

Теперь оформим верхнюю часть карточки. Здесь снова отлично подходит Flexbox. Используем justify-content: space-between, чтобы текст автоматически прижался к левому краю, а кнопка меню — к правому. Дополнительно зададим gap, чтобы элементы не прилипали друг к другу при уменьшении ширины карточки.

.task__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  margin-bottom: 20px;
  gap: 12px;
}

Кнопка с тремя точками — обычный <button>, для нее удобно использовать Flexbox даже несмотря на то, что внутри находится только одна SVG-иконка. Это позволяет буквально одной строчкой идеально выровнять содержимое по центру. Также добавим transition и небольшое изменение цвета при наведении — интерфейс сразу становится заметно приятнее. Также зададим максимальную ширину для текста, чтобы ограничить размер.

.task__text {
  max-width: 524px;
}

.task__more {
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 12px;
  width: 36px;
  height: 36px;
  cursor: pointer;
  background: #f2f5fa;
  transition: background 0.3s;
}

.task__more svg {
  display: block;
  width: 20px;
  height: 20px;
}

.task__more:hover {
  background: #e2e5e9;
}

Далее текст и теги. Для текста просто берем стили из макета. А вот количество тегов заранее неизвестно. Сейчас их три, а завтра можно сделать шесть. Поэтому лучше использовать здесь flex-wrap: wrap, тогда элементы перенесутся на новую строчку, если их будет много.

.task__title {
  margin: 0;
  margin-bottom: 8px;
  font-weight: 700;
  font-size: 20px;
  line-height: 120%;
  color: #162033;
}

.task__descr {
  margin: 0;
  font-weight: 400;
  font-size: 16px;
  line-height: 140%;
  color: #70809b;
}

.task__tags {
  display: flex;
  flex-wrap: wrap;
  max-width: 524px;
  gap: 4px;
}

.task__tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 100px;
  padding: 8px 12px;
  font-weight: 700;
  font-size: 12px;
  line-height: 120%;
  color: #2563eb;
  background: #edf4ff;
}

Осталась последняя часть — нижняя. здесь расположены три блока — участники, срок и кнопка открытия. Первые два элемента остаются рядом друг с другом, а кнопку надо переместить к правому краю. Для этого можно задать margin-left: auto, тогда флекс-элемент отступит на все сводобное пространство. Ну, а стили кнопки — простые, и здесь я от себя добавлю transition и :hover, и вам советую на будущее всегда так делать, даже если дизайнер не позаботился об этом.

Участники делаются интересно. Здесь я бы рекомендовал достать из макета изображения без обводки, отключив ее в свойствах элемента в Figma, чтобы сделать универсальные изображения. Тогда мы сможем задать обводку сами. Но, здесь еще изображения налезают друг на друга, и здесь мы можем использовать отрицательный margin. В этом нет ничего плохого, это нормальная практика.

.task__bottom {
  display: flex;
  align-items: center;
  margin-top: 20px;
  gap: 16px;
}

.task__users {
  display: flex;
  align-items: center;
}

.task__user {
  display: block;
  border: 3px solid #fff;
  border-radius: 100px;
  width: 42px;
  height: 42px;
  object-fit: cover;
}

.task__user:not(:first-child) {
  margin-left: -8px;
}

.task__date {
  font-weight: 600;
  font-size: 16px;
  line-height: 120%;
  color: #70809b;
}

.task__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: auto;
  border: none;
  border-radius: 12px;
  padding: 12px;
  font-family: inherit;
  font-weight: 700;
  font-size: 16px;
  line-height: 120%;
  color: #fff;
  background: #2563eb;
  cursor: pointer;
  transition: background 0.3s;
}

.task__btn:hover {
  background: #537fe1;
}

Обратите внимание, как именно мы задали отрицательный отступ. Использовали запись :not(:first-child), что буквально переводится как не первый элемент. Таким образом, мы всегда задаем отступ второму, третьему и так далее элементам, а первый остается на месте. Полезная техника для подобных случаев.

Теперь вы умеете создавать современные карточки задач с заголовком, описанием, тегами, участниками, сроком выполнения и кнопками управления. Заодно мы разобрали множество полезных приемов CSS: Flexbox, flex-wrap, псевдоэлементы, отрицательные margin, margin-left: auto, object-fit и создание переиспользуемых интерфейсных компонентов.

Решение задачи

предыдущий пост следующий пост