Как сверстать список задач
Эта статья основана на одном из практических заданий 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 и создание переиспользуемых интерфейсных компонентов.