Creare select personalizzate (custom) con CSS e JavaScript

In questo articolo vedremo come realizzare una select personalizzata (custom) con i CSS e JavaScript. Si tratta di una soluzione che rimedia al problema della gestione degli elementi select con i soli CSS.

Si tratta di trasformare questa struttura di partenza:

<select class="custom-select" name="custom-select">
    <option value="1">Option #1</option>
    <option value="2">Option #2</option>
    <option value="3">Option #3</option>
</select>

In questa struttura finale:

<select class="custom-select hidden" name="custom-select" data-id="select-tliumuqy8a">
    <option value="1">Option #1</option>
    <option value="2">Option #2</option>
    <option value="3">Option #3</option>
</select>
<div class="custom-select-wrapper" data-select="select-tliumuqy8a">
    <button type="button" class="custom-select-toggle">
        Choose an option
    </button>
        <ul>
            <li data-value="1">Option #1</li>
            <li data-value="2">Option #2</li>
            <li data-value="3">Option #3</li>
        </ul>
</div>

Come si può notare, il menu personalizzato che andremo a creare è collegato all'elemento select tramite attributi di dati che contengono un ID casuale generato dinamicamente. L'elemento select verrà nascosto in modo accessibile tramite una classe CSS specializzata.

Definiamo quindi il seguente codice JavaScript:

function createSelectRandomID() {
        return 'select-' + Math.random().toString(36).slice(2);
    }
    function setUpSelect() {
        const select = document.querySelectorAll('.custom-select');
        if(select.length === 0) {
            return;
        }
        for(const sel of select) {
            const parent = sel.parentElement;
            const id = createSelectRandomID();
            sel.classList.add('hidden');
            sel.setAttribute('data-id', id);

            const menu = document.createElement('div');
            menu.className = 'custom-select-wrapper';
            menu.setAttribute('data-select', id);

            const btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'custom-select-toggle';
            btn.innerText = 'Choose an option';
            menu.appendChild(btn);

            const ul = document.createElement('ul');

            const options = sel.querySelectorAll('option');
            let items = '';

            for(const option of options) {
                const value = option.getAttribute('value');
                const text = option.innerText;
                if(!value) {
                    continue;
                }
                items += `
                    <li data-value="${value}">
                        ${text}
                    </li>
                `;

            }
            ul.innerHTML = items;

            menu.appendChild(ul);
            parent.appendChild(menu);
        }

        
    }

A questo punto possiamo definire gli stili CSS principali:

.hidden {
    position: absolute;
    top: -9999em;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.custom-select-wrapper {
    display: inline-block;
    position: relative;
    min-width: 145px;
}

.custom-select-toggle {
    padding: 0.4rem;
    border: 1px solid #ddd;
    background-color: transparent;
    cursor: pointer;
    font-size: 1rem;
    display: block;
    width: 100%;
}

.custom-select-wrapper ul {
    margin: 0;
    padding: 0;
    list-style: none;
    border-style: solid;
    border-color: #ddd;
    border-width:  0 1px 1px 1px;
    position: absolute;
    top: auto;
    left: 0;
    width: 100%;
    display: none;
}

.custom-select-wrapper ul.visible {
    display: block;
}

.custom-select-wrapper ul li {
    display: block;
    padding: 0.4rem;
    cursor: pointer;
}

.custom-select-wrapper ul li:hover {
    background-color: #eee;
}

Quando l'utente cliccherà sul pulsante, verrà mostrato il menu personalizzato. Cliccando su ciascuna voce del menu, verrà impostato il valore dell'elemento select sul valore della voce corrente (salvato nell'attributo di dati data-value) e nascosto di nuovo il menu. Il testo del pulsante verrà impostato sul testo della voce corrente. Ecco il codice JavaScript che implementerà questa logica:

function handleSelect() {
         const toggles = document.querySelectorAll('.custom-select-toggle');
         if(toggles.length === 0) {
            return;
         }   
         for(const toggle of toggles) {
            const wrapper = toggle.parentElement;
            const select = document.querySelector('[data-id="' + wrapper.dataset.select + '"]');
            if(!select) {
                continue;
            }
            const ul = toggle.nextElementSibling;
            if(!ul) {
                continue;
            }
            const items = ul.querySelectorAll('li');
            if(items.length === 0) {
                continue;
            }

            toggle.addEventListener('click', () => {
                ul.classList.toggle('visible');
            });

            for(const item of items) {
                item.addEventListener('click', () => {
                    select.value = item.dataset.value;
                    toggle.innerText = item.innerText;
                    ul.classList.remove('visible');
                });
            }
         }
    }

Possiamo inizializzare il codice JavaScript come segue:

document.addEventListener('DOMContentLoaded', () => {
    setUpSelect();
    handleSelect();
});

Demo

Custom Select

Conclusione

La soluzione presentata offre un rimedio temporaneo al problema del layout degli elementi select con i CSS che, ricordiamolo, al momento possono essere gestiti solo in modo parziale.

Torna su