J3W1 / DESIGN SYSTEM
Combobox
A text input that filters a listbox of suggestions; the APG combobox with list autocomplete, rendered with the list open.
stable · LIGHT DOM · 1.1.0
Light DOM retains static content, native semantics, label relationships and host composition without duplicating controls.
Live variants
default
- Cascadia Mono
- SauceCodePro NFM
- Source Code Pro
- No fonts match.
Down opens the list; Enter accepts.
<j3w1-combobox id="example-combobox-default"><div class="combobox">
<label class="combobox-label" for="example-combobox-default-cb-font">Font family</label>
<div class="combobox-root">
<input class="combobox-input" id="example-combobox-default-cb-font" type="text" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="example-combobox-default-cb-font-list" autocomplete="off" value="Sauce" placeholder="Type to filter" aria-describedby="example-combobox-default-cb-font-help example-combobox-default-cb-font-message" name="example-combobox-default-cb-font">
<button class="combobox-toggle" type="button" tabindex="-1" aria-label="Show all fonts"><svg class="combobox-chevron" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false"><path d="M3.5 6 8 10.5 12.5 6" fill="none" stroke="currentColor" stroke-width="1.5"></path></svg></button>
</div>
<ul class="combobox-listbox" id="example-combobox-default-cb-font-list" role="listbox" aria-label="Fonts">
<li class="combobox-option" role="option" id="example-combobox-default-cb-font-opt-1">Cascadia Mono</li>
<li class="combobox-option" role="option" id="example-combobox-default-cb-font-opt-2" aria-selected="true">SauceCodePro NFM</li>
<li class="combobox-option" role="option" id="example-combobox-default-cb-font-opt-3">Source Code Pro</li>
<li class="combobox-empty" role="option" aria-disabled="true">No fonts match.</li>
</ul>
<p class="combobox-help" id="example-combobox-default-cb-font-help">Down opens the list; Enter accepts.</p>
<p class="combobox-message" id="example-combobox-default-cb-font-message">✕ Choose a font from the list.</p>
</div></j3w1-combobox>with-groups
-
Recent
- j3w1.github.io
- j3w1.local
-
Known
- j3w1-build.internal
- No hosts match.
Recent hosts first.
<j3w1-combobox id="example-combobox-with-groups"><div class="combobox">
<label class="combobox-label" for="example-combobox-with-groups-cb-host">Host</label>
<div class="combobox-root">
<input class="combobox-input" id="example-combobox-with-groups-cb-host" type="text" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="example-combobox-with-groups-cb-host-list" autocomplete="off" value="j3" placeholder="Name or address" aria-describedby="example-combobox-with-groups-cb-host-help example-combobox-with-groups-cb-host-message" name="example-combobox-with-groups-cb-host">
<button class="combobox-toggle" type="button" tabindex="-1" aria-label="Show all hosts"><svg class="combobox-chevron" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false"><path d="M3.5 6 8 10.5 12.5 6" fill="none" stroke="currentColor" stroke-width="1.5"></path></svg></button>
</div>
<ul class="combobox-listbox" id="example-combobox-with-groups-cb-host-list" role="listbox" aria-label="Hosts">
<li class="combobox-group" role="group" aria-labelledby="example-combobox-with-groups-cb-host-g1">
<span class="combobox-group-heading" id="example-combobox-with-groups-cb-host-g1" role="presentation">Recent</span>
<ul class="combobox-group-list" role="presentation">
<li class="combobox-option" role="option" id="example-combobox-with-groups-cb-host-opt-1" aria-selected="true">j3w1.github.io</li>
<li class="combobox-option" role="option" id="example-combobox-with-groups-cb-host-opt-2">j3w1.local</li>
</ul>
</li>
<li class="combobox-group" role="group" aria-labelledby="example-combobox-with-groups-cb-host-g2">
<span class="combobox-group-heading" id="example-combobox-with-groups-cb-host-g2" role="presentation">Known</span>
<ul class="combobox-group-list" role="presentation">
<li class="combobox-option" role="option" id="example-combobox-with-groups-cb-host-opt-3">j3w1-build.internal</li>
</ul>
</li>
<li class="combobox-empty" role="option" aria-disabled="true">No hosts match.</li>
</ul>
<p class="combobox-help" id="example-combobox-with-groups-cb-host-help">Recent hosts first.</p>
<p class="combobox-message" id="example-combobox-with-groups-cb-host-message">✕ Choose a known host.</p>
</div></j3w1-combobox>no-results
- No tags match.
Escape clears the filter.
<j3w1-combobox id="example-combobox-no-results"><div class="combobox">
<label class="combobox-label" for="example-combobox-no-results-cb-tag">Tag</label>
<div class="combobox-root">
<input class="combobox-input" id="example-combobox-no-results-cb-tag" type="text" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="example-combobox-no-results-cb-tag-list" autocomplete="off" value="zzq" placeholder="Type to filter" aria-describedby="example-combobox-no-results-cb-tag-help example-combobox-no-results-cb-tag-message" name="example-combobox-no-results-cb-tag">
<button class="combobox-toggle" type="button" tabindex="-1" aria-label="Show all tags"><svg class="combobox-chevron" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false"><path d="M3.5 6 8 10.5 12.5 6" fill="none" stroke="currentColor" stroke-width="1.5"></path></svg></button>
</div>
<ul class="combobox-listbox" id="example-combobox-no-results-cb-tag-list" role="listbox" aria-label="Tags">
<li class="combobox-empty combobox-empty-shown" role="option" aria-disabled="true">No tags match.</li>
</ul>
<p class="combobox-help" id="example-combobox-no-results-cb-tag-help">Escape clears the filter.</p>
<p class="combobox-message" id="example-combobox-no-results-cb-tag-message">✕ Choose an existing tag.</p>
</div></j3w1-combobox>Install and register
Use the exact version. If npm publication is pending, install the release tarball from the installation guide.
npm install --save-exact @j3w1/ui@1.1.0import '@j3w1/ui/tokens.css';
import '@j3w1/ui/styles/combobox.css';
import '@j3w1/ui/register/combobox';Insert the maintained markup above after loading these imports. Its native children provide accessible names and form behavior; the empty custom tag alone does not create a control.
Copy the complete implementation
npx --package=@j3w1/ui@1.1.0 j3w1-ui copy combobox --out ./vendor/j3w1/comboboxThe CLI verifies every file before writing and refuses overwrites. Serve the output over HTTP and open index.html. Keep its modules, CSS, tokens and licence notices together.
Public API
attributes
| Name | Type / signature | Meaning |
|---|---|---|
disabled | CustomEvent | Disable the native controls owned by this instance. |
loading | CustomEvent | Mark the primary control busy and prevent duplicate activation. |
properties
| Name | Type / signature | Meaning |
|---|---|---|
disabled | boolean | Delegates disabled state to native children. |
name | string | Native control name; application form keys remain explicit. |
value | string | Typed or accepted input value. |
open | boolean | List visibility. |
methods
| Name | Type / signature | Meaning |
|---|---|---|
refresh | (): void | Reconnect behavior after replacing native child structure. |
focus | (options?: FocusOptions): void | Focus the first usable native control. |
show | (allOptions?: boolean): void | Open the list, optionally showing every option. |
hide | (): void | Close without discarding typed text. |
events
| Name | Type / signature | Meaning |
|---|---|---|
j3w1-change | CustomEvent | An option was accepted or the value cleared. |
Dependencies and verification
Registered components: none. Style dependencies: none.
Package presence establishes an available implementation. Actual passes depend on the tested artifact and environment. Read specification execution evidence; read packed-consumer evidence; inspect the complete state matrix.
Full implementation manifest, API and token dependency closure
Canonical specification
Purpose
A text input whose value is completed from a list of suggestions. It is the right control when the set is large or open-ended (fonts, hosts, tags); a closed set of a dozen values is the select component. The demonstration renders the list open so every option state is visible without a script.
Anatomy
Label above the box; the box with the input and an optional toggle; the
listbox directly below, on color.surface.raised behind a 1px
color.border.overlay; options in color.text.default, an optional group
heading in color.text.subtle above a color.border.divider rule; the empty
message in color.text.muted; help and validation message below, as in the
text field.
States
| State | Visual | Non-colour channel |
|---|---|---|
| default | 1px color.border.control on color.surface.input; list on color.surface.raised with a 1px color.border.overlay | — |
| hover | box background → color.interaction.hover.bg; the hovered option → color.interaction.hover.bg-strong with color.text.link-hover | cursor: text on the box, default on the list |
| focus-visible | box border → color.border.active; ring 1px dashed color.interaction.focus.ring at −2px | the ring |
| open | list visible below the box; toggle glyph rotated 180° | aria-expanded="true"; the toggle glyph |
| closed | list absent; the box unchanged | aria-expanded="false"; nothing below the box |
| selected | the active option filled color.interaction.selection.bg with color.interaction.selection.text and a 1px dashed ring in color.interaction.focus.ring-container | aria-selected="true"; aria-activedescendant; the ring |
| invalid | box border 2px color.status.danger.border; message with the ✕ glyph | border width 1 → 2px; glyph; aria-invalid |
| invalid+focus-visible | the 2px danger border and the ring in color.interaction.focus.ring-container at −4px | double boundary |
| disabled | text color.text.disabled; border color.border.disabled; background color.interaction.disabled.bg; list never opens | disabled; cursor: not-allowed |
| no-results | the list holds only the empty row in color.text.muted, italic | aria-disabled row; italic; a polite announcement of zero matches |
Precedence when several apply: disabled > invalid > selected > hover; focus-visible is always drawn on the box, and the selection ring on the option.
Keyboard
DOM focus never leaves the input. Down and Up open the list and move the
active option, which the input reports through aria-activedescendant; Enter
accepts it; Escape closes the list and, when the list is already closed,
clears the value. Alt+Down and Alt+Up open and close without changing the
active option. Home and End move the caret. Tab closes the list and keeps
whatever is typed. Typing filters the list on every keystroke; the number of
matches is announced through a polite live region, not by re-reading the list.
Accessibility
The input carries role="combobox", aria-autocomplete="list",
aria-expanded, aria-controls pointing at the listbox and, while an option
is active, aria-activedescendant. The listbox has an accessible name
(aria-label or aria-labelledby the field label); options are role="option"
with aria-selected on the active one only; groups are role="group" with
aria-labelledby their heading. The empty row is a disabled option so the
list is never empty; the zero-match count is announced through the same polite
live region as every other count. Contrast: option text 8.43:1 on the
raised surface, selection text 12.47:1 on the selection fill, the selection ring
7.48:1 on the fill, the list border 4.57:1 on the raised surface, group
headings 4.96:1.
Portability
Everything is expressible with background, border and outline; the toggle
rotation is a transform inside the motion budget. Toolkits with a native
completion popup use it and map the option fill to the selection roles; the
list border is the popover frame. Toolkits without aria-activedescendant
move real focus to the option and record the deviation.
Non-examples
A select restyled as a combobox. Moving DOM focus into the list on Down. A list that floats over the page with a shadow or a rounded frame. Highlighting the active option with a colour change of the text alone. A no-results state that silently shows the full list. Opening on hover. A list that slides or fades in for longer than 150 ms.