Выставление рейтинга
Несколько лет назад я реализовывал подобный элемент — выставление рейтинга пользователем. И вот вновь столкнулся с подобной задачей. Но вариант копирования решения со старого проекта не рассматривался, ведь там просто сборище дивов и куча jQuery (а по-другому я и не умел). Поэтому, начал рассуждать, как сделать лучше.
Кроме непосредственных звёздочек есть ещё и поле для комментария, что помогает прийти к мысли о том, что это элемент формы. Рейтинг же — это просто число, нажимая на звёздочку мы выбираем одно из множества. Так это же радиобаттон! Делаем базовую разметку и стилизацию.
<div class="rating-count">
<input id="c1" type="radio" name="rating" value="1">
<label for="c1">1</label>
<input id="c2" type="radio" name="rating" value="2">
<label for="c2">2</label>
<input id="c3" type="radio" name="rating" value="3">
<label for="c3">3</label>
<input id="c4" type="radio" name="rating" value="4">
<label for="c4">4</label>
<input id="c5" type="radio" name="rating" value="5">
<label for="c5">5</label>
</div>
.rating-count {
display: flex;
}
.rating-count input {
display: none;
}
.rating-count input + label {
font-size: 0;
display: block;
width: 22px;
height: 21px;
cursor: pointer;
background: url(../img/rating-star.svg) no-repeat center / contain,
url(../img/rating-star_fill.svg) no-repeat center / 0 0;
/* небольшой приём для предзагрузки изображения,
дабы не делать спрайт и инлайном не вставлять SVG */
}
.rating-count input + label:hover,
.rating-count input:checked + label {
background-image: url(../img/rating-star_fill.svg);
}
Отлично, выбор работает, сохраняется, но нужно, чтобы и предыдущие звёздочки тоже загорались, а CSS не может смотреть на следующих соседей, только на предыдущих. Не беда, меняем порядок в разметке, а внешний вид восстаналиваем при помощи flex-direction: row-reverse
, и используем отношение между селекторами ~
.
.rating-count {
display: flex;
flex-direction: row-reverse;
}
.rating-count input + label:hover,
.rating-count input + label:hover ~ label,
.rating-count input:checked + label,
.rating-count input:checked + label ~ label {
background-image: url(../img/rating-star_fill.svg);
}
Теперь ховер работает так, как надо. Но когда мы поставили оценку и захотели её поменять, то хочется, чтобы при ховере не учитывалась выбранная оценка, т. е. механика была такая же, как и при отсутствии выбора. Для этого можно воспользоваться ховером по контейнеру, в итоге получается такой замысловатый CSS.
.rating-count input + label:hover,
.rating-count input + label:hover ~ label,
.rating-count:not(:hover) input:checked + label,
.rating-count:not(:hover) input:checked + label ~ label {
background-image: url(../img/rating-star_fill.svg);
}
Вот и всё. JavaScript? А не нужен он здесь.
Когда уже почти закончил заметку, в голову пришёл вопрос о доступности. А ведь не работает данный элемент с клавиатуры! Начинаем чинить. Первым делом уберём display: none
у инпутов, спрятав их доступно, а также добавим визуальное состояние для элемента в фокусе.
.rating-count input {
position: absolute;
clip: rect(0 0 0 0);
overflow: hidden;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
}
.rating-count input + label {
background: url(../img/rating-star.svg) no-repeat center / contain,
url(../img/rating-star_fill.svg) no-repeat center / 0 0,
url(../img/rating-star_focus.svg) no-repeat center / 0 0;
}
.rating-count:not(:hover) input:focus + label {
background-image: url(../img/rating-star_focus.svg);
}
Работает, но задом наперёд, спасибо row-reverse
за это. Использовать tabindex
не комильфо. Что ж, возвращаем прямой порядок, убираем row-reverse
, стилизуем так же при помощи ~
, но хитрее, используя вспомогательный input. Как заметили в комментариях, данный способ не позволяет получить изначальный фокус с клавиатуры. Используем сочетание псевдокласса invalid
и атрибута required
.
<div class="rating-count">
<input id="c1" type="radio" name="rating" value="1" required>
<label for="c1">1</label>
<input id="c2" type="radio" name="rating" value="2">
...
</div>
.rating-count input + label {
background-image: url(../img/rating-star_fill.svg);
}
.rating-count label:hover ~ label,
.rating-count:not(:hover) input:checked + label ~ label,
.rating-count:not(:hover) input:invalid + label {
background-image: url(../img/rating-star.svg);
}
.rating-count input + label:hover,
.rating-count:not(:hover) input:focus + label {
background-image: url(../img/rating-star_focus.svg);
}
Получилась комбинация из not
, hover
, focus
, checked
, invalid
, +
и ~
. Вот она, мощь HTML и CSS. Всё работает, работает доступно, без избыточной разметки и без JavaScript.
Итоговый код
<div class="rating-count">
<input id="c1" type="radio" name="rating" value="1" required>
<label for="c1">1</label>
<input id="c2" type="radio" name="rating" value="2">
<label for="c2">2</label>
<input id="c3" type="radio" name="rating" value="3">
<label for="c3">3</label>
<input id="c4" type="radio" name="rating" value="4">
<label for="c4">4</label>
<input id="c5" type="radio" name="rating" value="5">
<label for="c5">5</label>
</div>
.rating-count {
display: flex;
width: 110px;
}
.rating-count input {
position: absolute;
overflow: hidden;
clip: rect(0 0 0 0);
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
}
.rating-count input + label {
font-size: 0;
display: block;
width: 22px;
height: 21px;
cursor: pointer;
background-image: url(../img/rating-star_fill.svg);
}
.rating-count label:hover ~ label,
.rating-count:not(:hover) input:invalid + label,
.rating-count:not(:hover) input:checked + label ~ label {
background: url(../img/rating-star.svg) no-repeat center / contain,
url(../img/rating-star_fill.svg) no-repeat center / 0 0,
url(../img/rating-star_focus.svg) no-repeat center / 0 0;
}
.rating-count input + label:hover,
.rating-count:not(:hover) input:focus + label {
background-image: url(../img/rating-star_focus.svg);
}
{{ 'Comments (%count%)' | trans {count:count} }}
{{ 'Comments are closed.' | trans }}