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.
Import
import '@c2n/components/autocomplete'The entry also registers c2-overlay, c2-list and c2-list-item, which it renders internally.
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, with no header or footer. When the panel cannot fit the header, the footer and --c2-autocomplete__list--min-height (96px, about two rows) of suggestions, such as on a phone with the keyboard open, the header and footer are hidden so the suggestions keep the space.
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 (its label when there is no item-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.