SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/custom-selects

8 CSS Custom Selects

4 Pure CSS4 with JSPublished 2026-10-04Free · no sign-up

Eight select menus, from the most robust styled native select to a fully custom listbox: an icon tile beside a native select, a tag-based multi-select, a compact pill, grouped options, a type-to-filter datalist and the new customisable select using appearance: base-select.

01. Styled Native Select

FormNative

The most robust custom select: a real select element with appearance: none, a custom border and an SVG chevron as a background image. It keeps the native option list, keyboard behaviour and mobile pickers, which a fully custom dropdown cannot match.

View code
<div class="ns">
  <label for="ns-s">Country</label>
  <select id="ns-s"><option>Pakistan</option><option>United Kingdom</option><option>United States</option><option>Germany</option></select>
</div>
.ns { width: min(300px, 86vw); font-family: Inter, system-ui, sans-serif; }
.ns label { display: block; margin-bottom: 6px; color: #344054; font-weight: 600; font-size: 14px; }
.ns select { appearance: none; width: 100%; padding: 11px 40px 11px 12px; border: 1.5px solid #d0d5dd; border-radius: 10px; background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 7.5l5 5 5-5' fill='none' stroke='%23475467' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 12px center / 18px no-repeat; color: #101828; font: 15px Inter, sans-serif; cursor: pointer; }
.ns select:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,.15); }

02. Custom Listbox Dropdown

ARIAJavaScript

A fully custom select following the ARIA select-only combobox pattern: the button shows the choice, the list opens below with role="listbox", and arrow keys, Enter, Escape and type-to-jump all work. Use it only when a native select cannot do the job.

View code
<div class="lb">
  <span id="lb-l">Sort by</span>
  <button id="lb-b" role="combobox" aria-labelledby="lb-l lb-b" aria-haspopup="listbox" aria-expanded="false" aria-controls="lb-o">Newest</button>
  <ul id="lb-o" role="listbox" aria-labelledby="lb-l" hidden>
    <li role="option" aria-selected="true">Newest</li><li role="option">Price, low to high</li><li role="option">Price, high to low</li><li role="option">Best rated</li>
  </ul>
</div>
.lb { position: relative; width: min(260px, 86vw); font-family: Inter, system-ui, sans-serif; color: #c9d1e2; }
#lb-l { display: block; margin-bottom: 6px; font-weight: 600; font-size: 14px; }
#lb-b { display: flex; justify-content: space-between; align-items: center; width: 100%; padding: 11px 12px; border: 1px solid #343c51; border-radius: 10px; background: #151a26; color: #fff; font: 15px Inter, sans-serif; text-align: left; cursor: pointer; }
#lb-b::after { content: ""; width: 7px; height: 7px; border: solid #9aa4b8; border-width: 0 2px 2px 0; transform: rotate(45deg) translateY(-2px); }
#lb-b:focus-visible { outline: 2px solid #7c9cff; outline-offset: 2px; }
#lb-o { position: absolute; left: 0; right: 0; z-index: 2; margin: 6px 0 0; padding: 6px; list-style: none; border: 1px solid #2c3346; border-radius: 12px; background: #12161f; }
#lb-o li { padding: 9px 10px; border-radius: 8px; font-size: 14px; cursor: pointer; }
#lb-o li.act { background: #1e2433; color: #fff; }
#lb-o li[aria-selected=true]::after { content: " \2713"; color: #7c9cff; }
const btn = document.getElementById('lb-b'), list = document.getElementById('lb-o'), opts = [...list.children];
let act = 0;
function open(o) { list.hidden = !o; btn.setAttribute('aria-expanded', String(o)); if (o) mark(opts.findIndex(x => x.getAttribute('aria-selected') === 'true')); }
function mark(i) { act = (i + opts.length) % opts.length; opts.forEach((o, k) => { o.classList.toggle('act', k === act); o.id = 'lbo' + k; }); btn.setAttribute('aria-activedescendant', 'lbo' + act); }
function pick(i) { opts.forEach((o, k) => o.setAttribute('aria-selected', String(k === i))); btn.textContent = opts[i].textContent; open(false); btn.removeAttribute('aria-activedescendant'); }
btn.addEventListener('click', () => open(list.hidden));
btn.addEventListener('keydown', e => {
  if (list.hidden && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); open(true); return; }
  if (list.hidden) return;
  if (e.key === 'ArrowDown') { e.preventDefault(); mark(act + 1); }
  else if (e.key === 'ArrowUp') { e.preventDefault(); mark(act - 1); }
  else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(act); }
  else if (e.key === 'Escape') { open(false); }
  else if (e.key.length === 1) { const i = opts.findIndex(o => o.textContent.toLowerCase().startsWith(e.key.toLowerCase())); if (i > -1) mark(i); }
});
opts.forEach((o, i) => o.addEventListener('click', () => { pick(i); btn.focus(); }));
document.addEventListener('click', e => { if (!e.target.closest('.lb')) open(false); });

03. Select with Flag Icons

IconsNative

A currency picker that shows a symbol tile beside a native select. The tile is updated from the change event, so the select keeps all native behaviour while the visible design includes an icon, a trick that avoids rebuilding the dropdown.

View code
<div class="ci">
  <label for="ci-s">Currency</label>
  <div class="wrap"><span class="sym" id="ci-y" aria-hidden="true">$</span>
    <select id="ci-s"><option value="$">US dollar (USD)</option><option value="&pound;">British pound (GBP)</option><option value="&euro;">Euro (EUR)</option><option value="Rs">Pakistani rupee (PKR)</option></select>
  </div>
</div>
.ci { width: min(320px, 88vw); font-family: Inter, system-ui, sans-serif; }
.ci label { display: block; margin-bottom: 6px; color: #344054; font-weight: 600; font-size: 14px; }
.wrap { display: flex; align-items: center; border: 1.5px solid #d0d5dd; border-radius: 10px; background: #fff; }
.wrap:focus-within { border-color: #0f766e; box-shadow: 0 0 0 3px rgba(15,118,110,.15); }
.sym { display: grid; place-items: center; min-width: 34px; height: 30px; margin-left: 6px; border-radius: 7px; background: #ccfbf1; color: #115e59; font-weight: 800; font-size: 14px; }
.wrap select { appearance: none; flex: 1; padding: 11px 36px 11px 10px; border: 0; background: transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 7.5l5 5 5-5' fill='none' stroke='%23475467' stroke-width='2'/%3E%3C/svg%3E") right 10px center / 16px no-repeat; color: #101828; font: 15px Inter, sans-serif; outline: none; cursor: pointer; }
const cs = document.getElementById('ci-s');
cs.addEventListener('change', () => { document.getElementById('ci-y').innerHTML = cs.value; });

04. Multi-Select Tags

Multi-selectChips

A multi-select presented as removable tags above a list of checkboxes. Ticking a box adds a tag, each tag's remove button unticks it, and the remove buttons are named Remove Linen and so on, so the control works fully from the keyboard.

View code
<fieldset class="ms">
  <legend>Materials</legend>
  <div class="tags" aria-live="polite"></div>
  <div class="opts">
    <label><input type="checkbox" value="Cotton" checked> Cotton</label>
    <label><input type="checkbox" value="Linen" checked> Linen</label>
    <label><input type="checkbox" value="Wool"> Wool</label>
    <label><input type="checkbox" value="Silk"> Silk</label>
  </div>
</fieldset>
.ms { width: min(340px, 90vw); margin: 0; padding: 0; border: 0; font-family: Inter, system-ui, sans-serif; color: #c9d1e2; }
.ms legend { margin-bottom: 8px; font-weight: 700; }
.tags { display: flex; flex-wrap: wrap; gap: 6px; min-height: 34px; padding: 6px; border: 1px solid #343c51; border-radius: 10px; background: #151a26; }
.tags span { display: inline-flex; align-items: center; gap: 4px; padding: 3px 4px 3px 10px; border-radius: 999px; background: #2b3a6b; color: #fff; font-size: 13px; }
.tags button { width: 22px; height: 22px; border: 0; border-radius: 50%; background: rgba(255,255,255,.15); color: #fff; cursor: pointer; }
.tags button:focus-visible { outline: 2px solid #fff; }
.opts { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; font-size: 14px; }
.opts input { accent-color: #7c9cff; }
const tags = document.querySelector('.tags'), boxes = [...document.querySelectorAll('.opts input')];
function draw() {
  tags.innerHTML = '';
  boxes.filter(b => b.checked).forEach(b => {
    const s = document.createElement('span'); s.textContent = b.value;
    const x = document.createElement('button'); x.type = 'button'; x.textContent = '×'; x.setAttribute('aria-label', 'Remove ' + b.value);
    x.addEventListener('click', () => { b.checked = false; draw(); b.focus(); });
    s.appendChild(x); tags.appendChild(s);
  });
}
boxes.forEach(b => b.addEventListener('change', draw));
draw();

05. Pill Button Select

CompactNative

A small inline select styled as a pill, for sort and filter bars where a full-width field would be too heavy. The visible text is the label plus the current value, and the native select sits on top invisibly so the whole pill opens the menu.

View code
<label class="pill">
  <span>Show: <b id="pill-v">All orders</b></span>
  <select id="pill-s" aria-label="Show orders"><option>All orders</option><option>Unfulfilled</option><option>Paid</option><option>Refunded</option></select>
</label>
.pill { position: relative; display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; border: 1.5px solid #d0d5dd; border-radius: 999px; background: #fff; color: #475467; font: 500 14px Inter, system-ui, sans-serif; cursor: pointer; }
.pill b { color: #101828; }
.pill::after { content: ""; width: 6px; height: 6px; border: solid #475467; border-width: 0 1.5px 1.5px 0; transform: rotate(45deg) translateY(-2px); }
.pill select { position: absolute; inset: 0; width: 100%; opacity: 0; cursor: pointer; }
.pill:focus-within { border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,.15); }
const ps = document.getElementById('pill-s');
ps.addEventListener('change', () => document.getElementById('pill-v').textContent = ps.value);

06. Grouped Options Select

OptgroupNative

A native select with optgroup headings to organise a long list, here shipping services grouped by carrier. Group labels are announced by screen readers and cannot be selected, and the dark styling uses color-scheme so the open menu matches.

View code
<div class="gs">
  <label for="gs-s">Shipping service</label>
  <select id="gs-s">
    <optgroup label="Standard"><option>Postal, 3 to 5 days</option><option>Courier, 2 to 3 days</option></optgroup>
    <optgroup label="Express"><option>Next day</option><option>Same day (city only)</option></optgroup>
    <optgroup label="Collection"><option>Pick up in store</option></optgroup>
  </select>
</div>
.gs { width: min(320px, 88vw); font-family: Inter, system-ui, sans-serif; color-scheme: dark; }
.gs label { display: block; margin-bottom: 6px; color: #c9d1e2; font-weight: 600; font-size: 14px; }
.gs select { width: 100%; padding: 11px 12px; border: 1px solid #343c51; border-radius: 10px; background: #151a26; color: #fff; font: 15px Inter, sans-serif; }
.gs select:focus { outline: none; border-color: #fbbf24; box-shadow: 0 0 0 3px rgba(251,191,36,.2); }

07. Searchable Select with Datalist

DatalistSearch

A text input with a datalist, which gives a type-to-filter suggestion list using only HTML. It suits long lists such as cities or job titles, people can still type a value that is not on the list, and the browser draws an accessible suggestion menu.

View code
<div class="dl">
  <label for="dl-i">Job title</label>
  <input id="dl-i" list="dl-l" placeholder="Start typing">
  <datalist id="dl-l"><option value="Product designer"><option value="Product manager"><option value="Frontend developer"><option value="Backend developer"><option value="Data analyst"><option value="Marketing lead"></datalist>
  <small>Pick a suggestion or type your own.</small>
</div>
.dl { display: grid; gap: 6px; width: min(320px, 88vw); font-family: Inter, system-ui, sans-serif; }
.dl label { color: #344054; font-weight: 600; font-size: 14px; }
.dl input { padding: 11px 12px; border: 1.5px solid #d0d5dd; border-radius: 10px; color: #101828; font: 15px Inter, sans-serif; outline: none; }
.dl input:focus { border-color: #7c3aed; box-shadow: 0 0 0 3px rgba(124,58,237,.15); }
.dl small { color: #5d6679; font-size: 13px; }

08. Customisable Select with appearance: base-select

ModernExperimental

The new appearance: base-select lets you style a real select's button and option list with CSS, including icons in options, while keeping native behaviour. Browsers without support fall back to an ordinary select, so it is safe to use today.

View code
<div class="bs">
  <label for="bs-s">Status</label>
  <select id="bs-s">
    <option value="todo">&#9675; To do</option>
    <option value="doing" selected>&#9680; In progress</option>
    <option value="done">&#9679; Done</option>
  </select>
</div>
.bs { width: min(260px, 86vw); font-family: Inter, system-ui, sans-serif; color-scheme: dark; }
.bs label { display: block; margin-bottom: 6px; color: #c9d1e2; font-weight: 600; font-size: 14px; }
.bs select { width: 100%; padding: 10px 12px; border: 1px solid #343c51; border-radius: 10px; background: #151a26; color: #fff; font: 15px Inter, sans-serif; }
.bs select:focus-visible { outline: 2px solid #22d3ee; outline-offset: 2px; }
@supports (appearance: base-select) {
  .bs select, .bs select::picker(select) { appearance: base-select; }
  .bs select::picker(select) { margin-top: 6px; padding: 6px; border: 1px solid #2c3346; border-radius: 12px; background: #12161f; }
  .bs option { padding: 8px 10px; border-radius: 8px; color: #e7ebf5; }
  .bs option:hover, .bs option:focus-visible { background: #1e2433; }
  .bs option:checked { color: #22d3ee; }
}

About these CSS Custom Selects

Start with the native select

A real <select> brings keyboard support, screen reader support and the phone's own picker. With appearance: none and a background chevron you can match your design while keeping all of that.

When to build your own

Build a custom listbox only when the options need rich content. Then follow the ARIA select-only combobox pattern: a button with role="combobox", a role="listbox" list, aria-activedescendant, and full keyboard support including type-to-jump.

The new customisable select

appearance: base-select lets you style a real select's button and option list, including the open picker through ::picker(select). Wrap the styles in @supports, and older browsers simply show a normal select.

Long lists

For long lists such as countries, a datalist gives type-to-filter suggestions with no script, and optgroups organise options into labelled sections.

Frequently asked questions

Is a custom dropdown accessible?

Only if it implements the full ARIA pattern. A styled native select is accessible by default.

Can a select hold icons?

Not in most browsers yet. Use base-select where supported, or show an icon beside the select as in the icon demo.

Which demos need JavaScript?

The listbox, icon, multi-select and pill demos. The others are HTML and CSS.