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

в 12:58, , рубрики: css, html, веб-дизайн, извращения, интерфейсы, кастомизация, КодоБред

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

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

Структура

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

    <!-- Это 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. При клике на нее будет переключаться checkbox, который и отвечает за состояния открыт/закрыт у этой конструкции. 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/checkbox. Выбранный элемент виден всегда и при закрытом состоянии select перекрывает собой placeholder. При открытии select, показываются все остальные элементы для выбора, а wrapper, в который они вложены, немного съезжает вниз, что-бы было видно placeholder и пользователь не забыл, что же он, собственно, выбирает.

Поведение

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

    overflow-y: auto;
}

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

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

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

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

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

Из плюсов подхода можно отметить:

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

Конечно же, есть и минусы, куда же без них:

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

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

Автор: Cyapa

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js