Как сверстать страницу оформления заказа

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

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

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

В этой статье мы создадим страницу оформления заказа интернет-магазина. На ее примере разберем, как верстать большие формы, организовывать интерфейс из нескольких независимых блоков, создавать кастомные элементы управления и переключать этапы оформления заказа с помощью JavaScript. Дополнительно подключим библиотеку Choices.js для оформления выпадающего списка и реализуем адаптивную версию страницы.

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

Как обычно, начнем с создания базовой HTML-разметки. Для этого достаточно ввести ! и нажать Tab. Не забудьте изменить язык документа на русский, поскольку весь интерфейс написан именно на русском языке.

<!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>

Поскольку практически весь интерфейс представляет собой оформление заказа, в качестве основного элемента будем использовать <form>. Рядом с формой находится информационная панель с итоговой стоимостью заказа. Она не относится к форме напрямую, поэтому логично разместить обе части внутри общей обертки.

Получаем следующую структуру:

<div class="content">
  <form class="content__form">

  </form>

  <aside class="content__aside">

  </aside>
</div>

Обратите внимание, что информационный блок реализован именно через <aside>. Он содержит дополнительную информацию, связанную с оформлением заказа, но не является частью самой формы.

Создаем переключатели этапов

В верхней части страницы находятся две вкладки: доставка и оплата. Именно они будут управлять отображением различных частей формы.

Поэтому сразу подготовим разметку таким образом, чтобы JavaScript мог легко определить, какую вкладку нажали. Для этого каждой кнопке добавим собственный атрибут data-tab.

<div class="content__btns">
  <button
    class="content__btn content__btn--active"
    type="button"
    data-tab="delivery">
    1. Доставка
  </button>

  <button
    class="content__btn"
    type="button"
    data-tab="payment">
    2. Оплата
  </button>
</div>

Здесь стоит обратить внимание сразу на несколько моментов. Во-первых, обе кнопки имеют type="button". Если этот атрибут не указать, браузер автоматически будет считать их кнопками отправки формы. Во-вторых, первая кнопка сразу получает модификатор content__btn--active, поскольку именно вкладка доставки должна открываться после загрузки страницы.

Создаем контейнеры вкладок

Далее создадим два контейнера, между которыми позже будет переключаться JavaScript. Каждый контейнер получает собственный атрибут data-tab-content.

<div
  class="content__inner content__inner--active"
  data-tab-content="delivery">

</div>

<div
  class="content__inner"
  data-tab-content="payment">

</div>

Первый контейнер также получает класс content__inner--active, благодаря которому он отображается сразу после открытия страницы. Все остальные вкладки изначально будут скрыты.

Создаем блок контактных данных

Первым внутри вкладки располагается блок контактной информации. Структура у него максимально простая.

<div class="content__block">
  <h2 class="content__subtitle">
    Контактные данные
  </h2>

  <div class="content__items">

  </div>
</div>

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

Добавляем поля формы

Теперь можно переходить непосредственно к полям ввода. Каждое поле состоит из трех элементов:

  • <label>;
  • подписи;
  • элемента формы.

Получается следующая структура.

<label class="content__label">
  <span class="content__caption">
    Имя
  </span>

  <input
    class="content__input"
    type="text"
    placeholder="Иван">
</label>

Итого, первый блок выходит таким:

<div class="content__block">
  <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>
      <input
        type="tel"
        name="phone"
        autocomplete="phone"
        placeholder="+7 (912) 893-35-23"
        class="content__input"
        required
      />
    </label>
  </div>
</div>

Во втором блоке, помимо инпутов, есть и пара других полей. Это <select> и <textarea>. Селект мы будем делать отдельно с помощью библиотеки, а для textarea добавим класс content__input--textarea, чтобы взять все что нужно от инпута, но достилизовать.

<div class="content__block">
  <h2 class="content__subtitle">Адрес доставки</h2>
  <div class="content__items">
    <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">
      <span class="content__caption">Город</span>
      <input
        type="text"
        name="city"
        autocomplete="city"
        placeholder="Москва"
        class="content__input"
        required
      />
    </label>
    <label class="content__label">
      <span class="content__caption">Улица</span>
      <input
        type="text"
        name="street"
        autocomplete="street"
        placeholder="Ленина"
        class="content__input"
        required
      />
    </label>
    <label class="content__label">
      <span class="content__caption">Индекс</span>
      <input
        type="number"
        name="postcode"
        autocomplete="postcode"
        placeholder="629612"
        class="content__input"
        required
      />
    </label>
    <label class="content__label content__label--full">
      <span class="content__caption">Комментарий</span>
      <textarea
        name="comment"
        class="content__input content__input--textarea"
        spellcheck="true"
      ></textarea>
    </label>
  </div>
</div>

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

<div class="content__block">
  <h2 class="content__subtitle">Способ доставки</h2>
  <div class="content__radios">
    <div class="content__radio">
      <label class="custom-radio">
        <input
          type="radio"
          name="delivery"
          value="Курьер"
          required
          checked
          class="custom-radio__input"
        />
        <span class="custom-radio__elem">Курьер</span>
      </label>
    </div>
    <div class="content__radio">
      <label class="custom-radio">
        <input
          type="radio"
          name="delivery"
          value="ПВЗ"
          required
          class="custom-radio__input"
        />
        <span class="custom-radio__elem">ПВЗ</span>
      </label>
    </div>
    <div class="content__radio">
      <label class="custom-radio">
        <input
          type="radio"
          name="delivery"
          value="Экспресс"
          required
          class="custom-radio__input"
        />
        <span class="custom-radio__elem">Экспресс</span>
      </label>
    </div>
  </div>
</div>

Обратите внимание, что всем полям задан атрибут name, без него данные просто не улетят на сервер, т. к. при передаче данных используется формат «ключ: значение». А если ключа (name) нет — значение не отправится. И также важно значение внутри value сделать на английском языке без пробелов. Именно такие значения «любит» сервер.

Создаем нижнюю панель первого этапа

После выбора способа доставки остается добавить кнопку навигации.

<div class="content__bottom">
  <button class="content__btn content__btn--forward" type="button" data-tab-target="payment">
    Перейти к оплате
    <svg
      width="20"
      height="20"
      viewBox="0 0 20 20"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        d="M15 10C14.996 9.5616 14.8193 9.14243 14.5084 8.83335L10.9334 5.25002C10.7772 5.09481 10.566 5.00769 10.3459 5.00769C10.1257 5.00769 9.91451 5.09481 9.75837 5.25002C9.68027 5.32749 9.61827 5.41965 9.57597 5.5212C9.53366 5.62275 9.51188 5.73167 9.51188 5.84168C9.51188 5.95169 9.53366 6.06062 9.57597 6.16216C9.61827 6.26371 9.68027 6.35588 9.75837 6.43335L12.5 9.16668H4.16671C3.94569 9.16668 3.73373 9.25448 3.57745 9.41076C3.42117 9.56704 3.33337 9.779 3.33337 10C3.33337 10.221 3.42117 10.433 3.57745 10.5893C3.73373 10.7456 3.94569 10.8334 4.16671 10.8334H12.5L9.75837 13.575C9.60146 13.7308 9.51286 13.9426 9.51208 14.1637C9.5113 14.3849 9.59839 14.5973 9.75421 14.7542C9.91002 14.9111 10.1218 14.9997 10.3429 15.0005C10.5641 15.0013 10.7765 14.9142 10.9334 14.7584L14.5084 11.175C14.8214 10.8639 14.9982 10.4413 15 10Z"
      />
    </svg>
  </button>
</div>

Здесь есть один интересный момент. Кнопка имеют type="button", поскольку она не должна отправлять форму. Ее задача — переключить пользователя на следующий этап оформления заказа. Помимо класса она получает атрибут data-tab-target="payment". Именно этот атрибут позже будет использовать JavaScript. Вместо того чтобы искать кнопку по классу или тексту, мы просто получим нужное значение из data-tab-target и откроем соответствующую вкладку.

Создаем второй этап оформления заказа

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

<div class="content__block">
  <h2 class="content__subtitle">Способ оплаты</h2>
  <div class="content__items">
    <div class="content__radios">
      <div class="content__radio">
        <label class="custom-radio">
          <input
            type="radio"
            name="payment"
            value="Банковская карта"
            required
            checked
            class="custom-radio__input"
          />
          <span class="custom-radio__elem">Банковская карта</span>
        </label>
      </div>
      <div class="content__radio">
        <label class="custom-radio">
          <input
            type="radio"
            name="payment"
            value="СБП"
            required
            class="custom-radio__input"
          />
          <span class="custom-radio__elem">СБП</span>
        </label>
      </div>
      <div class="content__radio">
        <label class="custom-radio">
          <input
            type="radio"
            name="payment"
            value="T-Pay"
            required
            class="custom-radio__input"
          />
          <span class="custom-radio__elem">T-Pay</span>
        </label>
      </div>
    </div>
    <label class="content__label">
      <span class="content__caption">Номер карты</span>
      <input
        type="number"
        name="card_number"
        autocomplete="card_number"
        class="content__input"
        required
      />
    </label>
    <label class="content__label">
      <span class="content__caption">Имя владельца</span>
      <input
        type="text"
        name="card_name"
        autocomplete="card_name"
        class="content__input"
        required
      />
    </label>
    <label class="content__label">
      <span class="content__caption">MM / YY</span>
      <input
        type="number"
        name="mm_yy"
        autocomplete="mm_yy"
        class="content__input"
        required
      />
    </label>
    <label class="content__label">
      <span class="content__caption">CVV</span>
      <input
        type="number"
        name="cvv"
        autocomplete="cvv"
        class="content__input"
        required
      />
    </label>
  </div>
</div>

На этот раз вместо способов доставки пользователь выбирает способ оплаты заказа. Здесь используется тот же подход — несколько radio-кнопок, объединенных одним именем. Благодаря одинаковому значению атрибута name браузер автоматически понимает, что одновременно может быть выбран только один вариант.

После выбора способа оплаты идут уже привычные поля формы:

  • номер карты;
  • имя владельца;
  • срок действия;
  • CVV-код.

Все эти поля используют уже существующие классы, поэтому писать дополнительный HTML практически не приходится. Именно в этом и заключается преимущество заранее продуманной структуры компонентов — новые блоки собираются буквально из уже готовых элементов.

Добавляем кнопки второго этапа

Нижняя часть второй вкладки практически полностью повторяет предыдущую.

<div class="content__bottom">
  <button class="content__btn content__btn--back" type="button" data-tab-target="delivery">
    <svg
      width="20"
      height="20"
      viewBox="0 0 20 20"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        d="M5.00008 9.99998C5.00414 10.4384 5.18078 10.8576 5.49175 11.1666L9.06675 14.75C9.22288 14.9052 9.43409 14.9923 9.65425 14.9923C9.8744 14.9923 10.0856 14.9052 10.2417 14.75C10.3199 14.6725 10.3818 14.5803 10.4242 14.4788C10.4665 14.3772 10.4882 14.2683 10.4882 14.1583C10.4882 14.0483 10.4665 13.9394 10.4242 13.8378C10.3818 13.7363 10.3199 13.6441 10.2417 13.5666L7.50008 10.8333H15.8334C16.0544 10.8333 16.2664 10.7455 16.4227 10.5892C16.579 10.433 16.6667 10.221 16.6667 9.99998C16.6667 9.77897 16.579 9.56701 16.4227 9.41073C16.2664 9.25445 16.0544 9.16665 15.8334 9.16665H7.50008L10.2417 6.42498C10.3987 6.26917 10.4873 6.0574 10.488 5.83626C10.4888 5.61512 10.4017 5.40273 10.2459 5.24582C10.0901 5.08889 9.87833 5.0003 9.65719 4.99952C9.43606 4.99874 9.22367 5.08583 9.06675 5.24165L5.49175 8.82498C5.17876 9.13611 5.00194 9.55867 5.00008 9.99998Z"
      />
    </svg>
    Назад
  </button>
  <button class="content__btn content__btn--forward" type="button">
    Проверить заказ
    <svg
      width="20"
      height="20"
      viewBox="0 0 20 20"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        d="M15 10C14.996 9.5616 14.8193 9.14243 14.5084 8.83335L10.9334 5.25002C10.7772 5.09481 10.566 5.00769 10.3459 5.00769C10.1257 5.00769 9.91451 5.09481 9.75837 5.25002C9.68027 5.32749 9.61827 5.41965 9.57597 5.5212C9.53366 5.62275 9.51188 5.73167 9.51188 5.84168C9.51188 5.95169 9.53366 6.06062 9.57597 6.16216C9.61827 6.26371 9.68027 6.35588 9.75837 6.43335L12.5 9.16668H4.16671C3.94569 9.16668 3.73373 9.25448 3.57745 9.41076C3.42117 9.56704 3.33337 9.779 3.33337 10C3.33337 10.221 3.42117 10.433 3.57745 10.5893C3.73373 10.7456 3.94569 10.8334 4.16671 10.8334H12.5L9.75837 13.575C9.60146 13.7308 9.51286 13.9426 9.51208 14.1637C9.5113 14.3849 9.59839 14.5973 9.75421 14.7542C9.91002 14.9111 10.1218 14.9997 10.3429 15.0005C10.5641 15.0013 10.7765 14.9142 10.9334 14.7584L14.5084 11.175C14.8214 10.8639 14.9982 10.4413 15 10Z"
      />
    </svg>
  </button>
</div>

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

Обратите внимание, что для возврата используется тот же самый атрибут data-tab-target, только теперь он содержит значение delivery. Благодаря этому один и тот же JavaScript сможет переключать вкладки сразу в обе стороны.

Создаем боковую панель

Теперь HTML формы полностью готов. Остается оформить правую колонку с итоговой информацией о заказе.

<aside class="content__aside">
  <h2 class="content__subtitle">Ваш заказ</h2>
  <ul class="content__list">
    <li class="content__item">
      Orbit Headphones
      <b>12 990 ₽</b>
    </li>
    <li class="content__item">
      Чехол
      <b>990 ₽</b>
    </li>
    <li class="content__item">
      Скидка
      <span>-1 500 ₽</span>
    </li>
  </ul>
  <div class="content__full">12 480 ₽</div>
</aside>

Внутри располагается заголовок, список товаров и итоговая стоимость. Сам список реализован через обычный <ul>. Это правильное решение, поскольку товары представляют собой именно перечень элементов. Каждая позиция оформлена как отдельный <li>, а стоимость товара вынесена в отдельный элемент. Благодаря этому становится гораздо проще оформить содержимое через Flexbox. Последним элементом идет общая стоимость заказа.

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

Подключим файл стилей через <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/SegoeUISemiBold.woff2");
  font-display: swap;
  font-weight: 600;
}

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

body {
  margin: 0;
  padding: 20px;
  background: #EEF1F7;
  font-family: "Segoe UI", sans-serif;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  border: 0;
  padding: 0;

  white-space: nowrap;
  clip-path: inset(100%);
  clip: rect(0 0 0 0);
  overflow: hidden;
}

Помимо общего сброса, мы также добавляем класс visually-hidden, он нужен, чтобы скрыть наш заголовок <h1> для пользователя внешне, но оставить его доступным для скринридера.

Основные стили контента

Сперва разделим наш контент. Сделаем стили для левой и правой части, заставим их стоят в одну строку. Из важного: мы делаем для родителя колонок align-items: flex-start, чтобы правая колонка не была такой же огромной, как левая, что соответствует макету. Также, мы явно задаем левой части flex: 1, чтобы она заняла все доступное пространство, а вот правая часть сделана фиксированной. Ну и конечно, мы делаем content__inner изначально скрытым, а через дополнительный класс показываем его.

.content {
  display: flex;
  align-items: flex-start;
  gap: 24px;
  margin: 0 auto;
  border-radius: 24px;
  padding: 28px;
  max-width: 1180px;
  background: #fff;
}

.content__form {
  display: flex;
  flex-direction: column;
  gap: 24px;
  flex: 1;
}

.content__inner {
  display: none;
  flex-direction: column;
  gap: 24px;
}

.content__inner--active {
  display: flex;
}

.content__aside {
  border-radius: 20px;
  padding: 20px;
  flex-shrink: 0;
  width: 340px;
  background: #fafbfe;
}

Стилизация кнопок

Теперь стилизуем кнопки, и пожалуй сделаем сразу полную стилизацию всех кнопок. И верхних, и нижних. Базовые стили весьма простые, но дьявол как всегда в деталях.

Во-первых, мы используем .content__btns .content__btn вложенность, чтобы именно эти кнопки растянулись на все доступное им пространство, т. к. по макету так и есть. При этом остальные кнопки, имеющие ровно тот же класс content__btn, растянуты не будут.

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

.content__btns {
  display: flex;
  align-items: center;
  gap: 12px;
}

.content__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  border: none;
  border-radius: 12px;
  padding: 12px 16px;
  font-family: inherit;
  font-weight: 600;
  font-size: 16px;
  line-height: 120%;
  color: #20263a;
  background: #f2f4f9;
  cursor: pointer;
  transition: color 0.3s, background 0.3s;
}

.content__btn svg {
  fill: currentColor;
}

.content__btns .content__btn {
  flex: 1;
}

.content__btn--back {
  display: none;
}

.content__btn--back.content__btn--visible {
  display: inline-flex;
}

.content__btn--active {
  color: #fff;
  background: #4c6fff;
  pointer-events: none;
}

.content__btn:hover {
  color: #fff;
  background: #4c6fff;
}

.content__btn--forward {
  margin-left: auto;
  color: #fff;
  background: #4c6fff;
}

.content__btn--forward:hover {
  background: #3c57c8;
}

Стилизация блоков с полями

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

.content__block {
  border: 1px solid #e5e8f0;
  border-radius: 20px;
  padding: 20px;
}

.content__subtitle {
  margin: 0;
  margin-bottom: 20px;
  font-weight: 700;
  font-size: 24px;
  line-height: 120%;
  color: #20263a;
}

.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;
}

Стилизация селекта

Теперь стилизация селекта. Стилизовать селекты по-прежнему кроссбраузерно невозможно, поэтому это делается через Js-библиотеки, которые по сути скрывают обычный селект, и вокруг него строят свой, кастомный. В нашем проекте уже есть папка libs, в которой лежат нужные файлы библиотеки Choices. Подключим файлы внутри <head>.

<link rel="stylesheet" href="libs/choices.min.css" />
<script src="libs/choices.min.js" defer></script>
<script src="js/script.js" defer></script>

Обязательно добавляем атрибут defer для <script>, чтобы js-код грузился только после загрузки html-кода. И также важно, чтобы script.js был подключен ниже, чем файл библиотеки. Это же касается и стилей в style.css. Всегда сперва библиотеки, а потом уже наши файлы.

Теперь пойдем в script.js и напишем простейший скрипт.

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;
}

.content__radios {

Стилизация радиокнопок

Теперь стилизуем наши способы доставки (и заодно способы оплаты, ведь они идентичны). Сперва нам нужно скрыть радиокнопки, но так, чтобы они остались доступны для скринридера и с клавиатуры. Используем opacity и appearance.

Затем, мы будем стилизовать соседа инпута — custom-radio__elem. Мы стилизуем именно этот элемент как прямоугольник, потому что потом мы сможем легко добраться до него через CSS от инпута, с помощью селектора «+».

Собственно, так мы и делаем. Если радиокнопка «чекнута» — ищем соседа, и стилизуем его иначе — добавляем иной цвет обводки и цвет фона. То же самое проделываем и для состояния :focus-visible (это аналог :focus, но только с клавиатуры).

.content__radios {
  display: flex;
  width: 100%;
  gap: 16px;
}

.content__radio {
  flex: 1;
}

.custom-radio {
  position: relative;
}

.custom-radio__input {
  position: absolute;
  opacity: 0;
  appearance: none;
}

.custom-radio__elem {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid #dbe2ef;
  border-radius: 16px;
  padding: 16px;
  min-height: 51px;
  font-weight: 400;
  font-size: 16px;
  line-height: 120%;
  color: #20263a;
  cursor: pointer;
  transition: border 0.3s, background 0.3s;
}

.custom-radio__input:checked + .custom-radio__elem {
  border: 1px solid #4c6fff;
  background: #f5f7ff;
}

.custom-radio__input:focus-visible + .custom-radio__elem {
  border: 1px solid #20263a;
}

.custom-radio__elem:hover {
  border: 1px solid #4c6fff;
  background: #f5f7ff;
}

Стилизация контейнера кнопок

Сами кнопки у нас уже стилизованы, но не расставлены как надо. Исправляем ситуацию.

.content__bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}

Стилизация правой части

Здесь вообще все просто. Используем вертикальный flexbox для списка, а горизонтальный — для элемента списка, чтобы растянуть цифры от текста (с помощью space-between). В остальном просто берем все стили из макета.

.content__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.content__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-weight: 400;
  font-size: 16px;
  line-height: 120%;
  color: #20263a;
}

.content__item b {
  font-weight: 700;
}

.content__full {
  margin-top: 16px;
  border-top: 1px solid #dde3ef;
  padding-top: 16px;
  font-weight: 700;
  font-size: 20px;
  line-height: 120%;
  color: #20263a;
}

Переключение вкладок

Остался последний шаг — написать скрипт, который будет переключать наши вкладки. И тут все весьма просто.

Напишем функцию switchTab, которая будет выполнять переключение. В качестве параметра она пример сам tab.

function switchTab(tab) {}

Внутри функции мы через дата-атрибуты найдем элементы кнопок и элементы контента.

const tabs = document.querySelectorAll('[data-tab]');
const contents = document.querySelectorAll('[data-tab-content]');

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

tabs.forEach((btn) => {
  btn.classList.toggle('content__btn--active', btn.dataset.tab === tab);
});

contents.forEach((section) => {
  section.classList.toggle('content__inner--active', section.dataset.tabContent === tab);
});

Тут мы так и делаем. Используем toggle, и проверяем условие. Если data-tab равен значению параметра функции — значит там true, и класс добавится. Иначе — удалится.

То же самое происходит и для контента.

Также нам стоит включать показ кнопки «назад», если мы не на первом табе. Для этого напишем код.

const firstTab = document.querySelector('[data-tab]').dataset.tab;
  document.querySelectorAll('.content__btn--back').forEach((btn) => {
  btn.classList.toggle('content__btn--visible', tab !== firstTab);
});

Здесь мы находим элемент с data-tab. Именно первый, потому что JavaScript в таком случае всегда ищет именно первое вхождение. А дальше пробегаемся по кнопкам «Назад», и если tab не равен первому табу — добавляем класс видимости.

Остается только заставить функцию работать, правильно ее вызвав.

document.addEventListener('click', (e) => {
  const tabBtn = e.target.closest('[data-tab]');
  if (tabBtn) {
    switchTab(tabBtn.dataset.tab);
    return;
  }

  const targetBtn = e.target.closest('[data-tab-target]');
  if (targetBtn) {
    switchTab(targetBtn.dataset.tabTarget);
    return;
  }
});

Здесь мы делаем клик по всему документу, но используем делегирование события. То есть, проверяем, а куда именно мы кликаем. Если на элемент с data-tab или data-tab-target — вызываем функцию switchTab, передавая туда активный таб. Функция срабатывает, и табы переключаются. Готово!

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

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

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