12k
All articles

Стилизация выпадающего списка select с помощью base-select

Стилизуйте нативные списки select с помощью appearance: base-select. Разберите picker, варианты, значки, доступность, поддержку браузерами и ограничения.

OpenReplay Team
OpenReplay Team
Стилизация выпадающего списка select с помощью base-select

Если задать appearance: base-select для <select> и его псевдоэлемента ::picker(select), весь элемент управления можно стилизовать средствами CSS: кнопку, выпадающий список, стрелку, галочку и каждый вариант.

Многие команды поддерживают собственный компонент выпадающего списка по одной-единственной причине: нативный <select> нельзя было стилизовать под дизайн. Обработка клавиатуры, управление фокусом и ARIA-разметка прилагались в нагрузку. appearance: base-select устраняет эту причину.

В этом руководстве разобраны все возможности стилизации, от включения режима до вариантов со сложной разметкой, а также то, чем приходится жертвовать. Классические приёмы с appearance: none описаны в статье о стилизации элементов select с помощью современного CSS: она даёт более широкую картину, но base-select затрагивает лишь вскользь. Более старые подходы рассмотрены в статье о стилизации выпадающих списков select без JavaScript.

Ключевые выводы

  • appearance: base-select нужно задать и для select, и для ::picker(select). Если задать его только для пикера, ничего не произойдёт.
  • Пикер — это поповер в верхнем слое (top layer), привязанный к кнопке select через неявную ссылку на якорь, поэтому z-index, порталы и anchor-name не нужны.
  • <selectedcontent> размещается внутри <button>, который является первым дочерним элементом <select>, и показывает клон выбранного варианта в закрытом состоянии.
  • Каждый вариант должен содержать текст: варианты из одних иконок не дают экранным дикторам ничего для озвучивания и отображаются пустыми в браузерах без поддержки.
  • С base-select вы теряете системный пикер на мобильных устройствах, возможность отрисовки за пределами окна браузера и автоматическую подгонку ширины под самый длинный вариант.

Компонент, который можно удалить

Собственный компонент select обычно существует только для того, чтобы исправить внешний вид нативного элемента <select>, и при этом ему приходится брать на себя массу поведения. В анонсе команды Chrome говорится, что начиная с Chrome 135 элемент <select> можно стилизовать с помощью CSS, сохраняя доступность и стандартное поведение встроенного элемента управления. WebKit последовал примеру в Safari 27.0, где настоящий <select> теперь можно полностью перестилизовать. Как только браузер поддерживает эту возможность, главная причина держать listbox на div-ах исчезает, а вместе с ней — и поведение, которое приходилось воссоздавать ради него.

Как включить appearance: base-select?

Чтобы включить режим, примените appearance: base-select и к <select>, и к его пикеру. В руководстве MDN по настраиваемому select поясняется, что select можно перевести в этот режим отдельно, но пикер заработает только тогда, когда режим включён и для select. Рекомендации WebKit по настраиваемому select советуют оборачивать эти улучшения в @supports (appearance: base-select). Все последующие правила держите в том же блоке, чтобы браузеры без поддержки получали обычный select.

@supports (appearance: base-select) {
  select,
  ::picker(select) {
    appearance: base-select;
  }
  /* all following rules go here */
}
ЧастьСелекторПримечания
Закрытый элемент управленияselectКнопка select
Стрелка::picker-iconНе входит в дерево доступности
Открытое состояние:openСрабатывает, пока пикер отображается
Выпадающий список::picker(select)Всё, кроме кнопки — первого дочернего элемента
Каждый вариантoptionПо умолчанию display: flex
Выбранный вариантoption:checked
Отметка выбора::checkmarkНе входит в дерево доступности
Содержимое в закрытом состоянииselectedcontentКлон выбранного варианта

Стилизация закрытого элемента управления

Закрытый элемент управления — это сам элемент select. Стрелка — ::picker-icon, а :open срабатывает, пока пикер отображается, поэтому при открытии select стрелку можно повернуть.

select {
  border: 2px solid #d4d4d8;
  background: #f4f4f5;
  padding: 10px;
  transition: 0.4s;
}

select:hover,
select:focus {
  background: #e4e4e7;
}

select::picker-icon {
  color: #71717a;
  transition: 0.4s rotate;
}

select:open::picker-icon {
  rotate: 180deg;
}

transition для rotate обеспечивает плавный поворот стрелки при каждом открытии и закрытии пикера.

Стилизация открытого списка

Открытый выпадающий список настраиваемого select — это ::picker(select). Браузер помещает в него всё содержимое <select>, кроме кнопки, идущей первой. Варианты по умолчанию являются flex-контейнерами. :checked выбирает отмеченный вариант, а ::checkmark — отметку рядом с ним.

::picker(select) {
  border: none;
  border-radius: 8px;
}

option {
  display: flex;
  gap: 16px;
  padding: 10px;
}

option:hover,
option:focus {
  background: #ede9fe;
}

option:checked {
  font-weight: bold;
}

option::checkmark {
  order: 1;
  margin-left: auto;
  content: "✔";
}

order: 1 вместе с margin-left: auto переносит галочку из начала строки в конец.

Позиционирование и анимация

В настраиваемом select выпадающий список — это поповер, который открывается в верхнем слое. Браузер самостоятельно привязывает его к кнопке select с помощью CSS anchor positioning, поэтому z-index, портал или настройка anchor-name не нужны. Кнопку и пикер связывают те же отношения invoker/popover, которые описаны для кнопок и поповеров в руководстве по invoker commands. Стили по умолчанию также переносят пикер на другую сторону, если он выходит за пределы экрана, а собственное положение можно задать через anchor():

::picker(select) {
  top: calc(anchor(bottom) + 1px);
  left: anchor(10%);
}

Чтобы пикер плавно появлялся и исчезал, задайте переход между его состояниями поповера:

::picker(select) {
  opacity: 0;
  transition: all 0.4s allow-discrete;
}

:open::picker(select) {
  opacity: 1;
}

@starting-style {
  :open::picker(select) {
    opacity: 0;
  }
}

allow-discrete позволяет дискретным свойствам display и overlay участвовать в переходе. Без него пикер покинул бы верхний слой раньше, чем стало бы заметно затухание. @starting-style задаёт прозрачность, с которой начинается появление.

Варианты со сложной разметкой и selectedcontent

Поместите <selectedcontent> внутрь <button>, который является первым дочерним элементом <select>. Он показывает клон содержимого выбранного варианта в закрытом элементе управления, и его можно стилизовать отдельно от вариантов в выпадающем списке. Теперь варианты могут содержать разметку, например иконки и изображения.

<label for="status">Status</label>
<select id="status" name="status">
  <button>
    <selectedcontent></selectedcontent>
  </button>
  <option value="draft">
    <span class="icon" aria-hidden="true">📝</span>
    <span class="option-label">Draft</span>
  </option>
  <option value="review">
    <img class="icon" src="review.svg" alt="">
    <span class="option-label">In review</span>
  </option>
  <option value="published">
    <span class="icon" aria-hidden="true">✅</span>
    <span class="option-label">Published</span>
  </option>
</select>

MDN отмечает, что иконки могут увеличить высоту закрытой кнопки и сместить стрелку. Их можно скрыть только в закрытом состоянии:

selectedcontent .icon {
  display: none;
}

Варианты в открытом пикере сохраняют свои иконки. Кнопка select инертна, поэтому размещённые в ней ссылки или кнопки нельзя по отдельности сфокусировать или нажать — всё работает как одна кнопка. MDN также указывает, что дополнительное содержимое в кнопке может изменить то, что экранные дикторы озвучивают как значение select, поэтому оставляйте в кнопке только <selectedcontent>.

Что браузер берёт на себя?

С base-select браузер обрабатывает всё то, что собственному выпадающему списку приходится реализовывать заново: взаимодействие с клавиатурой, фокус, размещение в верхнем слое, отправку значения в составе формы и семантику для экранных дикторов. WebKit подчёркивает, что поддержка клавиатуры и доступность встроены изначально, поскольку это по-прежнему собственный элемент управления браузера. Воспроизведение пользовательских сессий на выпадающем списке из div-ов, как правило, выявляет ошибки с клавиатурой и фокусом — именно тех ошибок, которых нативный select лишён по своей природе. Чтобы оценить, сколько кода требует ручной подход, загляните в руководство по созданию доступного выпадающего списка select вручную.

Два правила по-прежнему действуют:

  • Оставляйте текст в каждом варианте. Главное правило WebKit: каждому варианту нужен настоящий текст или атрибут с доступным текстом. Визуально скрытый текст экранные дикторы всё равно зачитывают. Декоративным изображениям задаётся alt="", а смысл передаёт текстовая подпись.
  • Считайте собственные иконки декоративными. Экранные дикторы игнорируют всё, что добавлено через content в ::checkmark и ::picker-icon, поэтому собственная стрелка или галочка никогда не озвучивается.

Существующий JavaScript-код, который считывает значение select, продолжает работать. Если у варианта нет атрибута value, значением select становится textContent выбранного варианта с обрезанными пробелами, поэтому всегда задавайте value, если варианты содержат сложную разметку.

Поддержка, fallback и компромиссы

appearance: base-select поддерживается в Chrome 135 и новее, в других браузерах на базе Chromium, например Edge, а также в Safari 27.0. Firefox начал добавлять поддержку за настройкой: значение appearance: base-select появилось в Firefox 149 за флагом, но по умолчанию не включено. MDN по-прежнему помечает его как функцию с ограниченной доступностью (limited availability), поэтому сверяйтесь с таблицами совместимости на справочных страницах MDN. Safari поставляет собственные стили base-select по умолчанию, так что нестилизованные элементы управления выглядят в разных браузерах по-разному.

Браузеры без поддержки base-select игнорируют разметку <button> и <selectedcontent>. Они отрисовывают обычный, нормально работающий select, в котором от каждого варианта остаётся только текст. Такой fallback имеет смысл, только если текст есть.

В посте Chrome также перечислены три особенности поведения, которые вы теряете:

  • Открытый список не выходит за пределы окна браузера.
  • На телефонах и планшетах используется пикер браузера, а не собственный пикер операционной системы.
  • Ширина select больше не подстраивается под самый длинный вариант, поэтому задайте width для select, ::picker(select) самостоятельно.

Кроме того, base-select меняет то, как HTML-парсер обрабатывает содержимое <select>. MDN предупреждает, что это может приводить к конфликтам с JavaScript-фреймворками: одни блокируют эти возможности, а в других серверный рендеринг может нарушить гидратацию. Протестируйте функцию в своём фреймворке, прежде чем удалять существующий компонент.

Заключение

Нативный select с appearance: base-select даёт контроль над стилизацией, а браузер продолжает обрабатывать клавиатуру, фокус, порядок наложения и доступность. Чтобы попробовать, выберите один простой собственный выпадающий список, пересоберите его внутри блока @supports с текстом в каждом варианте и проверьте в браузере без поддержки base-select. Если там он по-прежнему работает как обычный select, а перечисленные выше компромиссы приемлемы, собственный компонент можно удалить.

Часто задаваемые вопросы

Чем отличаются appearance: none и appearance: base-select для select?

appearance: none лишь убирает нативное оформление закрытой кнопки. Выпадающий список и его варианты по-прежнему используют пикер браузера или операционной системы, и большая часть CSS на них не действует. appearance: base-select, заданный и для select, и для ::picker(select), позволяет также стилизовать пикер, варианты, стрелку и галочку, а ещё разрешает сложную HTML-разметку внутри вариантов. Браузеры без поддержки игнорируют его и сохраняют классический элемент управления.

Как определить поддержку base-select в JavaScript?

Вызовите CSS.supports('appearance', 'base-select'). Метод возвращает true только в браузерах, которые распознают это значение. Он проверяет то же самое, что и CSS-условие @supports (appearance: base-select), поэтому с его помощью можно решить, загружать ли резервный компонент выпадающего списка или добавить класс, включающий собственные стили. Используйте определение возможностей (feature detection), а не анализ user agent, поскольку каждый движок добавлял поддержку в разное время.

Работает ли base-select с select multiple или атрибутом size?

Да, в режиме listbox. Атрибут multiple или значение size больше 1 отображают варианты в виде блока на странице, а не кнопки, открывающей всплывающий список. Начиная с Chrome 145, base-select работает и в этом режиме — как для одиночного, так и для множественного выбора. Всплывающий список с множественным выбором, который получается при multiple и size=1, пока не поддерживается. У настраиваемого listbox нет пикера, поэтому ::picker(select), ::picker-icon и selectedcontent к нему не применяются. Поддержку в других движках проверяйте по таблицам совместимости MDN.

Digital experience platform

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.