Как стать автором
Обновить

Звездный рейтинг на HTML-CSS

Уровень сложностиПростой
Время на прочтение9 мин
Количество просмотров3.4K
Автор оригинала: Vaibhav Shete

Мы знакомы с поведением поля ввода пятизвездочного рейтинга. Отображены пустые звезды. Пользователь решает, сколько баллов из пяти будет содержать его оценка. Он выбирает (тап/клик) звезду, которая соответствует оценке (в зависимости от позиции с начала). Выбранная звезда и все звезды, расположенные перед ней, отображаются закрашенными, что обозначает их активное состояние. Если, например, пользователь хочет оценить на три из пяти, он кликает на третьей звезде. После этого первая, вторая и третья звезда «зажигаются». Если не выбрана ни одна звезда, считается, что пользователь не оценивал и оценка не приравнивается к нулю. Оценив однажды, пользователь не может вернуться, чтобы отменить оценивание.

Пример

Конечно, довольно просто сделать это с помощью JavaScript, но для тренировки давайте попробуем сделать это с помощью обычного CSS.

Анализ

Что мы хотим получить в итоге? Выбор. Выбор из пяти вариантов. Для этого существуют встроенные элементы формы, давайте посмотрим, можем ли мы их стилизовать, чтобы использовать для наших нужд.

Вариант 1: Выпадающий список select. Варианты выбора (option) стилизуются большей частью браузером для обеспечения лучшей совместимости с устройством пользователя. Не подходит для наших целей.

Вариант 2: Радиокнопки!

  • Позволяют выбрать один вариант из нескольких.

  • Могут быть по‑разному стилизованы.

  • Могут иметь независимые метки (теги label).

  • Могут быть по‑разному оформлены в зависимости от состояния (например выбран вариант или нет).

Добавить к этому комбинированные селекторы CSS одного уровня и все получится!

Цели

  1. Добиться результата используя только HTML‑CSS, без применения JavaScript.

  2. Сделать разметку настолько семантичной, насколько возможно.

  3. Сделать максимально дружественной для клавиатуры и вспомогательных технологий.

Исследование

Метки (labels)

Это специальные элементы формы, которые могут быть назначены элементам ввода (input) с помощью атрибута for. Они должны содержать атрибут id, совпадающий с id связанного элемента ввода. У каждого элемента ввода может быть несколько меток. Клик на метке активирует сопоставленный ей элемент ввода. Если элемент ввода — чекбокс, клик на метке изменит состояние элемента (включен/выключен). В общем, клик на метке аналогичен клику на элементе ввода. Срабатывает даже событие клика (полезно, в основном, для обработки в JS). Так, мы можем скрыть радио‑кнопки и отображать их метки. Как это сделать? И будут ли они менять вид в разных состояниях: в фокусе (focused) и отмеченный (checked).

Соседние комбинаторы + и ~

Используются в CSS с синтаксисом

селектор1 ~ селектор2

или

селектор1 + селектор2.

Соседние — те, у которых общий родительский элемент.

+ выбирает элемент, соответствующий селектору2, который следует непосредственно за селектором1.

Например p + div выберет любой тег div, следующий сразу за тегом p.

~ выберет все элементы, соответствующие селектору2, следующие за селектором1 и соседствующие с ним.

Например a ~ div выберет все теги div, идущие после тега a и соседствующие с тегом a.

Здесь хорошее объяснение от w3schools.

Псевдо-классы :checked, :open и другие

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

Реализация

Давайте поместим все радиокнопки и метки в один элемент‑обертку, чтобы все они были соседними. В разметке поместим каждую метку после назначенного ей элемента ввода. Это позволит нам стилизовать метку в зависимости от состояния назначенной ей радиокнопки. Каждая из меток будет содержать изображение звезды, которое можно изменить с помощью css для обозначения активного и неактивного состояния. Например простые элементы div с свойством clip-path в форме звезды, изменяемые с помощью корректировки background-color, или элементы svg, в которых форма звезды выполнена заливкой currentColor и может быть изменена с помощью редактирования цвета родительского элемента.

Это означает, что мы прикрепляем метки к элементам ввода один раз семантически, используя атрибут for, а затем еще раз в разметке, чтобы состояние радиокнопки влияло на оформление метки с помощью комбинатора +.

Обратите внимание, что обе эти пары могут быть взаимоисключающими. Это означает, что если мы расположим элементы в неправильном порядке, состояние элемента ввода может повлиять на стиль метки, которая не была ему назначена. Не существует селекторов 'метка:назначить‑радиокнопке‑с‑состоянием‑focused/checked'. Этого можно добиться с помощью разметки и понимания селекторов‑комбинаторов CSS.

Вот что мы будем делать:

<div class="star-wrap">
  <input type="radio" id="st-1" value="1" name="star-radio"/>
  <label for="st-1">...</label>
  <input type="radio" id="st-2" value="2" name="star-radio"/>
  <label for="st-2">...</label>
  .
  .
  <input type="radio" id="st-5" value="5" name="star-radio"/>
  <label for="st-5">...</label>
</div>

Теперь мы настроили дерево DOM таким образом, что состояние каждой радиокнопки может влиять на ее ярлык с помощью модификатора +.

Спрячем радиокнопки:

input[type=radio] {
  position:fixed;
  opacity:0;
  left:-90000px;
}

Мы не используем display: none, потому что это сделает радиокнопку нефокусируемой с клавиатуры в некоторых браузерах и не обнаруживаемой устройствами чтения с экрана в большинстве случаев. Кроме того, мы убираем ее за левую границу, потому что если бы мы убирали ее за правую или нижнюю границу, то размер документа был бы очень большим и добавились бы полосы прокрутки, чтобы показать убранный элемент управления.

Вот как это выглядит, когда разметка готова:

Я использовал Clippy, чтобы получить форму звезды с помощью clip-path.

Я также использовал JavaScript для отображения значения выбранной звезды. Никакой другой логики он не реализует.

Несмотря на то, что мы скрыли их визуально, радиокнопки все еще находятся в DOM, поэтому мы можем фокусировать/выбирать их с помощью клавиатуры и указателя (мыши/тач) через метки. Мы можем использовать псевдокласс :focus вместе с комбинатором +, чтобы выбрать метку, которая находится сразу после сфокусированной радиокнопки:

input[type=radio]:focus + label {
  outline: 2px dotted black;
}

В результате состояние фокусировки на радиокнопке будет влиять на ее метку следующим образом:

А теперь, для основной части, давайте воспользуемся модификатором ~, чтобы окрасить этикетки в золотой цвет.

input[type=radio]:checked ~ label {
  background-color: gold;
}

Но основная проблема...

Видите, в чем проблема? Золотыми становятся звезды, находящиеся за выбранной звездой, но мы хотим, чтобы золотыми были звезды перед выбранной. Теперь, если мы разрабатываем дизайн для пользователей с письменностью справа налево (как урду) и мы знаем, что они ожидают горящие звезды справа от выбранной, мы остановимся на этом. Но как быть с пользователями, использующими письмо слева направо (как в английском языке)?

Возможные решения:

Решение 1. Давайте превратим div‑обертку во flex и установим его flex-direction как row-reverse.

div.star-wrap {
  display: flex;
  flex-direction: row-reverse;
}

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

<div class="star-wrap">
  <input type="radio" id="st-1" value="5" name="star-radio"/>
  <label for="st-1">...</label>
  <input type="radio" id="st-2" value="4" name="star-radio"/>
  <label for="st-2">...</label>
  .
  .
  <input type="radio" id="st-5" value="1" name="star-radio"/>
  <label for="st-5">...</label>
</div>

Результат: работает нормально, но когда пользователь пытается сфокусироваться с помощью клавиатуры, клавиши со стрелками влево‑вправо следуют за направлением DOM для радиокнопок. Браузер рассматривает всю группу радиокнопок (сгруппированных по атрибуту name) как один элемент ввода. Мы можем перейти к ней с помощью tab, но, оказавшись внутри, нам придется использовать клавиши‑стрелки для перемещения между вариантами. В данном случае браузер не учел, что флекс расположил их в обратном порядке, и не настроил соответствующим образом ввод клавиш‑стрелок. Есть ли другой способ?

Решение 2: Давайте превратим обертку в обычный div с обратным направлением текста (справа налево). Затем сделаем метки инлайн‑элементами.

div.star-wrap {
  display: block;
  direction: rtl;
}

label {
  display:inline-flex;
}

Результат: Это решает проблему навигации по клавиатуре. Но при использовании программы чтения с экрана порядок элементов сбивает пользователя с толку. Я пробовал, но никакие свойства aria‑ не смогли исправить это. Я не эксперт по aria (Accessible Rich Internet Applications), но чаще всего дается следующий совет:

Не используйте свойства aria, если существует семантически правильное решение.

Давайте ориентироваться на него.

Сначала восстановим порядок атрибутов value элементов ввода.

<div class="star-wrap">
  <input type="radio" id="st-1" value="1" name="star-radio"/>
  <label for="st-1">...</label>
  <input type="radio" id="st-2" value="2" name="star-radio"/>
  <label for="st-2">...</label>
  .
  .
  <input type="radio" id="st-5" value="5" name="star-radio"/>
  <label for="st-5">...</label>
</div>

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

Как нам это сделать?

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

Есть мысль. Давайте сначала окрасим все звезды в золотой цвет. Затем обесцветим все звезды, которые находятся после отмеченной радиокнопки.

Но это будет включать и метку, которая находится сразу после радиокнопки. Это назначенная метка в нашей разметке.

Нужно ли изменить порядок? Сначала метка, а потом элемент ввода?

Работает! Но... О, великолепно. Теперь указание фокуса неверно.

Хм. Чтобы индикация фокуса была правильной, назначенный ярлык должен располагаться после радиокнопки. Давайте изменим порядок.

Что если объединить комбинаторы?

Помните комбинированные селекторы селектор1 ~ селектор2, о которых мы говорили ранее? Здесь селектор1 сам может быть комбинацией. Если мы сделаем все правильно, мы сможем выбрать метки, которые идут после метки, идущей после отмеченной радиокнопки!

label {
  background-color: gold;
}

input[type=radio]:checked + label ~ label {
  background-color: lightgray;
}

По‑английски это будет звучать так: «Раскрасьте все метки в золотой цвет. Теперь. Видите отмеченные радиокнопки? Да? Хорошо. Видите метку следом за ней? Отлично. Теперь выберите все соседние метки, которые идут после нее — и окрасьте их в светло‑серый цвет».

Теперь работает очень хорошо!

Давайте обновим. Превосх... Подождите. ЧТО?!

Правильно. Когда ни одна из радиокнопок не выбрана, ни один ярлык не окрашивается. По умолчанию все метки золотые!

И что теперь?

Полезен ли модификатор:not()? Нет, он просто выберет метки рядом с не отмеченными радиокнопками и не было смысла расставлять их в таком порядке все это время.

Стоит ли нам просто сдаться и использовать JavaScript для реализации логики?

.. 🤔...

Все метки, находящиеся после метки, следующей за отмеченной радиокнопкой, обесцвечиваются...

БИНГО! Что, если мы добавим скрытую пару «радиокнопка‑метка» перед всем этим? Мы сделаем так, чтобы она была отмечена по умолчанию. Но это означает, что им нужно присвоить некое значение. Так ли это? Мы можем отключить его. Или еще лучше. Мы можем присвоить ему значение, которое бэкэнд сможет интерпретировать как «пропущено». Например -1?

<div class="star-wrap">
  <input checked type="radio" value="-1" name="star-radio"/>
  <label>...</label>
  <input type="radio" id="st-1" value="1" name="star-radio"/>
  <label for="st-1">...</label>
  <input type="radio" id="st-2" value="2" name="star-radio"/>
  <label for="st-2">...</label>
.
.
.

Или мы можем просто добавить к нему атрибут disabled, чтобы он не отправлялся при отправке формы и, следовательно, интерпретировался как пропущенный.

Знаете что? Если мы сможем убедить команду бэкенда интерпретировать -1 как пропуск, мы сможем даже добавить кнопку для пропуска после того, как пользователь выберет какую‑то оценку! Как? Просто добавить еще одну метку — назначенную радиокнопке пропуска значения — перед концом того же элемента обертки, и оформить ее так, чтобы она скрывалась только при выборе радиокнопки пропуска выбора.

И мы закончили!

Семантически корректный звездный рейтинг с помощью только HTML и CSS!

Полный код смотрите в CodePen.

Теги:
Хабы:
Всего голосов 5: ↑5 и ↓0+6
Комментарии2

Публикации

Истории

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань