Кастомный select с анимацией выпадающего списка на CSS
Стандартный элемент select долгое время был одним из самых сложных элементов формы с точки зрения стилизации.
Можно было изменить шрифт, фон, рамку и внешний вид самого поля, но выпадающий список во многом отрисовывался браузером или операционной системой.
Из-за этого для полностью кастомного дизайна часто создавали собственный select из div, ul и JavaScript.
У такого решения есть существенный недостаток: разработчику приходится самостоятельно реализовывать работу с клавиатурой, фокусом, формами и средствами доступности.
Современный CSS постепенно решает эту проблему. Для обычного нативного <select> появились новые возможности:
appearance: base-select — включает режим расширенной стилизации;
::picker(select) — позволяет обратиться к выпадающему списку;
::picker-icon — позволяет стилизовать стрелку;
:open — определяет открытое состояние select;
:checked — позволяет оформить выбранный option;
::checkmark — позволяет изменить маркер выбранного пункта.
При этом сам элемент остаётся обычным HTML select.
Если новые возможности CSS не поддерживаются браузером, пользователь всё равно увидит стандартный рабочий select.
Так браузеры, которые ещё не поддерживают base-select, просто проигнорируют дополнительное оформление:
Добавим плавный поворот при открытии списка:
Например, сделаем одновременно плавное появление и небольшое смещение сверху:
opacity отвечает за плавное появление;
translate создаёт эффект раскрытия сверху вниз;
allow-discrete позволяет корректно работать с дискретными свойствами;
@starting-style задаёт начальное состояние элемента в момент его появления.
При закрытии меняются в том числе свойства display и overlay.
Обычная CSS transition не умеет плавно интерполировать такие значения, поэтому используется:
Именно поэтому стили, связанные с appearance: base-select, помещены внутрь:
Если поддержки нет, останется обычный нативный select.
Функциональность формы при этом не ломается.
Но сам по себе div не является элементом формы и не обладает поведением select.
Разработчику дополнительно придётся реализовать:
работу клавиш Enter, Escape и стрелок;
управление фокусом;
ARIA-роли и состояния;
выбор варианта с клавиатуры;
синхронизацию с настоящим значением формы;
обработку disabled;
закрытие списка;
поведение для экранных дикторов.
Поэтому для обычного одиночного выбора лучше сохранить нативный select.
В таком случае это уже не просто стилизация select, а полноценный компонент типа combobox/listbox, для которого необходимо отдельно учитывать клавиатурную навигацию и доступность.
Для этого используются:
appearance: base-select — для включения нового режима;
::picker(select) — для оформления выпадающего списка;
:open — для открытого состояния;
::picker-icon — для анимации стрелки;
@starting-style и allow-discrete — для плавного появления и скрытия списка.
Основное преимущество такого подхода — progressive enhancement.
В поддерживающих браузерах select получает полноценное кастомное оформление и анимацию, а в остальных остаётся стандартным, доступным и полностью рабочим элементом формы.
Вернуться к блогу
Можно было изменить шрифт, фон, рамку и внешний вид самого поля, но выпадающий список во многом отрисовывался браузером или операционной системой.
Из-за этого для полностью кастомного дизайна часто создавали собственный select из div, ul и JavaScript.
У такого решения есть существенный недостаток: разработчику приходится самостоятельно реализовывать работу с клавиатурой, фокусом, формами и средствами доступности.
Современный CSS постепенно решает эту проблему. Для обычного нативного <select> появились новые возможности:
appearance: base-select — включает режим расширенной стилизации;
::picker(select) — позволяет обратиться к выпадающему списку;
::picker-icon — позволяет стилизовать стрелку;
:open — определяет открытое состояние select;
:checked — позволяет оформить выбранный option;
::checkmark — позволяет изменить маркер выбранного пункта.
При этом сам элемент остаётся обычным HTML select.
HTML-разметка
Начнём с обычного нативного select:
<label for="project-type">Тип проекта</label>
<select
class="custom-select"
id="project-type"
name="project_type"
>
<option value="" selected disabled>
Выберите вариант
</option>
<option value="website">
Разработка сайта
</option>
<option value="support">
Поддержка сайта
</option>
<option value="integration">
Интеграция
</option>
<option value="design">
Дизайн
</option>
</select>
Это обычный элемент формы.Если новые возможности CSS не поддерживаются браузером, пользователь всё равно увидит стандартный рабочий select.
Базовые стили
Сначала оформим сам элемент так, чтобы он нормально выглядел даже без поддержки новых возможностей:
.custom-select {
width: 280px;
min-height: 44px;
padding: 10px 14px;
font: inherit;
color: #222;
background-color: #fff;
border: 1px solid #d5d5d5;
border-radius: 8px;
cursor: pointer;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.custom-select:hover {
border-color: #999;
}
.custom-select:focus {
outline: none;
border-color: #666;
box-shadow:
0 0 0 3px rgba(0, 0, 0, 0.08);
}
Эти стили применяются и к обычному select, поэтому базовый внешний вид сохраняется во всех современных браузерах.
Включаем кастомный режим select
Расширенную стилизацию лучше подключать через @supports.Так браузеры, которые ещё не поддерживают base-select, просто проигнорируют дополнительное оформление:
@supports (appearance: base-select) {
.custom-select,
.custom-select::picker(select) {
appearance: base-select;
}
}
Важно установить appearance: base-select и самому select, и его выпадающему picker.
Стилизация выпадающего списка
После включения нового режима можно обращаться непосредственно к выпадающей части select:
@supports (appearance: base-select) {
.custom-select::picker(select) {
margin-top: 6px;
background: #fff;
border: 1px solid #d5d5d5;
border-radius: 8px;
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.12);
overflow: hidden;
}
}
В отличие от старого подхода нам больше не требуется создавать отдельный ul.
Стилизация вариантов
Каждый option также можно оформить отдельно:
@supports (appearance: base-select) {
.custom-select option {
padding: 11px 14px;
color: #222;
background: #fff;
cursor: pointer;
}
.custom-select option:hover,
.custom-select option:focus {
background: #f5f5f5;
}
.custom-select option:checked {
font-weight: 600;
background: #eeeeee;
}
}
Таким образом можно задавать собственные отступы, фон и оформление активного пункта.
Анимация стрелки
Для встроенной стрелки используется псевдоэлемент ::picker-icon.Добавим плавный поворот при открытии списка:
@supports (appearance: base-select) {
.custom-select::picker-icon {
color: #777;
transition:
rotate 0.25s ease;
}
.custom-select:open::picker-icon {
rotate: 180deg;
}
}
Псевдокласс :open применяется в тот момент, когда список вариантов открыт.
Анимация выпадающего списка
Теперь можно анимировать непосредственно сам picker.Например, сделаем одновременно плавное появление и небольшое смещение сверху:
@supports (appearance: base-select) {
.custom-select::picker(select) {
opacity: 0;
translate: 0 -8px;
transition:
opacity 0.2s ease,
translate 0.2s ease,
display 0.2s allow-discrete,
overlay 0.2s allow-discrete;
}
.custom-select:open::picker(select) {
opacity: 1;
translate: 0 0;
}
@starting-style {
.custom-select:open::picker(select) {
opacity: 0;
translate: 0 -8px;
}
}
}
Здесь используются сразу несколько современных возможностей CSS.opacity отвечает за плавное появление;
translate создаёт эффект раскрытия сверху вниз;
allow-discrete позволяет корректно работать с дискретными свойствами;
@starting-style задаёт начальное состояние элемента в момент его появления.
Почему используются display и overlay
Выпадающий список современного select работает аналогично popover и отображается в верхнем слое браузера.При закрытии меняются в том числе свойства display и overlay.
Обычная CSS transition не умеет плавно интерполировать такие значения, поэтому используется:
allow-discrete
Это позволяет браузеру отложить окончательное скрытие picker до завершения визуальной анимации.
Полный CSS
В результате стили будут выглядеть следующим образом:
.custom-select {
width: 280px;
min-height: 44px;
padding: 10px 14px;
font: inherit;
color: #222;
background-color: #fff;
border: 1px solid #d5d5d5;
border-radius: 8px;
cursor: pointer;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.custom-select:hover {
border-color: #999;
}
.custom-select:focus {
outline: none;
border-color: #666;
box-shadow:
0 0 0 3px rgba(0, 0, 0, 0.08);
}
@supports (appearance: base-select) {
.custom-select,
.custom-select::picker(select) {
appearance: base-select;
}
.custom-select::picker(select) {
margin-top: 6px;
background: #fff;
border: 1px solid #d5d5d5;
border-radius: 8px;
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.12);
overflow: hidden;
opacity: 0;
translate: 0 -8px;
transition:
opacity 0.2s ease,
translate 0.2s ease,
display 0.2s allow-discrete,
overlay 0.2s allow-discrete;
}
.custom-select:open::picker(select) {
opacity: 1;
translate: 0 0;
}
@starting-style {
.custom-select:open::picker(select) {
opacity: 0;
translate: 0 -8px;
}
}
.custom-select::picker-icon {
color: #777;
transition:
rotate 0.25s ease;
}
.custom-select:open::picker-icon {
rotate: 180deg;
}
.custom-select option {
padding: 11px 14px;
color: #222;
background: #fff;
cursor: pointer;
}
.custom-select option:hover,
.custom-select option:focus {
background: #f5f5f5;
}
.custom-select option:checked {
font-weight: 600;
background: #eeeeee;
}
}
Полный HTML
Разметка при этом остаётся очень простой:
<label for="project-type">
Тип проекта
</label>
<select
class="custom-select"
id="project-type"
name="project_type"
>
<option value="" selected disabled>
Выберите вариант
</option>
<option value="website">
Разработка сайта
</option>
<option value="support">
Поддержка сайта
</option>
<option value="integration">
Интеграция
</option>
<option value="design">
Дизайн
</option>
</select>
JavaScript для открытия, закрытия и выбора значения здесь не требуется.
Получение выбранного значения через JavaScript
Если выбранное значение требуется использовать в JavaScript, всё работает так же, как с обычным select:
const select = document.querySelector('.custom-select');
select.addEventListener('change', () => {
console.log('Выбрано:', select.value);
});
Никакой дополнительной синхронизации между визуальным элементом и настоящим полем формы не требуется.
Поддержка браузерами
У нового подхода есть важный нюанс: возможности полностью кастомизируемого select пока поддерживаются не всеми браузерами.Именно поэтому стили, связанные с appearance: base-select, помещены внутрь:
@supports (appearance: base-select) {
/* Расширенное оформление */
}
Если браузер поддерживает новую технологию, пользователь увидит стилизованный выпадающий список с анимацией.Если поддержки нет, останется обычный нативный select.
Функциональность формы при этом не ломается.
Почему не стоит просто заменять select на div
Распространённый старый вариант выглядит примерно так:
<div class="custom-select">
<div class="select-header">
Выберите вариант
</div>
<ul>
<li>Вариант 1</li>
<li>Вариант 2</li>
</ul>
</div>
Визуально такой компонент сделать несложно.Но сам по себе div не является элементом формы и не обладает поведением select.
Разработчику дополнительно придётся реализовать:
работу клавиш Enter, Escape и стрелок;
управление фокусом;
ARIA-роли и состояния;
выбор варианта с клавиатуры;
синхронизацию с настоящим значением формы;
обработку disabled;
закрытие списка;
поведение для экранных дикторов.
Поэтому для обычного одиночного выбора лучше сохранить нативный select.
Когда всё-таки нужен полностью кастомный компонент
Собственный JavaScript-компонент по-прежнему может понадобиться, если список должен содержать сложные интерактивные элементы, поиск, изображения, динамическую фильтрацию или нестандартную бизнес-логику.В таком случае это уже не просто стилизация select, а полноценный компонент типа combobox/listbox, для которого необходимо отдельно учитывать клавиатурную навигацию и доступность.
Итог
Современный CSS позволяет сделать кастомный select с анимацией, не заменяя стандартный элемент формы набором div и ul.Для этого используются:
appearance: base-select — для включения нового режима;
::picker(select) — для оформления выпадающего списка;
:open — для открытого состояния;
::picker-icon — для анимации стрелки;
@starting-style и allow-discrete — для плавного появления и скрытия списка.
Основное преимущество такого подхода — progressive enhancement.
В поддерживающих браузерах select получает полноценное кастомное оформление и анимацию, а в остальных остаётся стандартным, доступным и полностью рабочим элементом формы.