Components / Form & Input / Combobox
Combobox beta
Type to filter, arrows to choose — the Select for lists too long to scan.
- Astro
- Django
- Hugo
- Laravel
- Next.js
- Nuxt
- Rails
- Remix
- SvelteKit
- WordPress
Uses: Input — include its CSS alongside this one.
<div class="k-combo" data-combo style="max-width:300px">
<input class="input" data-combo-input type="text" role="combobox" aria-expanded="false" aria-controls="cb-list" aria-activedescendant="" placeholder="Choose a framework" value="Astro" style="width:100%;padding-right:34px">
<span class="chev"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></span>
<ul class="k-combo-list" data-combo-list id="cb-list" role="listbox" hidden>
<li role="option" id="cb-0" data-val="Astro" aria-selected="true"><span class="ck"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 12.5 5 5L19.5 6.5"/></svg></span>Astro</li><li role="option" id="cb-1" data-val="Django" aria-selected="false"><span class="ck"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 12.5 5 5L19.5 6.5"/></svg></span>Django</li><li role="option" id="cb-2" data-val="Hugo" aria-selected="false"><span class="ck"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 12.5 5 5L19.5 6.5"/></svg></span>Hugo</li><li role="option" id="cb-3" data-val="Laravel" aria-selected="false"><span class="ck"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 12.5 5 5L19.5 6.5"/></svg></span>Laravel</li><li role="option" id="cb-4" data-val="Next.js" aria-selected="false"><span class="ck"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 12.5 5 5L19.5 6.5"/></svg></span>Next.js</li><li role="option" id="cb-5" data-val="Nuxt" aria-selected="false"><span class="ck"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 12.5 5 5L19.5 6.5"/></svg></span>Nuxt</li><li role="option" id="cb-6" data-val="Rails" aria-selected="false"><span class="ck"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 12.5 5 5L19.5 6.5"/></svg></span>Rails</li><li role="option" id="cb-7" data-val="Remix" aria-selected="false"><span class="ck"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 12.5 5 5L19.5 6.5"/></svg></span>Remix</li><li role="option" id="cb-8" data-val="SvelteKit" aria-selected="false"><span class="ck"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 12.5 5 5L19.5 6.5"/></svg></span>SvelteKit</li><li role="option" id="cb-9" data-val="WordPress" aria-selected="false"><span class="ck"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 12.5 5 5L19.5 6.5"/></svg></span>WordPress</li>
<li class="none" style="display:none">No matches.</li>
</ul>
</div>.k-combo{position:relative;font-family:var(--k-font);text-align:left}
.k-combo .chev{position:absolute;right:11px;top:50%;transform:translateY(-50%);color:var(--k-ink-3);pointer-events:none;display:grid;place-items:center}
.k-combo-list{position:absolute;left:0;right:0;top:calc(100% + 6px);z-index:50;list-style:none;margin:0;padding:5px;background:var(--k-bg);border:1px solid var(--k-line);border-radius:12px;box-shadow:0 20px 44px -14px rgba(16,24,40,.22);max-height:238px;overflow:auto}
.k-combo-list[hidden]{display:none}
.k-combo-list li{padding:8px 10px;border-radius:8px;font-size:13.5px;color:var(--k-ink);cursor:pointer;display:flex;align-items:center;gap:8px}
.k-combo-list li .ck{color:var(--k-accent);visibility:hidden;display:grid;place-items:center}
.k-combo-list li.act{background:var(--k-bg-soft)}
.k-combo-list li[aria-selected="true"] .ck{visibility:visible}
.k-combo-list li.none{color:var(--k-ink-3);cursor:default}
const combo = document.querySelector('[data-combo]');
const comboInput = combo.querySelector('[data-combo-input]');
const comboList = combo.querySelector('[data-combo-list]');
const comboItems = [...comboList.querySelectorAll('[data-val]')];
let comboActive = -1;
function comboOpen() {
comboList.hidden = false;
comboInput.setAttribute('aria-expanded', 'true');
}
function comboClose() {
comboList.hidden = true;
comboInput.setAttribute('aria-expanded', 'false');
comboActive = -1;
comboItems.forEach(li => li.classList.remove('act'));
}
function comboFilter() {
const q = comboInput.value.trim().toLowerCase();
let visible = 0;
comboItems.forEach(li => {
const hit = li.dataset.val.toLowerCase().includes(q);
li.style.display = hit ? '' : 'none';
if (hit) visible++;
});
comboList.querySelector('.none').style.display = visible ? 'none' : '';
}
function comboSetActive(i) {
const vis = comboItems.filter(li => li.style.display !== 'none');
if (!vis.length) return;
comboActive = (i + vis.length) % vis.length;
comboItems.forEach(li => li.classList.remove('act'));
vis[comboActive].classList.add('act');
comboInput.setAttribute('aria-activedescendant', vis[comboActive].id);
vis[comboActive].scrollIntoView({ block: 'nearest' });
}
function comboPick(li) {
comboInput.value = li.dataset.val;
comboItems.forEach(x => x.setAttribute('aria-selected', x === li ? 'true' : 'false'));
comboClose();
}
comboInput.addEventListener('focus', () => { comboFilter(); comboOpen(); });
comboInput.addEventListener('input', () => { comboFilter(); comboOpen(); });
comboInput.addEventListener('keydown', e => {
if (e.key === 'ArrowDown') { e.preventDefault(); if (comboList.hidden) comboOpen(); comboSetActive(comboActive + 1); }
if (e.key === 'ArrowUp') { e.preventDefault(); comboSetActive(comboActive - 1); }
if (e.key === 'Enter') {
const vis = comboItems.filter(li => li.style.display !== 'none');
if (!comboList.hidden && vis[comboActive]) { e.preventDefault(); comboPick(vis[comboActive]); }
}
if (e.key === 'Escape') comboClose();
});
comboItems.forEach(li => li.addEventListener('click', () => comboPick(li)));
document.addEventListener('click', e => { if (!combo.contains(e.target)) comboClose(); });
Usage
For long option lists: frameworks, countries, customers. Under a dozen options, a Select is faster and needs no JavaScript.
Accessibility
The full ARIA combobox pattern: aria-expanded, aria-activedescendant, listbox and option roles, so screen readers announce the active option without moving focus.
Related parts
Button stable
Black leads; lines and ghosts support. Hover, disabled and loading states included.
Checkbox / Radio stable
Native inputs with a restyled face. Keyboard support and form behavior stay intact.
Color Picker beta
A curated palette first, any color second — swatches for the 95% case, a native picker for the rest.
Copy field beta
A value that exists to be copied: install commands, tokens, links. One click, confirmed in place.