Skip to content

Instantly share code, notes, and snippets.

@maxsei
Last active July 3, 2026 18:08
Show Gist options
  • Select an option

  • Save maxsei/d6713933e13331c27730b9c9b1aa55e4 to your computer and use it in GitHub Desktop.

Select an option

Save maxsei/d6713933e13331c27730b9c9b1aa55e4 to your computer and use it in GitHub Desktop.
editable select box
<html data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Combobox</title>
<link
rel="stylesheet"
href="https://unpkg.com/open-props@1.7.23/normalize.min.css"
/>
<link
rel="stylesheet"
href="https://unpkg.com/open-props@1.7.23/theme.light.switch.min.css"
/>
<link
rel="stylesheet"
href="https://unpkg.com/open-props@1.7.23/open-props.min.css"
/>
<script
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.2/bundles/datastar.js"
type="module"
integrity="sha384-SnyFlWTdFL3c8+9/1WsPuMFBq6AQOGC1LmS9upY4YkM3En3wZr5q2UvydHaMgOVG"
crossorigin="anonymous"
></script>
<style>
body {
display: grid;
place-items: center;
min-height: 100dvh;
padding: var(--size-fluid-3);
}
form {
width: min(100%, 22rem);
padding: var(--size-fluid-3);
}
label {
display: block;
font-weight: var(--font-weight-6);
margin-bottom: var(--size-2);
}
/* --- combobox --- */
.combobox {
position: relative;
}
.combobox input[type="text"] {
width: 100%;
box-sizing: border-box;
padding-inline-end: var(--size-8);
border: var(--border-size-1) solid var(--gray-5);
border-radius: var(--radius-2);
font-size: var(--font-size-2);
}
.combobox input[type="text"]:focus-visible {
outline: var(--border-size-2) solid var(--blue-6);
outline-offset: 2px;
}
.combobox .trigger {
position: absolute;
inset-block: 0;
inset-inline-end: 0;
inline-size: var(--size-8);
display: grid;
place-items: center;
background: transparent;
border: none;
cursor: pointer;
color: var(--gray-7);
}
.combobox .trigger:focus-visible {
outline: var(--border-size-2) solid var(--blue-6);
outline-offset: -2px;
border-radius: var(--radius-1);
}
.combobox .trigger svg {
transition: rotate var(--ease-3) var(--ease-out-3);
}
.combobox .trigger[aria-expanded="true"] svg {
rotate: 180deg;
}
/* popover listbox, anchored to the combobox */
[popover] {
margin: 0;
padding: var(--size-1);
border: var(--border-size-1) solid var(--gray-5);
border-radius: var(--radius-2);
box-shadow: var(--shadow-3);
background: var(--surface-1);
width: anchor-size(width);
position-anchor: --combo;
position-area: bottom span-right;
margin-block-start: var(--size-1);
max-height: min(16rem, 40dvh);
overflow-y: auto;
}
/* fallback positioning for browsers without anchor positioning */
@supports not (position-area: bottom span-right) {
[popover] {
position: fixed;
}
}
.combobox .input-wrap {
anchor-name: --combo;
}
[role="option"] {
padding: var(--size-2) var(--size-3);
border-radius: var(--radius-1);
cursor: pointer;
font-size: var(--font-size-2);
list-style: none;
}
[role="option"]:hover,
[role="option"][data-active="true"] {
background: var(--blue-1);
}
[role="option"][aria-selected="true"] {
font-weight: var(--font-weight-6);
}
[role="option"][aria-selected="true"]::after {
content: "✓";
float: inline-end;
color: var(--blue-7);
}
ul[role="listbox"] {
margin: 0;
padding: 0;
}
.hint {
font-size: var(--font-size-1);
color: var(--gray-7);
margin-block-start: var(--size-2);
}
button[type="submit"] {
margin-block-start: var(--size-4);
padding: var(--size-2) var(--size-4);
border-radius: var(--radius-2);
border: none;
background: var(--blue-6);
color: white;
font-weight: var(--font-weight-6);
cursor: pointer;
}
</style>
</head>
<body>
<form data-signals="{pet: 'Dog', activeIndex: -1}">
<label for="pet-input">Choose or type a pet</label>
<div class="combobox">
<span class="input-wrap">
<input
id="pet-input"
type="text"
name="pet"
role="combobox"
aria-expanded="false"
aria-controls="pet-listbox"
aria-autocomplete="none"
autocomplete="off"
data-bind="pet"
data-on:focus="el.nextElementSibling.nextElementSibling.showPopover()"
data-on:keydown="
if (evt.key === 'ArrowDown') {
evt.preventDefault();
el.nextElementSibling.nextElementSibling.showPopover();
$activeIndex = Math.min($activeIndex + 1, 5);
} else if (evt.key === 'ArrowUp') {
evt.preventDefault();
$activeIndex = Math.max($activeIndex - 1, 0);
} else if (evt.key === 'Enter' && $activeIndex >= 0) {
evt.preventDefault();
el.nextElementSibling.nextElementSibling.querySelectorAll('[role=option]')[$activeIndex].click();
} else if (evt.key === 'Escape') {
el.nextElementSibling.nextElementSibling.hidePopover();
}
"
/>
<button
type="button"
class="trigger"
aria-label="Show pet options"
aria-expanded="false"
data-on:click="
const pop = el.nextElementSibling;
pop.matches(':popover-open') ? pop.hidePopover() : pop.showPopover();
"
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<ul
id="pet-listbox"
role="listbox"
popover
aria-label="Pet options"
data-on:toggle="
evt.newState === 'open'
? (el.previousElementSibling.setAttribute('aria-expanded','true'),
document.getElementById('pet-input').setAttribute('aria-expanded','true'))
: (el.previousElementSibling.setAttribute('aria-expanded','false'),
document.getElementById('pet-input').setAttribute('aria-expanded','false'))
"
>
<li
role="option"
data-attr:aria-selected="$pet === 'Dog'"
data-attr:data-active="$activeIndex === 0"
data-on:click="$pet = 'Dog'; el.closest('[popover]').hidePopover()"
>Dog</li>
<li
role="option"
data-attr:aria-selected="$pet === 'Cat'"
data-attr:data-active="$activeIndex === 1"
data-on:click="$pet = 'Cat'; el.closest('[popover]').hidePopover()"
>Cat</li>
<li
role="option"
data-attr:aria-selected="$pet === 'Hamster'"
data-attr:data-active="$activeIndex === 2"
data-on:click="$pet = 'Hamster'; el.closest('[popover]').hidePopover()"
>Hamster</li>
<li
role="option"
data-attr:aria-selected="$pet === 'Parrot'"
data-attr:data-active="$activeIndex === 3"
data-on:click="$pet = 'Parrot'; el.closest('[popover]').hidePopover()"
>Parrot</li>
<li
role="option"
data-attr:aria-selected="$pet === 'Spider'"
data-attr:data-active="$activeIndex === 4"
data-on:click="$pet = 'Spider'; el.closest('[popover]').hidePopover()"
>Spider</li>
<li
role="option"
data-attr:aria-selected="$pet === 'Goldfish'"
data-attr:data-active="$activeIndex === 5"
data-on:click="$pet = 'Goldfish'; el.closest('[popover]').hidePopover()"
>Goldfish</li>
</ul>
</span>
</div>
<p class="hint">Pick from the list, or just type your own — nothing gets filtered out.</p>
<button type="submit">Save</button>
</form>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment