Create a custom select with CSS and JavaScript

In this article we’ll see how to build a custom select element with CSS and JavaScript. This is a solution that fixes the problem of handling select elements only with CSS.

We start from this basic structure:

<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>

And we transform it into this final structure:

<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>

As you can see, the custom menu we’re going to create is linked to the select element via data attributes containing a dynamically generated random ID. The select element will be hidden in an accessible way using a specialized CSS class.

Let’s define the following JavaScript code:

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);
        }

        
    }

Now we can define the main CSS styles:

.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;
}

When the user clicks on the button, the custom menu will be displayed. Clicking on each menu item will set the select element’s value to the current item’s value (stored in the data-value attribute) and hide the menu again. The button text will be set to the current item’s text. Here’s the JavaScript code that implements this logic:

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');
                });
            }
         }
    }

We can initialize the JavaScript code as follows:

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

Demo

Custom Select

Conclusion

The solution presented here provides a temporary workaround for styling select elements with CSS, which, let’s remember, can currently only be styled partially.

Back to top