Autocomplete
Searchable combobox for local or remote items with customizable list rows.
c2-autocomplete filters a local suggestions array or asks an asynchronous dataSource for matches. Its overlay composes an optional header, a real c2-list of c2-list-item rows, and an optional footer. Like c2-virtual-list, the default renderer reads only labelField and optional descriptionField. Arbitrary objects use field mappings and renderItem; no group, icon, meta or type fields receive special treatment. The component keeps selection and keyboard accessibility around custom content, cancels stale server requests and returns the original selected item.
Installation
npm install @c2n/autocomplete@c2n/overlay, @c2n/list and @c2n/list-item are dependencies and are registered automatically.
Usage
The examples below are real application patterns rather than visual variants. The teammate picker combines field mapping, a custom renderer, actionable header/footer slots and selection-behavior="preserve". The catalog search uses an asynchronous dataSource, cancellable requests and custom loading/empty content.
renderItem, dataSource and event listeners are JavaScript properties, so they are assigned after the declarative structure. This is the essential setup behind the two live examples:
import { html } from 'lit'
teamPicker.renderItem = ({ item }) => html`
<c2-avatar slot="prefix-icon" name=${item.name} status=${item.status} auto-color aria-hidden="true"></c2-avatar>
<span>${item.name}</span>
<span slot="description">${item.role} · ${item.location}</span>
<c2-badge slot="suffix-icon" tone=${item.status === 'online' ? 'success' : 'neutral'}>${item.status}</c2-badge>
`
teamPicker.addEventListener('suggestion-select', ({ detail }) => mention(detail.item))
catalog.dataSource = async (query, signal) => {
const response = await fetch(`/api/products?q=${encodeURIComponent(query)}`, { signal })
return response.json()
}
catalog.renderItem = ({ item }) => html`
<c2-avatar slot="prefix-icon">${item.emoji}</c2-avatar>
<span>${item.name}</span>
<span slot="description">${item.sku} · ${item.category} · ${item.price}</span>
<c2-badge slot="suffix-icon" tone=${item.stock ? 'success' : 'danger'}>${item.stock ? `${item.stock} left` : 'Sold out'}</c2-badge>
`selection-behavior="replace" is the default: choosing a suggestion replaces the input with its key and fires input, change and suggestion-select. Use selection-behavior="preserve" for command palettes or search results: choosing a row fires only suggestion-select, keeps the query unchanged and lets the same result list reopen when the input is clicked again.
For remote data, assign a function to dataSource. The component debounces calls and aborts the previous signal whenever the query changes. Reopening an unchanged query reuses its last successful results immediately; call load() when you need to force a refresh:
const autocomplete = document.querySelector('c2-autocomplete')
autocomplete.dataSource = async (query, signal) => {
const response = await fetch(`/api/suggestions?q=${encodeURIComponent(query)}`, { signal })
return response.json()
}
autocomplete.addEventListener('suggestion-select', (event) => {
console.log(event.detail.value, event.detail.item)
})Arbitrary item structures
Map identity and text with dotted field paths, then use renderItem({ item, index, search }) for complex markup. The returned content is placed inside c2-list-item, so prefix, description and suffix slots work exactly as they do in a standalone list.
import { html } from 'lit'
autocomplete.itemKey = 'id'
autocomplete.labelField = 'profile.name'
autocomplete.descriptionField = 'profile.team'
autocomplete.searchFields = ['profile.name', 'profile.team', 'code']
autocomplete.suggestions = serverObjects
autocomplete.renderItem = ({ item }) => html`
<c2-avatar slot="prefix-icon" name=${item.profile.name}></c2-avatar>
<strong>${item.profile.name}</strong>
<span slot="description">${item.profile.team}</span>
<c2-badge slot="suffix-icon">${item.code}</c2-badge>
`For structures that cannot be described by fields, set matcher(item, normalizedQuery) to replace local matching. Remote dataSource results are assumed to be filtered by the server.