vue-auto-combo
vue-auto-combo
A fully-featured, dependency-free autocomplete / combo box component for Vue 3, built on raw HTML elements and the WAI-ARIA combobox pattern. The full behavioral requirements live in spec.md.
Features
- Autocomplete over a list of strings (case-insensitive substring matching, or bring your own
filter) - Single-select (
v-modelasstring | null) and multi-select (string[]) - Chip-style display of multi-select values (or a plain text summary with
chips: false) - Strict mode (values must come from
options) or free-text entry (freeText: true) with acreateevent - Full keyboard support: arrows, Home/End, Enter, Escape, Tab, Backspace-to-remove-chip
- Accessible: combobox/listbox roles,
aria-activedescendant, labelled chip remove buttons - Client-side validation:
rulesfunctions, avalidationevent, and an exposedvalidate()method prefix/suffixslots for icons or icon buttons inside the control- Loading spinner (
loading) — a purely visual busy indicator inside the control - Character counter (
showCounter) with nativemaxlengthenforcement - Themeable via
--ac-*CSS custom properties; zero runtime dependencies besides Vue
Usage
Installation
npm install vue-auto-combo
Or with pnpm:
pnpm add vue-auto-combo
Then import the component and its stylesheet:
<script setup>
import { ref } from 'vue'
import { AutoCombo } from 'vue-auto-combo'
import 'vue-auto-combo/style.css'
const fruit = ref(null) // single-select
const tags = ref(['vue']) // multi-select
</script>
<template>
<AutoCombo v-model="fruit" :options="['Apple', 'Banana', 'Cherry']" label="Fruit" />
<AutoCombo v-model="tags" :options="['vue', 'react', 'svelte']" multiple chips free-text />
</template>
Or register globally: app.use(VueAutoCombo) (default export).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue |
string | string[] | null |
null |
v-model; string[] when multiple |
options |
string[] |
— | Suggestions to autocomplete from |
multiple |
boolean |
false |
Multi-select mode |
chips |
boolean |
true |
Chip display for multi-select values |
freeText |
boolean |
false |
Allow values not present in options |
createOption |
boolean |
true |
Show the Add "…" row in free-text mode (Enter-based free text still works when off) |
filter |
(option, query) => boolean |
substring | Custom matcher |
maxSelections |
number |
— | Cap on multi-select values |
placeholder |
string |
'' |
Input placeholder |
disabled |
boolean |
false |
Disable the whole control |
clearable |
boolean |
true |
Show a clear-all button |
noResultsText |
string |
'No matching options' |
Empty-state message |
showNoResults |
boolean |
true |
Show the empty-state message; when false the dropdown hides entirely while nothing matches |
label |
string |
— | Visible <label> (or aria-label with hideLabel) |
ariaLabel |
string |
— | Accessible name used when no visible label is rendered |
hideLabel |
boolean |
false |
Use label as aria-label only |
openOnFocus |
boolean |
true |
Open the dropdown on focus/click |
rules |
Array<(value) => true | string> |
— | Validation rules; run on change and blur, first failing message is shown |
loading |
boolean |
false |
Show a spinner in the control (purely visual) |
showCounter |
boolean |
false |
Show a character counter for the search text |
maxlength |
number |
— | Max search-text length (native enforcement + counter) |
appendTo |
'body' | 'self' | string | HTMLElement |
'body' |
Where to render the dropdown. 'body' teleports it out so it escapes ancestor overflow/stacking (e.g. a modal); a selector/element teleports there; 'self' renders it in place |
Events
update:modelValue, search, open, close, select, remove, create,
validation ((valid: boolean, message: string | null), emitted on every validation run)
Slots
| Slot | Description |
|---|---|
prefix |
Rendered at the front of the control, before chips and the input — icons or icon buttons; interactive elements keep their native behavior |
suffix |
Rendered after the input, before the clear button |
Exposed methods (template ref)
| Method | Description |
|---|---|
validate(): boolean |
Run rules now (e.g. on form submit); shows/clears the message and returns validity |
focus(): void |
Focus the text input |
Styling
The component ships usable default styles in dist/vue-auto-combo.css (import
it once: import 'vue-auto-combo/style.css'). Customize the look by targeting
its classes from your own stylesheet.
The component's own <style> block is scoped, but scoping only affects
selectors written inside that block — the class names are plain and stable,
so a normal (non-scoped) stylesheet loaded after the component's CSS can
target them directly.
Theming with CSS custom properties
The fastest way to reskin the component is to reassign its --ac-* custom
properties on the .auto-combo class (or on a wrapper class around it).
Custom properties inherit through the DOM, so setting them anywhere above
the component in the tree works too.
| Property | Default | Used for |
|---|---|---|
--ac-border |
#c8ccd4 |
Control and dropdown border |
--ac-border-focus |
#4f6df5 |
Focus ring/border, active option check, match underline |
--ac-bg |
#fff |
Control and dropdown background |
--ac-text |
#1f2430 |
Primary text color |
--ac-muted |
#7a8194 |
Placeholder, icons, counter text |
--ac-chip-bg |
#eef1ff |
Multi-select chip background |
--ac-chip-text |
#35439c |
Chip text, create-option row, selected-check color |
--ac-active-bg |
#eef1ff |
Highlighted/hovered option background |
--ac-error |
#cf3a3a |
Error border, error text, counter-at-limit text |
--ac-disabled-bg |
#f3f4f6 |
Control background when disabled |
--ac-shadow |
0 8px 24px rgba(15, 20, 40, 0.12) |
Dropdown panel shadow |
--ac-radius |
6px |
Control and dropdown corner radius |
--ac-font |
inherit |
Font shorthand for the whole component |
--ac-listbox-max-height |
260px |
Design cap on dropdown height (the panel also shrinks to fit available space) |
--ac-listbox-z |
9999 |
z-index of the teleported dropdown (with appendTo: 'body') |
Set these on the component (.auto-combo) as usual — when the dropdown teleports
to <body>, its resolved --ac-* theme is forwarded onto the panel so it stays
styled outside the component tree.
/* Theme every instance from a global stylesheet */
.auto-combo {
--ac-bg: #262a36;
--ac-text: #e7e9f0;
--ac-border: #3a3f4f;
--ac-border-focus: #8ea2ff;
--ac-chip-bg: #333a52;
--ac-chip-text: #c3cbff;
}
/* Or scope a theme to a subset of instances with an extra class */
.theme-dark.auto-combo {
--ac-bg: #262a36;
--ac-text: #e7e9f0;
}
<template>
<AutoCombo v-model="fruit" :options="fruits" class="theme-dark" />
</template>
--ac-radiusshapes both the control and the dropdown panel, so cranking it up to a pill value (e.g.999px) ovals out the dropdown along with the input. For a true pill input, keep--ac-radiusat a normal value and round just the control with a class override (see below) instead.
Class reference
For changes the custom properties don't cover — padding, shape, borders on individual parts — target the classes directly.
| Class | Element | Notes |
|---|---|---|
.auto-combo |
Root wrapper | Add --ac-* overrides here |
.auto-combo--disabled |
Root wrapper | Present when disabled |
.auto-combo--error |
Root wrapper | Present when a validation rule fails |
.ac-label |
<label> |
Visible label (label prop, hideLabel unset) |
.ac-field |
Wrapper around control + dropdown | Positioning context for the dropdown |
.ac-control |
The bordered box | Contains prefix, chips/summary, input, spinner, suffix, clear button |
.ac-prefix / .ac-suffix |
Slot wrappers | Wrap the prefix/suffix slot content |
.ac-chip |
One selected value (multi-select + chips) |
|
.ac-chip__text |
Chip label text | |
.ac-chip__remove |
Chip's × remove button |
|
.ac-summary |
Comma-joined value text | Multi-select with chips: false |
.ac-input |
The <input> element |
|
.ac-spinner |
Loading spinner | Present when loading |
.ac-clear |
Clear-all × button |
Present when clearable and a value is selected |
.ac-footer |
Row under the control | Holds the error message and/or counter |
.ac-error |
Error message text | |
.ac-counter |
Character counter text | |
.ac-counter--limit |
Counter | Present at maxlength |
.ac-listbox |
Dropdown <ul> |
|
.ac-option |
One <li> row |
|
.ac-option--active |
Option | Keyboard/mouse-highlighted row |
.ac-option--selected |
Option | Already-selected value |
.ac-option--create |
Option | The Add "…" free-text row |
.ac-option__text |
Option label span | Wraps the highlighted match |
.ac-option__check |
Selected-option checkmark | |
.ac-match |
Matched substring within an option | Wrapped in <mark> |
.ac-empty |
No-results row | Shown when showNoResults and nothing matches |
/* Example: square corners on chips, bolder active-option highlight */
.auto-combo .ac-chip {
border-radius: 4px;
}
.auto-combo .ac-option--active {
background: var(--ac-chip-text);
color: #fff;
}
See the Styling page in Storybook for live, editable examples of both approaches.
Development
pnpm install
pnpm storybook # interactive demos at http://localhost:6006
pnpm test # Vitest component tests (one per spec requirement)
pnpm build # library build (ESM + UMD + types) into dist/
Related Packages
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.1.1 | ^3.4.0 | MIT | |
| 0.1.0 | ^3.4.0 | MIT |