Как сверстать рабочую область приложения
Эта статья основана на одном из практических заданий verstaem.online.
Вы можете читать материал как обычное руководство или сначала попробовать выполнить задание самостоятельно, а затем сравнить свое решение с подробным разбором.
Перейти к заданиюВ этом уроке мы создадим рабочую область веб-приложения с несколькими независимыми блоками. Сверстаем карточку проекта, статистику, календарь, список файлов, канбан-доску и блок активности, используя CSS Grid и 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>
Итак, давайте разберемся с базовой структурой. Создадим общий див, в него вложим контейнер, который будет ровнять все по центру. А внутри два блока — верхний и нижний. Верхний — своего рода шапка, а нижний — просто набор блоков внутри.
<div class="content">
<div class="container">
<div class="content__top"></div>
<div class="content__inner grid"></div>
</div>
</div>
Здесь мы сразу задаем сетку для нижнего блока, т. к. внутри него будет основной контент. А для верхушки грид не нужен, там достаточно и флекса.
Итак, наверху располагаем логотип, поле поиска и блок с кнопками.
<div class="content__top">
<div class="content__logo">Aurora</div>
<input
type="text"
class="content__input"
placeholder="Поиск проектов..."
/>
<div class="content__btns">
<button class="content__btn" aria-label="Уведомления">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_31001_11)">
<path
d="M3.38996 15H16.4366C16.8382 15 17.2338 14.9033 17.59 14.7181C17.9463 14.5328 18.2527 14.2645 18.4833 13.9358C18.7139 13.6071 18.8619 13.2277 18.9149 12.8297C18.9678 12.4316 18.9241 12.0267 18.7875 11.6492L16.4108 5.07833C16.0049 3.61809 15.1319 2.33099 13.9252 1.41389C12.7186 0.496791 11.2447 0.000176732 9.72913 8.47759e-10C8.1514 -2.46602e-05 6.62083 0.53799 5.39013 1.52521C4.15944 2.51244 3.3022 3.88984 2.95996 5.43L1.00162 11.7633C0.886325 12.1372 0.860404 12.533 0.925949 12.9188C0.991493 13.3045 1.14667 13.6695 1.379 13.9844C1.61133 14.2992 1.91432 14.5551 2.26359 14.7315C2.61287 14.9079 2.99867 14.9999 3.38996 15Z"
/>
<path
d="M5.91663 16.6667C6.10789 17.6086 6.6189 18.4554 7.36308 19.0637C8.10725 19.6719 9.03883 20.0042 9.99996 20.0042C10.9611 20.0042 11.8927 19.6719 12.6368 19.0637C13.381 18.4554 13.892 17.6086 14.0833 16.6667H5.91663Z"
/>
</g>
<defs>
<clipPath id="clip0_31001_11">
<rect width="20" height="20" />
</clipPath>
</defs>
</svg>
</button>
<button class="content__btn" aria-label="Пользователь">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_31001_19)">
<path
d="M10 10C12.7614 10 15 7.76142 15 5C15 2.23858 12.7614 0 10 0C7.23858 0 5 2.23858 5 5C5 7.76142 7.23858 10 10 10Z"
/>
<path
d="M10 11.6667C5.85977 11.6713 2.50461 15.0265 2.5 19.1667C2.5 19.6269 2.87309 20 3.33332 20H16.6666C17.1269 20 17.5 19.6269 17.5 19.1667C17.4954 15.0265 14.1402 11.6713 10 11.6667Z"
/>
</g>
<defs>
<clipPath id="clip0_31001_19">
<rect width="20" height="20" />
</clipPath>
</defs>
</svg>
</button>
</div>
А вот внутри content__inner будем располагать блоки с контентом. Прежде всего обращу ваше внимание, что все блоки внутри — одинаковые по сути. У них один фон, одни отступы, одна тень. Значит, можно реализовать их как один элемент, но дополнительно раздать еще один css-класс, чтобы достилизовать то, что возможно потребуется.
В первом блоке мы сделаем бейджик спринта, заголовок, а также изображение. Текст лучше обернуть в отдельный див, чтобы можно было использовать flexbox.
<div class="content__block content__main">
<div class="content__text">
<div class="content__sprint">Sprint 24</div>
<h1 class="content__title">Рабочая область</h1>
<p class="content__descr">
Большой hero-блок занимает отдельную область сетки. Внутри него
ещё один Grid с текстом и превью.
</p>
</div>
<img
src="img/content-image.png"
aria-hidden="true"
alt=""
class="content__pic"
В данном случае, класс content__block будет отвечать за общие стили для каждого блока.
Далее у нас блок статистики. Внутри — просто набор дивов с данными.
/>
</div>
<div class="content__block content__statistics">
<div class="content__statistics-item">
<div class="content__statistics-val">148</div>
<div class="content__statistics__caption">Коммитов</div>
</div>
<div class="content__statistics-item">
<div class="content__statistics-val">18</div>
<div class="content__statistics__caption">PR</div>
</div>
<div class="content__statistics-item">
<div class="content__statistics-val">12</div>
<div class="content__statistics__caption">Файлов</div>
</div>
<div class="content__statistics-item">
<div class="content__statistics-val">6</div>
<div class="content__statistics__caption">Релизов</div>
</div>
</div>
<div class="content__block content__calendar">
<h3 class="content__subtitle">Календарь</h3>
<ul class="content__calendar-list">
<li class="content__calendar-item">1</li>
<li class="content__calendar-item">2</li>
<li class="content__calendar-item">3</li>
<li class="content__calendar-item content__calendar-item--active">
4
</li>
<li class="content__calendar-item">5</li>
<li class="content__calendar-item">6</li>
<li class="content__calendar-item">7</li>
<li class="content__calendar-item">8</li>
<li class="content__calendar-item">9</li>
Следующий — календарь. Здесь давайте используем список. Список из 28 элементов.
<li class="content__calendar-item">10</li>
<li class="content__calendar-item">11</li>
<li class="content__calendar-item">12</li>
<li class="content__calendar-item">13</li>
<li class="content__calendar-item">14</li>
<li class="content__calendar-item">15</li>
<li class="content__calendar-item">16</li>
<li class="content__calendar-item">17</li>
<li class="content__calendar-item">18</li>
<li class="content__calendar-item">19</li>
<li class="content__calendar-item">20</li>
<li class="content__calendar-item">21</li>
<li class="content__calendar-item">22</li>
<li class="content__calendar-item">23</li>
<li class="content__calendar-item">24</li>
<li class="content__calendar-item">25</li>
<li class="content__calendar-item">26</li>
<li class="content__calendar-item">27</li>
<li class="content__calendar-item">28</li>
</ul>
</div>
<div class="content__block content__files">
<h3 class="content__subtitle">Файлы</h3>
<ul class="content__list">
<li class="content__item">
<a href="#" class="content__link"> 📄 UI Kit.fig </a>
</li>
<li class="content__item">
<a href="#" class="content__link"> 📦 Assets.zip </a>
</li>
<li class="content__item">
<a href="#" class="content__link"> 📑 Roadmap.pdf </a>
</li>
<li class="content__item">
<a href="#" class="content__link"> 🖼 Hero.psd </a>
</li>
</ul>
</div>
<div class="content__block content__kanban">
<div class="content__kanban-col">
<h3 class="content__subtitle">Бэклог</h3>
<ul class="content__list">
<li class="content__item">Главная</li>
<li class="content__item">Каталог</li>
<li class="content__item">SEO</li>
</ul>
</div>
<div class="content__kanban-col">
<h3 class="content__subtitle">В работе</h3>
<ul class="content__list">
<li class="content__item">Dashboard</li>
<li class="content__item">Grid</li>
<li class="content__item">Profile</li>
</ul>
</div>
<div class="content__kanban-col">
<h3 class="content__subtitle">Сделано</h3>
<ul class="content__list">
<li class="content__item">Login</li>
<li class="content__item">API</li>
<li class="content__item">Docs</li>
</ul>
</div>
</div>
<div class="content__block content__activity">
<h3 class="content__subtitle">Активность</h3>
<ul class="content__list">
<li class="content__item">✅ Merge</li>
<li class="content__item">💬 Комментарий</li>
<li class="content__item">🎉 Release</li>
</ul>
</div>
</div>
</div>
</div>
</body>
</html>
Далее блок с файлами. Сразу скажу, что эмоджи, которые вы берете из макета, скорее всего иначе отобразятся в браузере. Ничего страшного. Ну, а по сути это просто список со ссылками.
<div class="content__block content__files">
<h3 class="content__subtitle">Файлы</h3>
<ul class="content__list">
<li class="content__item">
<a href="#" class="content__link"> 📄 UI Kit.fig </a>
</li>
<li class="content__item">
<a href="#" class="content__link"> 📦 Assets.zip </a>
</li>
<li class="content__item">
<a href="#" class="content__link"> 📑 Roadmap.pdf </a>
</li>
<li class="content__item">
<a href="#" class="content__link"> 🖼 Hero.psd </a>
</li>
</ul>
</div>
Далее канбан. Здесь мы функционал не делаем, поэтому просто покажем это как три списка.
<div class="content__block content__kanban">
<div class="content__kanban-col">
<h3 class="content__subtitle">Бэклог</h3>
<ul class="content__list">
<li class="content__item">Главная</li>
<li class="content__item">Каталог</li>
<li class="content__item">SEO</li>
</ul>
</div>
<div class="content__kanban-col">
<h3 class="content__subtitle">В работе</h3>
<ul class="content__list">
<li class="content__item">Dashboard</li>
<li class="content__item">Grid</li>
<li class="content__item">Profile</li>
</ul>
</div>
<div class="content__kanban-col">
<h3 class="content__subtitle">Сделано</h3>
<ul class="content__list">
<li class="content__item">Login</li>
<li class="content__item">API</li>
<li class="content__item">Docs</li>
</ul>
</div>
</div>
И последний блок — активность. Здесь также просто список.
<div class="content__block content__activity">
<h3 class="content__subtitle">Активность</h3>
<ul class="content__list">
<li class="content__item">✅ Merge</li>
<li class="content__item">💬 Комментарий</li>
<li class="content__item">🎉 Release</li>
</ul>
</div>
Относительно простая разметка. Давайте переходить к CSS.
Добавляем стили
Подключим файл стилей через <link> и зададим несколько базовых правил. Для этого задания достаточно переопределить box-sizing, сбросить отступ у <body> и подключить шрифт Inter, который используется в макете.
* {
box-sizing: border-box;
}
@font-face {
font-family: "Segoe UI";
src: url("../fonts/SegoeUI.woff2");
font-display: swap;
font-weight: 400;
}
@font-face {
font-family: "Segoe UI";
src: url("../fonts/SegoeUIBold.woff2");
font-display: swap;
font-weight: 700;
}
body {
margin: 0;
background: #f2f5f8;
font-family: "Segoe UI", sans-serif;
}
.container {
margin: 0 auto;
padding: 0 20px;
max-width: 1920px;
}
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 20px;
}
Здесь мы, помимо базовых стилей, сразу задаем контейнер и сетку. Обратите внимание на контейнер. Его ширина — 1920, по ширине макета, но также он имеет паддинги, чтобы совпадать с задумкой. Грид же устроен просто — мы делаем 12 одинаковых колонок с отступами по 20 пикселей, прямо как в нашем макете.
Переходим к основной части. Зададим отступы у всего контента, а затем сделаем стилизацию верхней части. Здесь нам нужен будет flexbox.
.content {
padding: 20px 0;
}
.content__top {
display: flex;
align-items: center;
border-radius: 20px;
padding: 12px 20px;
box-shadow: 0 15px 50px 0 rgba(0, 0, 0, 0.08);
background: #fff;
}
.content__logo {
font-weight: 700;
font-size: 24px;
line-height: 120%;
text-transform: uppercase;
color: #ff7b54;
}
.content__input {
margin-left: 216px;
padding: 0 12px;
padding-left: 32px;
border: none;
width: 300px;
height: 40px;
font-family: inherit;
font-weight: 400;
font-size: 16px;
line-height: 120%;
color: #24303d;
background-image: url("../img/search.svg");
background-position: left center;
background-size: 20px 20px;
background-repeat: no-repeat;
}
.content__btns {
display: flex;
align-items: center;
margin-left: auto;
gap: 12px;
}
.content__btn {
border: none;
padding: 0;
width: 20px;
height: 20px;
color: #24303D;
background: transparent;
}
.content__btn svg {
display: block;
width: 100%;
height: 100%;
fill: currentColor;
}
Здесь мы сделали иконку поиска прямо у инпута, с помощью background-image. Не забудьте передать background-repeat: no-repeat, иначе иконка будет повторяться постоянно. Также важный момент — это margin-left: auto, который «толкает» блок с кнопками в самый правый край. Эта запись будет работать только внутри флекс-контейнера, имейте ввиду.
Идем дальше, и теперь нам стоит задать общие стили для всех блоков внутри content__inner, и ему самому. Ему даем отступ, а блокам — обводку, фон, тень и т. д.
.content__inner {
margin-top: 20px;
}
.content__block {
position: relative;
border-radius: 20px;
padding: 20px;
box-shadow: 0 15px 50px 0 rgba(0, 0, 0, 0.08);
background: #fff;
}
Теперь можем заниматься каждым блоком в отдельности. Дополнительный класс нужен нам как минимум, чтобы задать количество колонок, которые будет занимать каждый блок. Но также в некоторые можно передать и дополнительные стили. Например, в первом нам явно нужен будет flex, чтобы текст и изображение встали в строку.
.content__main {
display: flex;
justify-content: space-between;
gap: 20px;
grid-column: 5 span;
}
.content__pic {
width: 311px;
}
.content__text {
max-width: 277px;
}
.content__sprint {
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
border-radius: 100px;
padding: 4px 8px;
font-weight: 400;
font-size: 12px;
line-height: 120%;
color: #fff;
background: #ff7b54;
}
.content__title {
margin: 0;
margin-bottom: 20px;
font-weight: 700;
font-size: 24px;
line-height: 120%;
color: #24303d;
}
.content__descr {
margin: 0;
font-weight: 400;
font-size: 16px;
line-height: 120%;
color: #24303d;
}
Далее делаем блок статистики, и тут нам пригодится внутренний грид. Делаем его как две колонки. И конечно, задаем самому блоку количество колонок, которое он займет — 3.
.content__statistics {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
grid-column: 3 span;
}
.content__statistics-item {
border-radius: 16px;
padding: 12px;
min-height: 98px;
background: #f6f9fc;
}
.content__statistics-val {
margin-bottom: 4px;
font-weight: 700;
font-size: 16px;
line-height: 120%;
color: #24303d;
}
.content__statistics-caption {
font-weight: 400;
font-size: 16px;
line-height: 120%;
color: #24303d;
}
Далее календарь. И здесь также очень удобен будет грид, но уже на 7 колонок. Также добавим класс активности, чтобы выделить одно из чисел как «текущее».
.content__calendar {
grid-column: 4 span;
}
.content__subtitle {
margin: 0;
margin-bottom: 20px;
font-weight: 700;
font-size: 20px;
line-height: 120%;
color: #24303d;
}
.content__calendar-list {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
}
.content__calendar-item {
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
padding: 8px;
font-weight: 400;
font-size: 16px;
line-height: 120%;
color: #24303d;
background: #f4f7fb;
}
.content__calendar-item--active {
color: #fff;
background: #24303d;
}
Далее файлы. Здесь мы сделаем стили наперед, т. к. список у файлов, у канбана и у активности по факту идентичный. Не забудьте, что для ссылок файлов нужно обязательно сбросить подчеркивание, а также задать цвет текста такой же, как у родителя (inherit).
.content__files {
grid-column: 3 span;
}
.content__list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.content__item {
border-radius: 16px;
padding: 12px;
background: #f6f9fc;
}
.content__link {
color: inherit;
text-decoration: none;
}
Поскольку стили для списка мы уже сделали, канбан делается просто — мы всего лишь сделаем здесь внутренний грид, чтобы колонки встали как на макете. А активности вообще зададим только grid-column, остальное встанет само.
.content__kanban {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
grid-column: 6 span;
}
.content__activity {
grid-column: 3 span;
}
Верстка десктопа закончена, но нас еще ожидает адаптив.
Делаем адаптив
Итак, для адаптива я всегда придерживаюсь одного подхода, и он всегда работает. Подход можно описать как «Не трогаем, если не нужно. Если нужно — возвращаемся назад к ближайшему безопасному брейкпоинту и меняем верстку». Это значит, что если, например, мы нашли косяк на ширину 1111 пикселей, надо вернуться к ближайшему брейкпоинту (а это 1200), и писать стили именно для него.
В данном случае верстка начинает ломаться в районе 1300, уже перестает помещаться текст в первом блоке. Поэтому мы используем медиа 1366, чтобы изменить количество колонок у всех блоков внутри.
@media (max-width: 1366px) {
.content__main {
grid-column: 7 span;
}
.content__statistics {
grid-column: 5 span;
}
.content__calendar {
grid-column: 6 span;
}
.content__files {
grid-column: 6 span;
}
.content__activity {
grid-column: 6 span;
}
}
Так, мы увеличиваем область для content__main, а календарь вообще переносим ниже, и также размещаем все что под первой строкой поровну.
Далее все в порядке вплоть до примерно 1000 пикселей. Снова возвращаемся назад, к 1024, и меняем стили. На самом деле, поскольку это рабочая область, то есть по факту какая-то внутрянка приложения, мы можем упростить адаптив донельзя, и сделать каждому блоку 12 колонок, то есть чтобы все блоки были друг под другом.
@media (max-width: 1024px) {
.content__main {
grid-column: 12 span;
}
.content__statistics {
grid-column: 12 span;
}
.content__calendar {
grid-column: 12 span;
}
.content__files {
grid-column: 12 span;
}
.content__kanban {
grid-column: 12 span;
}
.content__activity {
grid-column: 12 span;
}
}
Ну и на мобилке нам останется лишь подправить отступы сайта, сделать их по 16, а также немного подправить верхушку и блоки. У верхушки явно надо изменить инпут, сделать контент друг под другом и выровнять по центру. В блоке статистики и канбана нужно сделать каждый элемент друг под другом, и тут нам поможет запись grid-column: -1 / 1, которая обозначает то, что элемент начнет с начала грид-сетки, и закончит в конце. То есть, по сути, займет 100% ширины родителя.
@media (max-width: 768px) {
.container {
padding: 0 16px;
}
.content {
padding: 16px 0;
}
.content__top {
flex-direction: column;
padding: 20px;
gap: 16px;
}
.content__input {
margin: 0;
width: 240px;
height: 32px;
}
.content__btns {
margin-left: 0;
}
.content__main {
flex-direction: column-reverse;
gap: 16px;
}
.content__pic {
max-width: 288px;
}
.content__statistics-item {
grid-column: -1 / 1;
min-height: 66px;
}
.content__calendar-list {
grid-template-columns: repeat(3, 1fr);
}
.content__kanban-col {
grid-column: -1 / 1;
}
}
В результате мы собрали полноценную рабочую область приложения с несколькими независимыми секциями и адаптивной сеткой. Во время выполнения задания закрепили работу с CSS Grid и Flexbox, научились комбинировать разные варианты построения интерфейсов и создавать страницы, которые корректно выглядят как на компьютерах, так и на мобильных устройствах.