Как сверстать настройки профиля

Эта статья основана на одном из практических заданий 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;
}

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

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

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