Как сверстать настройки профиля
Эта статья основана на одном из практических заданий verstaem.online.
Вы можете читать материал как обычное руководство или сначала попробовать выполнить задание самостоятельно, а затем сравнить свое решение с подробным разбором.
Перейти к заданиюВ этой статье мы сверстаем страницу настроек профиля пользователя. На ее примере разберем, как организовывать большие формы, создавать независимые секции интерфейса, работать с кастомными чекбоксами и выпадающими списками, а также подключим библиотеку Choices.js для оформления стандартного элемента <select>.
Создаем 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>
Сразу после этого подключим библиотеку Choices.js, собственный файл стилей и JavaScript. Атрибут defer позволит выполнить скрипт только после того, как браузер полностью построит HTML-документ.
<link rel="stylesheet" href="libs/choices.min.css" />
<link rel="stylesheet" href="css/style.css" />
<script src="libs/choices.min.js" defer></script>
<script src="js/script.js" defer></script>
Теперь можно переходить к самому интерфейсу. Сделаем блок с классом content, в котором будет все содержимое компонента.
Верхняя часть страницы состоит из заголовка, небольшого описания и кнопки сохранения. Здесь все достаточно просто: создаем контейнер, внутри которого будет текстовая часть и кнопка.
Следом разместим навигацию между разделами страницы. Несмотря на то что внешне это обычные кнопки, логически они являются навигацией, поэтому используем элемент <nav>. Внутри него расположим обычный маркированный список со ссылками.
<div class="content__top">
<div class="content__text">
<h1 class="content__title">Настройки профиля</h1>
<p class="content__descr">
Управление аккаунтом и персональными параметрами.
</p>
</div>
<button class="content__btn content__btn--save">Сохранить</button>
</div>
<nav class="content__nav">
<ul class="content__nav-list">
<li class="content__nav-item">
<a href="#profile" class="content__nav-link content__nav-link--active">Профиль</a>
</li>
<li class="content__nav-item">
<a href="#safety" class="content__nav-link">Безопасность</a>
</li>
<li class="content__nav-item">
<a href="#notifications" class="content__nav-link">Уведомления</a>
</li>
<li class="content__nav-item">
<a href="#delete" class="content__nav-link">Удаление</a>
</li>
</ul>
</nav>
Создаем форму
Основную часть страницы занимает форма настроек. Все поля относятся к одному процессу редактирования профиля, поэтому вместо нескольких отдельных форм используем один элемент <form>.
Внутри формы разместим несколько независимых блоков:
- личные данные;
- безопасность;
- уведомления;
- удаление аккаунта.
Каждый блок содержит собственный заголовок и набор элементов управления.
<form class="content__form">
<div class="content__block" id="profile">
<h2 class="content__subtitle">
Личные данные
</h2>
<div class="content__items">
<label class="content__label">
<span class="content__caption">Имя</span>
<input
type="text"
name="name"
autocomplete="name"
placeholder="Иван"
class="content__input"
required
/>
</label>
<label class="content__label">
<span class="content__caption">Фамилия</span>
<input
type="text"
name="surname"
autocomplete="surname"
placeholder="Петров"
class="content__input"
required
/>
</label>
<label class="content__label">
<span class="content__caption">E-mail</span>
<input
type="email"
name="email"
autocomplete="email"
placeholder="mail@example.com"
class="content__input"
required
/>
</label>
<label class="content__label">
<span class="content__caption">Страна</span>
<select name="country" class="content__select">
<option value="russia">Россия</option>
<option value="england">Великобритания</option>
<option value="sudan">Судан</option>
</select>
</label>
<label class="content__label content__label--full">
<span class="content__caption">О себе</span>
<textarea name="about" class="content__input content__input--textarea" placeholder="Опишите себя" spellcheck="true"></textarea>
</label>
</div>
</div>
<div class="content__block" id="safety">
<h2 class="content__subtitle">
Безопасность
</h2>
<div class="content__items">
<label class="content__label">
<span class="content__caption">Новый пароль</span>
<input
type="password"
name="password"
autocomplete="password"
class="content__input"
required
/>
</label>
<label class="content__label">
<span class="content__caption">Повторите пароль</span>
<input
type="password"
name="confirm_password"
autocomplete="password"
class="content__input"
required
/>
</label>
</div>
</div>
<div class="content__block" id="notifications">
<h2 class="content__subtitle">
Уведомления
</h2>
<div class="content__checkboxes">
<label class="custom-checkbox">
<input type="checkbox" name="email_notifications" class="custom-checkbox__input" />
<span class="custom-checkbox__text"
>Email-уведомления</span
>
</label>
<label class="custom-checkbox">
<input type="checkbox" name="push_notifications" class="custom-checkbox__input" />
<span class="custom-checkbox__text"
>Push-уведомления</span
>
</label>
<label class="custom-checkbox">
<input type="checkbox" name="all_events" class="custom-checkbox__input" />
<span class="custom-checkbox__text"
>Все события</span
>
</label>
<label class="custom-checkbox">
<input type="checkbox" name="only_important" class="custom-checkbox__input" />
<span class="custom-checkbox__text"
>Только важные</span
>
</label>
</div>
</div>
<div class="content__block content__block--delete" id="delete">
<h2 class="content__subtitle">
Удаление аккаунта
</h2>
<div class="content__txt">
<div class="content__subdescr">
Удаление аккаунта без возможности восстановления.
</div>
<button class="content__btn content__btn--delete">Удалить аккаунт</button>
</div>
</div>
<div class="content__bottom">
<button class="content__btn content__btn--cancel">Отмена</button>
<button class="content__btn content__btn--save">Сохранить изменения</button>
</div>
</form>
Обратите внимание, что для каждого поля используется элемент <label>. Благодаря этому пользователь сможет нажать на подпись и сразу получить фокус в соответствующем поле ввода.
Также используем подходящие элементы формы:
<input>— для коротких текстовых данных;<textarea>— для описания пользователя;<select>— для выбора страны;<input type="checkbox">— для настроек уведомлений.
И еще важный момент — это атрибуты id и их связь с меню. Я сделал для каждого раздела свой id, а в меню указал эти id внутри href ссылок, тем самым реализовав меню компонента. Также, в форме использован атрибут spellcheck, который включает проверку правописания.
Добавляем стили
Подключим файл стилей через <link> и зададим несколько базовых правил. Для этого задания достаточно переопределить box-sizing, сбросить отступ у <body> и подключить шрифт Segoe UI, который используется в макете.
* {
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;
padding: 20px;
background: #f4f6fb;
font-family: "Segoe UI", sans-serif;
}
Обратите внимание на подключение шрифтов. Мы используем одинаковое значение font-family, но разные значения font-weight. Благодаря этому браузер автоматически будет выбирать нужное начертание при использовании обычного или жирного текста.
Далее оформим основной контент страницы. Он представляет собой вертикальный Flexbox с одинаковыми промежутками между секциями. Такой вариант отлично подходит для подобных интерфейсов, поскольку все карточки располагаются одна под другой.
.content {
display: flex;
flex-direction: column;
gap: 24px;
margin: 0 auto;
max-width: 940px;
}
Верхний блок также построен на Flexbox. Он позволяет без дополнительных приемов расположить заголовок слева, а кнопку сохранения — справа. Для меню также будем использовать Flexbox. И также отдельно хочу упомянуть кнопки. Кнопок на странице 3 вида, и все эти виды мы описываем сразу — с помощью классов-модификаторов.
.content__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.content__text {
max-width: 520px;
}
.content__title {
margin: 0;
margin-bottom: 4px;
font-weight: 700;
font-size: 32px;
line-height: 120%;
color: #1f2937;
}
.content__descr {
margin: 0;
font-weight: 400;
font-size: 16px;
line-height: 120%;
color: #6b7280;
}
.content__btn {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 12px;
padding: 12px 16px;
font-family: inherit;
font-weight: 700;
font-size: 16px;
line-height: 120%;
color: #fff;
cursor: pointer;
transition: background 0.3s;
}
.content__btn--save {
background: #4f6ef7;
}
.content__btn--save:hover {
background: #425bcb;
}
.content__btn--delete {
background: #ff5c66;
}
.content__btn--delete:hover {
background: #c4444c;
}
.content__btn--cancel {
color: #1f2937;
background: #e9edf6;
}
.content__btn--cancel:hover {
background: #b9bcc2;
}
.content__nav-list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
align-items: center;
gap: 12px;
}
.content__nav-link {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 100px;
padding: 12px 16px;
font-weight: 400;
font-size: 16px;
line-height: 120%;
color: #1f2937;
background: #fff;
text-decoration: none;
transition: color 0.3s, background 0.3s;
}
.content__nav-link--active {
color: #fff;
background: #1f2937;
pointer-events: none;
}
.content__nav-link:hover {
color: #fff;
background: #1f2937;
}
Не забывайте указывать состояние наведения для кнопок\ссылок. Это повышает удобство интерфейса, делает его живым.
Создаем карточки
Теперь оформим сами секции настроек. Все они имеют одинаковое оформление:
- белый фон;
- скругленные углы;
- внутренние отступы;
- тонкую рамку.
Поэтому имеет смысл создать один общий класс, который будет отвечать за внешний вид карточек.
.content__form {
display: flex;
flex-direction: column;
gap: 24px;
}
.content__block {
border: 1px solid #e7ebf3;
border-radius: 20px;
padding: 24px;
background: #fff;
}
.content__block--delete {
border: 1px solid #ffd8d8;
background: #fff7f7;
}
Отдельно обратите внимание на секцию удаления аккаунта. Она выглядит практически так же, как остальные карточки, но у нее используется собственный цвет рамки и фона. Поэтому здесь удобнее добавить модификатор класса, а не создавать полностью новые стили.
Размещаем поля формы
Практически все поля располагаются по два в строке. Для этого используем Flexbox с переносом элементов (flex-wrap). Каждое поле занимает половину ширины контейнера, а некоторые элементы, например описание пользователя, растягиваются сразу на всю строку.
.content__subtitle {
margin: 0;
margin-bottom: 20px;
font-weight: 700;
font-size: 20px;
line-height: 120%;
color: #1f2937;
}
.content__items {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.content__label {
display: flex;
flex-direction: column;
width: calc(50% - 8px);
gap: 8px;
}
.content__label--full {
width: 100%;
}
.content__caption {
font-weight: 400;
font-size: 12px;
line-height: 120%;
color: #6b7280;
}
.content__input {
border: 1px solid #d9e0ea;
border-radius: 16px;
padding: 0px 16px;
height: 54px;
font-family: inherit;
font-weight: 400;
font-size: 16px;
line-height: 120%;
color: #1f2937;
background: #fafbfe;
}
.content__input--textarea {
padding: 16px;
resize: none;
height: 90px;
}
.content__input::placeholder {
color: #1f2937;
}
Далее блок с чекбоками. Ему дадим flex и расставим элементы. А вот сам чекбокс — это интересно. По сути, управлять всем будет <input>, но мы скроем его, заменив на свое представление.
.content__addition {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 16px;
margin-bottom: 16px;
gap: 20px;
}
.content__checkboxes {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.custom-checkbox {
position: relative;
max-width: 460px;
cursor: pointer;
}
.custom-checkbox__input {
position: absolute;
opacity: 0;
appearance: none;
}
.custom-checkbox__text {
position: relative;
display: inline-flex;
align-items: center;
font-weight: 400;
font-size: 16px;
line-height: 120%;
color: #6b7280;
gap: 8px;
}
.custom-checkbox__text::before {
content: "";
display: block;
border: 1px solid #d9e0ea;
border-radius: 4px;
flex-shrink: 0;
width: 16px;
height: 16px;
}
.custom-checkbox__text::after {
content: "";
position: absolute;
left: 5px;
top: 6px;
border-radius: 2px;
width: 8px;
height: 8px;
background: #d9e0ea;
opacity: 0;
transition: opacity 0.3s;
}
.custom-checkbox__input:checked + .custom-checkbox__text::after {
opacity: 1;
}
.custom-checkbox__input:focus + .custom-checkbox__text {
outline: 1px solid #6b7280;
outline-offset: 3px;
border-radius: 2px;
}
Давайте разберем, что же тут сделано. Ну, во-первых, мы даем cursor: pointer для лейбла, делая его как бы кликабельным. Затем явно скрываем инпут с помощью opacity и appearance. Оно как раз и нужно, чтобы сбросить внешний вид элемента.
Далее задаем стили тексту, в том числе flexbox. И вот тут начинается магия — мы делаем псевдоэлемент ::before для текста, и рисуем с его помощью чекбокс — по сути, просто квадрат с обводкой. А с помощью ::after рисуем квадратик, который будет внутри, то есть будет обозначать, отмечен чекбокс или нет. Этот квадратик мы по умолчанию скроем с помощью opacity.
И теперь, как я и говорил, всем будет управлять скрытый <input>. Важно не скрывать его с помощью display: none, иначе он не будет работать.
.custom-checkbox__input:checked + .custom-checkbox__text::after {
opacity: 1;
}
Здесь мы явно говорим «если чекбокс отмечен, найди следующий элемент (через +), и у него псевдоэлементу ::after задай opacity: 1„. Мы используем фишку с комбинаторными селекторами, и находим следующий за инпутом элемент через селектор „+“. Как вы понимаете, если бы структура html была другой, это бы просто не сработало. Имейте это ввиду.
Стилизуем селект
Стандартный элемент <select> крайне ограничен в плане оформления. Именно поэтому во многих проектах используются специальные библиотеки.
В этом задании мы подключим Choices.js. Для начала достаточно создать экземпляр библиотеки и передать в него нужный <select>.
const element = document.querySelector('.content__select');
const choices = new Choices(element, {
searchEnabled: false,
itemSelectText: '',
});
В этом коде мы выполняем инициализацию библиотеки, после этого обычный селект скроется, и появится тот, который сделала библиотека. Его внешний вид, конечно, совсем не тот, что нам нужен.
Но как менять стили? Самое важное правило — менять стили нужно, используя точно такие же селекторы стилей, как и у библиотеки. Их можно увидеть в devtools. Итак, нам нужно поменять основные стили селекта, сделать их похожими на инпут.
.choices[data-type="select-one"] .choices__inner {
display: flex;
align-items: center;
border: 1px solid #d9e0ea;
border-radius: 16px;
padding: 0px 16px;
height: 54px;
font-family: inherit;
font-weight: 400;
font-size: 16px;
line-height: 120%;
color: #1f2937;
background: #fafbfe;
}
Селектор немаленький, сложный, но именно такой, как в devtools. Только так мы можем быть уверены, что наши стили «перебьют» стили библиотеки. Иначе не хватит веса селектора.
Далее добавим отступы для dropdown-части, а также обозначим внешний вид при состоянии фокуса. После этого скроем стрелочку по умолчанию (она сделана через border, поэтому достаточно указать border: none), и покажем свою, свгшную. Чтобы сделать абсолютный элемент по центру по вертикали — указываем top: 50% и translate: 0 -50%. Так мы сдвигаем элемент вниз на 50%, а затем возращаем назад на 50% своей высоты, так элемент и встает по центру.
.choices__list {
padding: 8px 0;
}
.choices[data-type*=select-one]:focus-visible {
border-radius: 16px;
outline: 1px solid #000;
}
.choices[data-type*=select-one]::after {
right: 16px;
top: 50%;
margin: 0;
border: none;
width: 16px;
height: 16px;
background-image: url("../img/angle.svg");
background-position: center;
background-size: cover;
background-repeat: no-repeat;
translate: 0 -50%;
}
Далее через класс is-open сделаем разворот стрелочки, а также сбросим margin, которые библиотека добавляет. Затем, добавим стили для дропдаун части — фон, обводку, отступ от селекта из макета.
.choices[data-type*=select-one].is-open::after {
margin: 0;
rotate: 180deg;
}
.choices__list[aria-expanded] {
z-index: 2;
margin-top: 4px;
border: 1px solid #d9e0ea !important;
border-radius: 16px;
background: #fafbfe;
}
Библиотека рассчитана так, что выпадающая часть может открыться не только вниз, но и вверх, если высоты браузера не хватает. Это управляется классом is-flipped, поэтому мы должны добавить стили и для него.
.is-flipped .choices__list--dropdown,
.is-flipped .choices__list[aria-expanded] {
margin-bottom: 4px;
border-radius: 16px;
}
Теперь можем стилизовать элементы дропдауна, и сразу же — состояние и при наведении, и при активности.
.choices__list[aria-expanded] .choices__item {
padding: 8px 16px;
font-size: 16px;
color: #1f2937;
}
.choices__list--dropdown .choices__item--selectable.is-highlighted, .choices__list--dropdown .choices__item--selectable.is-selected, .choices__list[aria-expanded] .choices__item--selectable.is-highlighted, .choices__list[aria-expanded] .choices__item--selectable.is-selected {
color: #fff;
background: #4f6ef7;
}
После выполнения задания у вас получится полноценная страница настроек профиля с несколькими разделами, адаптивной сеткой и аккуратно оформленными элементами управления. Эти приемы регулярно используются в личных кабинетах, административных панелях и различных веб-приложениях.