Свой Select на чистом CSS и JavaScript

В этой статье мы разберём пример реализации кастомного селекта на чистом CSS и JavaScript, который будет очень похож на стандартный HTML-тег <select>
.
Зачем создавать свой Select, если есть <select>
Когда мы создаем селект с помощью стандартного HTML-элемента <select>
, мы ограничены в его настройке. Так как не всегда можем стилизовать и запрограммировать его так, как нам это нужно.
В этом случае более грамотным решением будет написать свою реализацию селекта с нужной логикой и оформлением.
Здесь мы рассмотрим Select, расположенный на GitHub, который называется ItcCustomSelect
. Он написан на чистом CSS и JavaScript. ItcCustomSelect
имеет простой функционал, который позволяет выбрать одно из нескольких значений в выпадающем списке.
Вот так он выглядит:


Несмотря на то, что ItcCustomSelect
имеет только базовый функционал, его в зависимости от задачи можно расширить. Вообще в веб-разработке создание выпадающего меню – это одна из наиболее часто возникающих задач.
Подключение CSS и JavaScript файлов
Исходные коды ItcCustomSelect
расположены на GitHub в папке custom-select
. ItcCustomSelect
- это один из компонентов в наборе ui-components
. Там кроме него есть ещё много других, которые вы можете использовать на сайте.
ItcCustomSelect
состоит из 2 файлов: itc-custom-select.css
и itc-custom-select.js
. Скрипт написан на чистом JavaScript и не имеет зависимостей от других плагинов, в том числе и от библиотеки jQuery.
Для его установки необходимо загрузить в своё веб-приложение или сайт 2 этих файла и подключить их:
<!-- подключаем CSS-селекта -->
<link rel="stylesheet" href="/aasets/css/itc-custom-select.css">
<!-- подключаем JS-селекта -->
<script src="/assets/js/itc-custom-select.js"></script>
CSS подключаем с помощью тега <link>
, а JavaScript с помощью <script>
.
Вставка селекта на страницу и его активация
Кастомный селект представляет собой класс ItcCustomSelect
. Работать с ним можно 2 способами.
Вариант 1. Он подразумевает непосредственную вставку HTML-кода селектора на страницу и его инициализацию с помощью JavaScript как ItcCustomSelect
.
В этом варианте необходимо вставить на страницу полный HTML-код селекта, который имеет следующую структуру:
<!-- Изначально не активна ни одна опция -->
<div class="itc-select" id="select-1">
<!-- Кнопка для открытия выпадающего списка -->
<button type="button" class="itc-select__toggle" name="car" value="" data-select="toggle" data-index="-1">Выберите из списка</button>
<!-- Выпадающий список -->
<div class="itc-select__dropdown">
<ul class="itc-select__options">
<li class="itc-select__option" data-select="option" data-value="volkswagen" data-index="0">Volkswagen</li>
<li class="itc-select__option" data-select="option" data-value="ford" data-index="1">Ford</li>
<li class="itc-select__option" data-select="option" data-value="toyota" data-index="2">Toyota</li>
<li class="itc-select__option" data-select="option" data-value="nissan" data-index="3">Nissan</li>
</ul>
</div>
</div>
После вставки HTML-кода его корневой элемент, то есть .itc-select
, нужно активировать как ItcCustomSelect
посредством JavaScript:
// select-1 – id элемента
const select1 = new ItcCustomSelect('#select-1');
Если нужно сформировать опции через AJAX, то сделать это можно очень просто, например, так:
<!-- Изначально не активна ни одна опция -->
<div class="itc-select">
<!-- Кнопка для открытия выпадающего списка -->
<button type="button" class="itc-select__toggle" name="car" value="" data-select="toggle" data-index="-1" disabled>Выберите из списка</button>
<!-- Выпадающий список -->
<div class="itc-select__dropdown">
<ul class="itc-select__options"></ul>
</div>
</div>
<script>
(async() => {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
if (response.ok) {
const data = await response.json();
const values = Object.keys(data).map((key, index) => {
return `<li class="itc-select__option" data-select="option" data-value="${key}" data-index="${index}">${data[key].name}</li>`;
});
document.querySelector('.itc-select__options').innerHTML = values.join('');
new ItcCustomSelect('#select-1');
document.querySelector('.itc-select__toggle').disabled = false;
}
})();
</script>
Теперь более подробно про HTML-код. В нём имеется элемент <button>
, который используется для переключения видимости выпадающего списка. Действие, которая эта кнопка выполняет, определяется в JavaScript через атрибут data-select="toggle"
.
У кнопки кроме data-select="toggle"
имеются ещё другие атрибуты name
и value
. Они определяют соответственно имя селекта и его значение. Посредством них, если селект поместить в <form>
, его значение вместе с другими данными формы будут отправлены на сервер.
Атрибут data-index
содержит индекс выбранной опции. Если по умолчанию не должна быть активна какая-то опция, то value
следует задать пустую строку, а data-index
– значение -1
.
Внутри itc-select__options
необходимо расположить необходимые опции. Здесь это осуществляется посредством <li>
, к которому необходимо добавить класс itc-select__option
и следующие атрибуты:
data-select="option"
– означает, что это опция;data-value
– значение опции;data-index
– индекс (порядковый номер) опции.
Если изначально какая-то опция должна быть активна, то к ней необходимо добавить класс itc-select__item_selected
. Кроме этого для <button>
в атрибут value
нужно поместить её значение, в data-index
– её индекс, а в содержимое – её контент.
Пример HTML-кода, в котором первая опция установлена по дефолту:
<!-- по дефолту активна 1 опция -->
<div class="itc-select" id="select-1">
<button type="button" class="itc-select__toggle" name="car" value="ford" data-select="toggle" data-index="1">Выберите из списка</button>
<div class="itc-select__dropdown">
<ul class="itc-select__options">
<li class="itc-select__option" data-select="option" data-value="volkswagen" data-index="0">Volkswagen</li>
<li class="itc-select__option select__option_selected" data-select="option" data-value="ford" data-index="1">Ford</li>
<li class="itc-select__option" data-select="option" data-value="toyota" data-index="2">Toyota</li>
<li class="itc-select__option" data-select="option" data-value="nissan" data-index="3">Nissan</li>
</ul>
</div>
</div>
Вариант 2. Этот вариант предполагает вставку в HTML пустого элемента <div>
, который следует использовать в качестве селекта. То есть в этом случае HTML-код будет следующим:
<div id="select-2"></div>
В этом варианте всю структуру создаст сам JavaScript. Варианты и дефолтный текст селекту необходимо передать при создании экземпляра класса ItcCustomSelect
посредством аргумента в формате объекта:
const select2 = new ItcCustomSelect('#select-2', {
name: 'car', // значение атрибута name у кнопки
targetValue: 'ford', // значение по умолчанию
options: [['volkswagen', 'Volkswagen'], ['ford', 'Ford'], ['toyota', 'Toyota'], ['nissan', 'Nissan']], // опции
});
Значение options
– это массив массивов. Первый элемент массива – это значение опции, а второй – её текстовое представление.
Если не нужно чтобы селект имел значение по умолчанию, то установите ключу targetValue
пустую строку или вообще его не указывайте:
const select2 = new ItcCustomSelect('#select-2', {
name: 'car', // значение атрибута name у кнопки
options: [['volkswagen', 'Volkswagen'], ['ford', 'Ford'], ['toyota', 'Toyota'], ['nissan', 'Nissan']], // опции
});
Свойства и методы ItcCustomSelect
После инициализации селекта, нам будут доступны следующие свойства и методы:
value
– позволяет как получить выбранную опцию, так и установить её;selectedIndex
– индекс выбранного элемента (нумерация начинается с 0);show()
– показывает выпадающий список с опциями;hide()
– скрывает dropdown меню;toggle()
– переключает видимость выпадающего меню;dispose()
– удаляет обработчики событий, связанных с этим селектом.
Использование свойства value
:
// установим в качестве выбранной опции элемент со значением toyota
select2.value = 'toyota';
// получим значение выбранной опции
console.log(select2.value); // toyota
Кроме этого value
позволяет также сбросить выбранную опцию. Для этого value
нужно установить пустую строку или значение не соответствующее ни одной из опций:
// сбросим выбранную опцию
select2.value = '';
Использование свойства selectedIndex
:
// установим в качестве выбранной опции элемент с индексом 2
select2.selectedIndex = 2;
// получим индекс выбранной опции
console.log( select2.selectedIndex );
Если ни один из элементов не выбран, то selectedIndex
возвращает -1
:
select2.value = '';
// получим индекс выбранного элемента
console.log( select2.selectedIndex ); // -1
Сбросить выбранный элемент можно не только посредством value
, но также, если установить selectedIndex
число -1
или индекс элемента, которого нет:
select2.selectedIndex = -1;
Если нам необходимо выполнить некоторые действия при выборе элемента отличного от текущего, то мы можем воспользоваться событием itc.select.change
, генерируемым в JavaScript коде:
document.querySelector('#select-2').addEventListener('itc.select.change', (e) => {
const btn = e.target.querySelector('.itc-select__toggle');
// выбранное значение
console.log(`Выбранное значение: ${btn.value}`);
// индекс выбранной опции
console.log(`Индекс выбранной опции: ${btn.dataset.index}`);
// выбранный текст опции
const selected = e.target.querySelector('.itc-select__option_selected');
const text = selected ? selected.textContent : '';
console.log(`Выбранный текст опции: ${text}`);
});
Кроме как использовать событие, это действие также можно выполнить с помощью метода onSelected
при создании экземпляра объекта ItcCustomSelect
:
2 способ (через метод onSelect
):
new ItcCustomSelect('#select-2', {
name: 'car',
targetValue: 'ford',
data: [['volkswagen', 'Volkswagen'], ['ford', 'Ford'], ['toyota', 'Toyota'], ['nissan', 'Nissan']],
onSelected(select, option) {
// выбранное значение
console.log(`Выбранное значение: ${itc.select.value}`);
// индекс выбранной опции
console.log(`Индекс выбранной опции: ${select.selectedIndex}`);
// выбранный текст опции
const text = option ? option.textContent : '';
console.log(`Выбранный текст опции: ${text}`);
}
});
Что внутри? Как написан ItcCustomSelect?
Компонент Select построен с использованием HTML, CSS и JavaScript. Его HTML-код мы уже рассматривали. Он имеет следующую структуру:
<div class="itc-select">
<button type="button" class="itc-select__toggle" name="car" value="" data-select="toggle" data-index="-1">Выберите из списка</button>
<div class="itc-select__dropdown">
<ul class="itc-select__options">
<li class="itc-select__option" data-select="option" data-value="volkswagen" data-index="0">Volkswagen</li>
<li class="itc-select__option elect__option_selected" data-select="option" data-value="ford" data-index="1">Ford</li>
<li class="itc-select__option" data-select="option" data-value="toyota" data-index="2">Toyota</li>
</ul>
</div>
</div>
Элемент с классом itc-select
является корневым. В нём находится вся HTML-структура селекта. Тег <button>
с классом itc-select__toggle
предназначен для отображения выбранного значения опции и переключения видимости выпадающего списка. Само выпадающее меню реализовано через элемент .itc-select__dropdown
. Оно с помощью CSS настраивается так, чтобы оно было расположено под <button>
. Список вариантов .itc-select__options
организован посредством маркированного списка. Выбранный элемент в нём отмечается посредством добавления к нему класса itc-select__option_selected
.
Этот HTML-кода не обязательно создавать вручную на странице. Имеется также автоматический вариант его создания с помощью JavaScript. В этом случае как уже было отмечено выше нужно лишь на страницу поместить пустой элемент <div>
.
Затем необходимо активировать его с помощью JavaScript как ItcCustomSelect
. На этом с HTML-кодом всё.
Классы будем использовать в CSS для добавления к элементам стилей, а data-атрибуты – в JavaScript.
CSS-код компонента Select можно посмотреть на GitHub.
Для элемента, выступающего в качестве обёртки установлено относительное позиционирование. Это необходимо для того, чтобы выпадающее меню можно было позиционировать относительно его.
.itc-select {
position: relative;
...
}
Элемент .itc-select__toggle
стилизуем как кнопку. Текущий вариант селекта будем выводить как её содержимое:
.itc-select__toggle {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0.375rem 0.75rem;
font-size: 1rem;
font-style: italic;
line-height: 1.4;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 0.3125rem;
cursor: pointer;
user-select: none;
}
Иконку к кнопке добавим через псевдоэлемент ::after
:
.itc-select__toggle::after {
flex-shrink: 0;
width: 0.75rem;
height: 0.75rem;
margin-left: 1rem;
background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" height="100" width="100"%3E%3Cpath d="M97.625 25.3l-4.813-4.89c-1.668-1.606-3.616-2.41-5.84-2.41-2.27 0-4.194.804-5.777 2.41L50 52.087 18.806 20.412C17.223 18.805 15.298 18 13.03 18c-2.225 0-4.172.804-5.84 2.41l-4.75 4.89C.813 26.95 0 28.927 0 31.23c0 2.346.814 4.301 2.439 5.865l41.784 42.428C45.764 81.174 47.689 82 50 82c2.268 0 4.215-.826 5.84-2.476l41.784-42.428c1.584-1.608 2.376-3.563 2.376-5.865 0-2.26-.792-4.236-2.375-5.932z"/%3E%3C/svg%3E');
background-size: cover;
content: "";
}
По умолчанию dropdown меню не будет показываться. Включение его отображения будем осуществлять посредством добавления к нему класса itc-select_show
:
.itc-select_show .itc-select__dropdown {
display: block;
}
При этом при показе dropdown меню иконку будем поворачивать на 180 градусов посредством CSS-трансформации:
.itc-select_show .itc-select__toggle::after {
transform: rotate(180deg);
}
CSS-код для стилизации dropdown меню:
.itc-select__dropdown {
position: absolute;
top: 2.5rem;
right: 0;
left: 0;
z-index: 2;
display: none;
max-height: 10rem;
overflow-y: auto;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 0.3125rem;
}
.itc-select__options {
margin: 0;
padding: 0;
list-style: none;
}
.itc-select__option {
padding: 0.375rem 0.75rem;
}
Стилизация при наведении на пункт меню:
.itc-select__option:hover {
background-color: #f5f5f5;
cursor: pointer;
transition: 0.2s background-color ease-in-out;
}
JavaScript код компонента доступен на GitHub. Его можно открыть, используя эту ссылку.
Код написан в виде класса ItcCustomSelect
:
class ItcCustomSelect {
...
}
Свойства и методы, которые не нужно использовать вне класса, начинаются с нижнего подчеркивания:
class ItcCustomSelect {
constructor(target, params) {
this._el = typeof target === 'string' ? document.querySelector(target) : target;
...
}
...
_onClick(e) { ... }
...
}
Подчеркивание перед именем — это общепринятое соглашение для именования свойств и методов, к которым не нужно обращаться вне класса. Они используются для реализации логики внутри класса.
Структура JavaScript кода:
class ItcCustomSelect {
// статический метод для создания HTML-кода селекта
static template(params) { }
// для закрытия открытого селекта при клике вне его
static hideOpenSelect() { }
// конструктор
constructor(target, params) { }
// обработчик события click
_onClick(e) { }
// обновляет значения атрибутов в зависимости от выбранной опции, генерирует событие 'itc.elect.change'
_updateOption(el) { }
// сбрасывает состояние, генерирует событие 'itc.select.change'
_reset() { }
// при изменении выбранной опции
_changeValue(option) { }
// включает отображение выпадающего списка
show() { }
// скрывает список с опциями
hide() { }
// переключает список с опциями
toggle() { }
// удаления слушателей события click селекта
dispose() { }
// геттер, который возвращает значение выбранной опции
get value() { }
// сеттер, который позволяет установить опцию по значению
set value(value) { }
// геттер, который возвращает индекс выбранной опции
get selectedIndex() { }
// сеттер, который позволяет выбрать опцию по её индексу
set selectedIndex(index) { }
}
ItcCustomSelect.hideOpenSelect();
Комментарии: 44
Здравия, Александр!
Это - лучший селект из всех, с которыми я работал. Он оказался даже лучше, чем я ожидал! Особенно в плане использования в адаптивном сайте. Работает абсолютно корректно при любой ширине экрана и любой длине строк в выбираемых значениях! Респект и уважуха!Я селект использую в форме обратной связи, вызываемой в модальном окне. Отсюда приходится загружать функции из файла "itc-custom-select.js" после того как будет выстроена сама форма в окне. Получается, что эти же функции загружаются при каждом вызове формы. Этих загрузок может быть несколько.
Вопрос - стоит ли беспокоится об этих повторных загрузках или лучше заморочиться на тему что бы не было повторных загрузок? Я использую ваши модальные окна.Привет! Благодарю!
Если правильно понял, то нужно просто удалить ненужные данные из памяти. В движке JavaScript этим занимается фоновый процесс «сборщик мусора».
Для этого, когда экземпляр класса
ItcCustomSelect
не будет нужен, необходимо удалить обработчики события и ссылку на этот объект:Теперь, созданный объект стал недоступен, так как нет переменных, ссылающихся на него. Сборщик мусора увидит это и автоматически удалит его.
Спасибо, крутой Селект! Использую дефолтный селект-1 только со своим оформлением. Из-за слабого знания js не могу понять как сделать активным выбор селекта передав ему Значение из ajax. Идея сделать активным выбор получив Значение из ajax (значение после обработки формы). Буду Вам признателен, но в любом случае спасибо)
Задача решена, спасибо.
Спасибо за отзыв! Пример с AJAX добавил в статью может кому-нибудь пригодится.
Спасибо!
2. На GitHub добавил статический метод create. Он проверяет наличие элемента на странице, и если он имеется, то активирует его как ItcCustomSelect:
Также немного обновил код селекта на Github и текст статьи. В рамках ребрединга переименовал его в ItcCustomSelect.
Подскажите, как добавить обработчик события по клику на выбираемый элемент выпадающего списка?
Моя задача — сделать так, чтобы при клике на выбранный элемент выпадающего списка открывался соответствующий таб. Функцию открытия соответствующего таба я написал, а вот как теперь привязать элемент списка к табу… пока не знаю…
Для этого можно использовать событие select.change:
У меня возникла небольшая проблема.
Дело в том что в моём списке используются стили и обвертки в «span». Если назначить одну из опций по умолчанию то всё ОК и спаны с классами и стилями выводятся в кнопку. Но как только выбираешь из списка другую опцию в кнопке отображается строка без «span».
Вот пример кода чтобы вам было проще меня понять
И как только я написал вопрос я нашел путь к ответу. Если вдруг кто-то столкнется с таким же вопросом то вот решение. Найди верхнюю строку и замени в ней .textContent на .innerHTML:
Только заметил что в самом вопросе не отобразились «span». они у меня были в значении «options»
Необходимо добавить ещё в начало метода _update следующую строчку (на Github обновил этот момент):
Столкнулся с несколькими проблемами.
Проект разрабатываю на Laravel.
Первая проблема — при отправке данных на бэк, данные селектов не приходят, несмотря на наличие атрибута name и значения, но эту проблему обошел путем добавления hidden input и дальнейшего добавления в него выбранных данных при помощи js.
Вторая проблема — есть форма с двумя селектами, которая заполняется пользователем поэтапно. Есть кнопки prev и next, при нажатии next блок с первым селектом (data-block=«1») скрывается и ajax-запросом получаются подкатегории выбранной в первом блоке категории, и на основании ответа во втором блоке (data-block=«2») создается новый селект с подкатегориями, после чего отображается второй блок.
html код
js код
Суть проблемы — при первом отображении второго селекта все работает, но когда нажимаю кнопку prev, скрываю второй селект, перехожу на первый, выбираю новую категорию и перехожу обратно на второй селект, html код с новыми подкатегориями формируется, НО при клике на селект не открывается дропдаун с подкатегориями, если еще раз вернусь назад, выбираю другую категорию, и перехожу снова на второй селект, дропдаун уже открывается, но при выборе нужной подкатегории он уже не закрывается.
Вот пример состоящий из 2 селектов. Может он поможет. В нём при выборе элемента из 1 селекта, второй наполняется данными посредством AJAX в зависимости от значения первого селекта.
2 — в начале скрипта вы определяете константы вне класса, вне конструктора. Это для каких целей делается?
Вот пример кода:
Буду очень благодарен за помощь. Спасибо.
Не знаю, что такое select-reset? Класс?
После того как выбрали элементы с помощью querySelectorAll, их нужно перебрать, например, с помощью forEach. Так напрямую нельзя.
у меня возникла проблема,
Мне нужно два селекта, они расположены рядом в форме, добавляю их Первым способом
все работает хорошо, кроме закрытия по клику на второй селект.
1) Кликаем на первый селект, ничего, не выбрано, кликаем, на второй селект, первый остается в открытом состоянии, но не активен при ховере, если кликнуть «оживает», получается два открытых одновременно селекта.
При этом в обратном порядке работает правильно
2) Кликаем второй селект, кликаем на первый второй закрыватся при клике на первый.
select1.selectedItem(6); // (где 6 — это 7 элемент списка).
Но у меня к сожалению, так не сработало :(
P.S. спасибо большое за реализацию.
как я понимаю, так работать не будет, так как в файле default.js в параметрах скрипта mFilter2, завязано всё на теге «a»
если изменить на тег «li», то сортировка начинает работать, но не сохраняет выбранное значение при перезагрузке страницы. Александр, если возможно расскажите пожалуйста как нужно и лучше сделать, чтобы сортировка работала и сохраняла выбранное значение при перезагрузки страницы?
Чтобы в mFilter2 изменить sort_link можно использовать параметр &filterOptions:
Установить выбранную сортировку в CustomSelect после перезагрузки страницы можно посредством добавления следующего скрипта на страницу:
Если создание нового экземпляра объекта CustomSelect происходит таким образом
то в консоли получаю следующую ошибку
На странице категории есть сортировка, сделанная через стандартный select. При выборе какой-либо сортировки происходит перезагрузка страницы.
Пытаюсь применить Ваш плагин. В итоге код получился такой
Не получается подставить выбранное значение в button.
Подскажите, пожалуйста, как это можно сделать?
Установите выбранное значение в button с помощью JavaScript:
Спасибо, Александр!