Last active
July 3, 2026 18:08
-
-
Save maxsei/d6713933e13331c27730b9c9b1aa55e4 to your computer and use it in GitHub Desktop.
editable select box
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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