Custom Select with Animated Dropdown Using CSS

7 min read
The standard select element has traditionally been one of the most difficult form controls to customize.

You could change the font, background, border, and appearance of the field itself, but the dropdown menu was still largely rendered by the browser or operating system.

Because of this, fully custom selects were often built using div, ul, and JavaScript.

This approach has a major disadvantage: the developer has to manually implement keyboard controls, focus management, form behavior, and accessibility.

Modern CSS is gradually solving this problem. New customization features are now available for a regular native <select>:

appearance: base-select — enables advanced select styling;
::picker(select) — targets the dropdown picker;
::picker-icon — allows styling of the arrow icon;
:open — targets the open state of the select;
:checked — allows styling of the selected option;
::checkmark — allows customization of the selected-item marker.

The element itself remains a standard HTML select.

HTML markup

Let us start with a regular native select:


<label for="project-type">Project type</label>

<select
    class="custom-select"
    id="project-type"
    name="project_type"
>
    <option value="" selected disabled>
        Select an option
    </option>

    <option value="website">
        Website development
    </option>

    <option value="support">
        Website support
    </option>

    <option value="integration">
        Integration
    </option>

    <option value="design">
        Design
    </option>
</select>

This is still a regular form control.

If the browser does not support the new CSS features, the user will simply see a standard working select.

Basic styles

First, we will style the element so that it still looks acceptable even without support for the new customization features:


.custom-select {
    width: 280px;
    min-height: 44px;
    padding: 10px 14px;

    font: inherit;
    color: #222;

    background-color: #fff;

    border: 1px solid #d5d5d5;
    border-radius: 8px;

    cursor: pointer;

    transition:
        border-color 0.2s ease,
        box-shadow 0.2s ease;
}

.custom-select:hover {
    border-color: #999;
}

.custom-select:focus {
    outline: none;

    border-color: #666;

    box-shadow:
        0 0 0 3px rgba(0, 0, 0, 0.08);
}

These styles are applied to the regular select as well, so the basic appearance remains consistent across modern browsers.

Enabling the customizable select mode

Advanced styling is best enabled inside an @supports rule.

This allows browsers that do not yet support base-select to simply ignore the additional styling:


@supports (appearance: base-select) {

    .custom-select,
    .custom-select::picker(select) {
        appearance: base-select;
    }

}

It is important to apply appearance: base-select both to the select itself and to its dropdown picker.

Styling the dropdown list

Once the new mode is enabled, we can target the dropdown part of the select directly:


@supports (appearance: base-select) {

    .custom-select::picker(select) {
        margin-top: 6px;

        background: #fff;

        border: 1px solid #d5d5d5;
        border-radius: 8px;

        box-shadow:
            0 10px 30px rgba(0, 0, 0, 0.12);

        overflow: hidden;
    }

}

Unlike the older approach, there is no need to create a separate ul element.

Styling options

Each option can also be styled individually:


@supports (appearance: base-select) {

    .custom-select option {
        padding: 11px 14px;

        color: #222;
        background: #fff;

        cursor: pointer;
    }

    .custom-select option:hover,
    .custom-select option:focus {
        background: #f5f5f5;
    }

    .custom-select option:checked {
        font-weight: 600;

        background: #eeeeee;
    }

}

This makes it possible to customize spacing, background colors, and the appearance of the selected option.

Animating the arrow icon

The built-in arrow icon can be styled using the ::picker-icon pseudo-element.

Let us add a smooth rotation when the dropdown is opened:


@supports (appearance: base-select) {

    .custom-select::picker-icon {
        color: #777;

        transition:
            rotate 0.25s ease;
    }

    .custom-select:open::picker-icon {
        rotate: 180deg;
    }

}

The :open pseudo-class applies while the list of options is open.

Animating the dropdown

Now we can animate the picker itself.

For example, we can combine a fade effect with a slight upward offset:


@supports (appearance: base-select) {

    .custom-select::picker(select) {
        opacity: 0;

        translate: 0 -8px;

        transition:
            opacity 0.2s ease,
            translate 0.2s ease,
            display 0.2s allow-discrete,
            overlay 0.2s allow-discrete;
    }

    .custom-select:open::picker(select) {
        opacity: 1;

        translate: 0 0;
    }

    @starting-style {

        .custom-select:open::picker(select) {
            opacity: 0;

            translate: 0 -8px;
        }

    }

}

Several modern CSS features are used here.

opacity controls the fade effect;
translate creates the slide-down motion;
allow-discrete makes transitions involving discrete properties possible;
@starting-style defines the initial state when the picker appears.

Why display and overlay are included

The dropdown of a modern customizable select behaves similarly to a popover and is displayed in the browser's top layer.

When the picker closes, properties such as display and overlay also change.

A regular CSS transition cannot interpolate these values smoothly, so the following value is used:


allow-discrete

This allows the browser to delay the final hiding of the picker until the visual animation has completed.

Complete CSS

The final styles look like this:


.custom-select {
    width: 280px;
    min-height: 44px;
    padding: 10px 14px;

    font: inherit;
    color: #222;

    background-color: #fff;

    border: 1px solid #d5d5d5;
    border-radius: 8px;

    cursor: pointer;

    transition:
        border-color 0.2s ease,
        box-shadow 0.2s ease;
}

.custom-select:hover {
    border-color: #999;
}

.custom-select:focus {
    outline: none;

    border-color: #666;

    box-shadow:
        0 0 0 3px rgba(0, 0, 0, 0.08);
}


@supports (appearance: base-select) {

    .custom-select,
    .custom-select::picker(select) {
        appearance: base-select;
    }


    .custom-select::picker(select) {
        margin-top: 6px;

        background: #fff;

        border: 1px solid #d5d5d5;
        border-radius: 8px;

        box-shadow:
            0 10px 30px rgba(0, 0, 0, 0.12);

        overflow: hidden;

        opacity: 0;

        translate: 0 -8px;

        transition:
            opacity 0.2s ease,
            translate 0.2s ease,
            display 0.2s allow-discrete,
            overlay 0.2s allow-discrete;
    }


    .custom-select:open::picker(select) {
        opacity: 1;

        translate: 0 0;
    }


    @starting-style {

        .custom-select:open::picker(select) {
            opacity: 0;

            translate: 0 -8px;
        }

    }


    .custom-select::picker-icon {
        color: #777;

        transition:
            rotate 0.25s ease;
    }


    .custom-select:open::picker-icon {
        rotate: 180deg;
    }


    .custom-select option {
        padding: 11px 14px;

        color: #222;
        background: #fff;

        cursor: pointer;
    }


    .custom-select option:hover,
    .custom-select option:focus {
        background: #f5f5f5;
    }


    .custom-select option:checked {
        font-weight: 600;

        background: #eeeeee;
    }

}

Complete HTML

The markup remains very simple:


<label for="project-type">
    Project type
</label>

<select
    class="custom-select"
    id="project-type"
    name="project_type"
>
    <option value="" selected disabled>
        Select an option
    </option>

    <option value="website">
        Website development
    </option>

    <option value="support">
        Website support
    </option>

    <option value="integration">
        Integration
    </option>

    <option value="design">
        Design
    </option>
</select>

No JavaScript is required to open, close, or select an option.

Getting the selected value with JavaScript

If the selected value needs to be used in JavaScript, it works exactly like a regular select:


const select = document.querySelector('.custom-select');

select.addEventListener('change', () => {
    console.log('Selected:', select.value);
});

No additional synchronization between the visual control and the actual form field is required.

Browser support

The new approach has one important limitation: fully customizable selects are not yet supported by every browser.

This is why all styles related to appearance: base-select are placed inside:


@supports (appearance: base-select) {
    /* Advanced styling */
}

If the browser supports the feature, the user will see the custom animated dropdown.

If it does not, the select remains a standard native control.

The form continues to work normally in both cases.

Why you should not simply replace select with div

A common older implementation looks like this:


<div class="custom-select">
    <div class="select-header">
        Select an option
    </div>

    <ul>
        <li>Option 1</li>
        <li>Option 2</li>
    </ul>
</div>

Visually, such a component is easy to build.

However, a div is not a native form control and does not behave like a select.

The developer must additionally implement:

Enter, Escape, and arrow key handling;
focus management;
ARIA roles and states;
keyboard selection;
synchronization with the real form value;
disabled-state handling;
dropdown closing behavior;
screen reader compatibility.

For a standard single-choice field, keeping the native select is usually the better option.

When a fully custom component is still necessary

A JavaScript-based custom component may still be necessary if the dropdown must contain complex interactive elements, search, images, dynamic filtering, or non-standard business logic.

In that case, it is no longer just a styled select, but a full combobox or listbox component, and keyboard navigation and accessibility must be implemented separately.

Summary

Modern CSS makes it possible to build a custom animated select without replacing the native form element with div and ul elements.

The main features used are:

appearance: base-select — enables customizable select rendering;
::picker(select) — styles the dropdown;
:open — targets the open state;
::picker-icon — animates the arrow;
@starting-style and allow-discrete — provide smooth opening and closing transitions.


The main advantage of this approach is progressive enhancement.


In supporting browsers, the select receives custom styling and animation, while in other browsers it remains a standard, accessible, and fully functional form control.
Back to blog