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

Полная кастомизация select без использования JS

Время на прочтение 4 мин
Количество просмотров 83K
imageСколько я не мучил поисковик, а решения этого вопроса так и не нашлось. Конечно, всегда можно использовать JS и это нормально, но иногда заказчик душа просит изысков.

В заголовке я несколько приврал: всем известно, что select полностью кастомизировать нельзя, поэтому мы будем имитировать select. Сделаем мы это с помощью нескольких radio, нескольких label и одного div. Не так уж и много, правда?

Структура
<!-- Вот эта штука и заменит нам select -->
<label class="selectGeneral" placeholder="select your OS..."> <!-- Да, да, placeholder тоже поддерживается -->
    <!-- Если этот radio выбран - select открыт, если нет - select закрыт -->
    <input type="radio" name="OS">

    <!-- Это wrapper для вариантов выбора -->
    <div>
        <!-- Группа radio и есть аналог оригинальных option -->
        <input 
            type="radio"
            name="OS"
            value="linux"
            id="OS[linux]"
        >
        <!-- Аналог видимой части option -->
        <label for="OS[linux]">linux</label> 

        <input
            type="radio"
            name="OS"
            value="windows"
            id="OS[windows]"
        >
        <label for="OS[windows]">windows</label>

        <input
            type="radio"
            name="OS"
            value="other"
            id="OS[other]"
        >
        <label for="OS[other]">other</label>
    </div>
</label>


Корневая label будет всегда видимой частью нашего альтернативного select. При клике на нее будет переключаться основной radio, который и отвечает за состояния открыт/закрыт у этой конструкции. Placeholder и традиционная стрелочка будут реализованы через псевдоэлементы :before и :after у корневой label. Все остальное, кроме wrapper (тот самый единственный div), по умолчанию скрыто. Почему мы не скрываем wrapper? Потому что в нем находится выбранный элемент (если такой есть), а он должен быть виден всегда.

Основная часть
label.selectGeneral
{
    display: block;
    position: relative;
}

/** Это обещанный placeholder **/
label.selectGeneral:before
{
    content: attr(placeholder); /** Взять текст из атрибута placeholder **/
    display: inline-block;
    position: absolute;
    top: 0;
    left: 0;
    z-index: -1;

    max-width: 100%;

    text-align: left;
    white-space: nowrap; /** Не переносить слова **/

    color: #adadad;

    overflow-x: hidden; /** Скрыть лишнее **/
}

/** А это стрелочка **/
label.selectGeneral:after
{
    content: "<>";
    display: inline-block;
    position: absolute;
    top: 0;
    right: 0;

    text-align: center;

    background-color: #ffffff;

    transform: rotate(90deg);
}

label.selectGeneral input,
label.selectGeneral label
{
    display: none;
}

label.selectGeneral div
{
    min-width: 100%;
    max-height: 500px; /** Ограничения на высоту списка выборов **/

    overflow-x: hidden;
}


Осталось добавить немного магии — реализовать поведение всего этого добра. Магия будет основана на соседних селекторах и :checked у radio. Выбранный элемент виден всегда и при закрытом состоянии select перекрывает собой placeholder. При открытии select, показываются все остальные элементы для выбора, а wrapper, в который они вложены, немного съезжает вниз, что-бы было видно placeholder и пользователь не забыл, что же он, собственно, выбирает.

Поведение
/** Если наш альтернативный select открыт, то wrapper **/
label.selectGeneral input[type="radio"]:checked ~ div
{
    position: absolute; /** приобретает абсолютную позицию **/
    top: <высота label.selectGeneral>; /** и смешается немного вниз, открывая placeholder **/

    overflow-y: auto;
}

/** Все label внутри wrapper'а при открытом select **/
label.selectGeneral input[type="radio"]:checked ~ div > label,
/** И выбранный вариант **/
label.selectGeneral input[type="radio"]:checked + label
{
    display: block; /** должны быть видимыми **/
}

/** Подсветим элемент на который наведена мышь при открытом select **/
label.selectGeneral input[type="radio"]:checked ~ div > label:hover
{
    background-color: #ffa834;
}

/** При закрытом select, нужно делегировать событие клика мышью с выбранного элемента родительскому label **/
label.selectGeneral input[type="radio"]:not(:checked) ~ div > input[type="radio"]:checked + label
{
    position: relative;
    z-index: -1; 
}


В конце применен трюк с z-index, который позволяет расположить дочерний элемент ниже (глубже по z-оси) родительского. Этот замечательный факт позволяет делегировать реакцию на клик по выбранному элементу нашему select'у, что бы он раскрылся.

Рабочий пример можно лицезреть тут.

Из плюсов подхода можно отметить:
  • Кроссбраузерность — это работает везде, где работают label
  • Относительная легкость — код не перенасыщен лишними элементами
  • Возможность полной кастомизацией — стилизуется каждая мелочь
  • Гибкость — не придется дописывать новых стилей при добавлении пунктов выбора

Конечно же, есть и минусы, куда же без них:
  • Отсутствие деградации — если не поддерживается, то стандартный select не спасет ситуацию (как подсказывают комментаторы — очень важный пункт для портативных устройств)
  • Легкость легкостью, а дополнительный код все таки будет
  • Невалидный код — div внутри label и атрибут placeholder у нее же, это не по стандарту
  • Это не совсем минус, но эта штука не захлопывается сама при клике в любом месте экрана


Не думаю, что кто-то станет использовать это в production, но подход явно имеет право на жизнь.

UDPATE: Изменен код — добавлена функция автоматического закрытия select при выборе.
Теги:
Хабы:
+33
Комментарии 90
Комментарии Комментарии 90

Публикации

Истории

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

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн