Query — fuzzy search
The fuzzy Query demo keeps its data local and searches it with
createFuzzySearchSource(). Use the implementation switch in the preview to
compare the Web Components and Vue adapters with the same search contract.
Source for this preview
---import {multilingualSearchPrompts} from '../../multilingual-destinations';---
<article class="demo-card combobox-card"> <div class="card-top"> <span class="number">F</span> <span class="chip">search sources</span> </div> <section class="combobox-panel" id="combobox-demo" > <h3>Multilingual query</h3> <p>Search by city, country, local script, alias, or forgiving typo.</p>
<div class="combobox-shell"> <label class="combobox-label" for="destination-search"> Destination </label> <floating-root id="combobox-root" placement="bottom-start"> <floating-list navigation loop allow-escape> <floating-query id="combobox-query" query-trigger-selector="#combobox-hints .search-sample" > <floating-reference> <input id="destination-search" class="combobox-input" type="text" autocomplete="off" spellcheck="false" aria-describedby="combobox-hints combobox-status" placeholder="Search city or country…" /> </floating-reference> <template slot="content"> <floating-results class="combobox-popup" aria-label="Destination suggestions" > <floating-results-status type="loading"> <div class="combobox-empty" role="option" aria-disabled="true" > Searching… </div> </floating-results-status> <floating-results-status type="error"> <div class="combobox-empty" role="option" aria-disabled="true" > Search failed. </div> </floating-results-status> <floating-results-status type="empty"> <div class="combobox-empty" role="option" aria-disabled="true" > No destination found for “<span data-search-text="$query" ></span>” </div> </floating-results-status> <floating-results-item> <floating-list-item> <div class="combobox-option"> <span> <strong data-search-text="label"></strong> <small data-search-text="region"></small> </span> <span class="language-badge" data-search-text="language" ></span> </div> </floating-list-item> </floating-results-item> </floating-results> </template> <p id="combobox-status" class="sr-only" aria-live="polite" > Destination suggestions closed </p> </floating-query> </floating-list> </floating-root> <span class="combobox-icon" aria-hidden="true">⌕</span> </div>
<div id="combobox-hints" class="combobox-hints"> { multilingualSearchPrompts.map(([sample, destination]) => ( <button type="button" value={sample} class="search-sample"> <code>{sample}</code><span>→ {destination}</span> </button> )) } </div>
<code> <floating-query> + <floating-results> + <floating-list-item> </code> </section></article>
<script> import { FloatingRootElement, createFuzzySearchSource, dismiss, flip, offset, size, shift, type FloatingQueryElement, } from '@floating-ui-plus/web-components'; import { multilingualDestinations, multilingualSearchKeys, type MultilingualDestination, } from '../../multilingual-destinations'; import {initializeExample} from './initialize-example';
initializeExample('combobox', (scope) => { const floating = FloatingRootElement.query( scope, '#combobox-root', ); const query = scope.querySelector<FloatingQueryElement>( '#combobox-query', ); if (!query) return;
query.configure<MultilingualDestination>({ search: { source: createFuzzySearchSource(multilingualDestinations, { keys: multilingualSearchKeys, threshold: 0.35, }), getItemKey: (item) => item.id, debounceMs: 0, }, getItemLabel: (item) => item.label, status: { closed: 'Destination suggestions closed', idle: 'Start typing to search', loading: 'Searching destinations', error: 'Destination search failed', empty: ({search: state}) => `No destinations found for ${state.query}`, results: ({search: state}) => `${state.items.length} destinations available`, }, }); const activate = (event: Event) => { const item = ( event as CustomEvent<{item: MultilingualDestination}> ).detail.item; const input = scope.querySelector<HTMLInputElement>( '#destination-search', ); if (input) input.value = item.label; }; query.addEventListener('queryactivate', activate);
floating.configure({ middleware: [ offset(8), flip({padding: 18}), shift({padding: 18}), size({ padding: 18, rootBoundary: 'viewport', apply({availableHeight, elements}) { const maxHeight = `${Math.max(0, availableHeight)}px`; elements.floating.style.setProperty( '--combobox-popup-max-height', maxHeight, ); elements.floating.style.maxHeight = maxHeight; }, }), ], plugins: [dismiss()], });
return () => query.removeEventListener('queryactivate', activate); });
</script><script setup lang="ts">import { FloatingContent, FloatingList, FloatingListItem, FloatingReference, FloatingRoot, FloatingResults, autoUpdate, createFuzzySearchSource, dismiss, flip, offset, size, shift, useQuery, useSearch,} from '@floating-ui-plus/vue';import {shallowRef} from 'vue';
import { multilingualDestinations, multilingualSearchKeys, multilingualSearchPrompts, type MultilingualDestination,} from '../../multilingual-destinations';const source = createFuzzySearchSource(multilingualDestinations, { keys: multilingualSearchKeys, threshold: 0.35,});const search = useSearch<MultilingualDestination>({ source, getItemKey: (item) => item.id, debounceMs: 0,});const selectedDestination = shallowRef<MultilingualDestination | null>(null);
const { open, activeIndex, statusText, inputProps, rolePlugin, getOptionProps, getQueryTriggerProps, getNavigationOptions,} = useQuery({ search, getItemLabel: (item) => item.label, onActivate(item) { selectedDestination.value = item; // QueryController leaves result presentation to the application. Preserve // the selected label in this destination field without reopening it. search.controller.setQuery(item.label); }, status: { closed: () => selectedDestination.value ? `${selectedDestination.value.label} selected` : 'Destination suggestions closed', idle: 'Start typing to search', loading: 'Searching destinations', error: 'Destination search failed', empty: ({search: state}) => `No destinations found for ${state.query}`, results: ({search: state}) => `${state.items.length} destinations available`, },});
const options = { placement: 'bottom-start', middleware: [ offset(8), flip({padding: 18}), shift({padding: 18}), size({ padding: 18, rootBoundary: 'viewport', apply({availableHeight, elements}) { const maxHeight = `${Math.max(0, availableHeight)}px`; elements.floating.style.setProperty( '--vue-combobox-popup-max-height', maxHeight, ); elements.floating.style.maxHeight = maxHeight; }, }), ], whileElementsMounted: autoUpdate,} as const;const plugins = [dismiss(), rolePlugin];const navigationOptions = getNavigationOptions({ allowEscape: true,});</script>
<template> <article class="vue-demo-card vue-combobox-card"> <div class="vue-card-top"> <span class="vue-number">F</span> <span>search sources</span> </div> <section class="vue-combobox-panel" > <h3>Multilingual combobox</h3> <p>Search by city, country, local script, alias, or forgiving typo.</p>
<FloatingRoot v-model:open="open" :options="options" :plugins="plugins"> <FloatingList v-model:active-index="activeIndex" navigation loop :navigation-options="navigationOptions" > <div class="vue-combobox-shell"> <label class="vue-combobox-label" for="vue-destination-search"> Destination </label> <span class="vue-combobox-icon" aria-hidden="true">⌕</span> <FloatingReference id="vue-destination-search" as="input" type="text" autocomplete="off" spellcheck="false" placeholder="Search city or country…" aria-describedby="vue-query-hints vue-query-status" class="vue-combobox-input" v-bind="inputProps" /> </div>
<FloatingContent class="vue-combobox-popup"> <FloatingResults :search="search"> <template #loading> <div class="vue-combobox-empty" role="option" aria-disabled="true"> Searching… </div> </template> <template #error> <div class="vue-combobox-empty" role="option" aria-disabled="true"> Search failed. </div> </template> <template #results> <FloatingListItem v-for="(item, index) in search.items.value" :key="item.id" tag="div" :label="item.label" :value="item" v-bind="getOptionProps(item, index)" class="vue-combobox-option" > <span> <strong>{{ item.label }}</strong> <small>{{ item.region }}</small> </span> <span class="vue-language-badge"> {{ item.language }} </span> </FloatingListItem> </template> <template #empty> <div class="vue-combobox-empty" role="option" aria-disabled="true"> No destination found for “{{ search.query.value }}” </div> </template> </FloatingResults> </FloatingContent> </FloatingList> </FloatingRoot>
<div id="vue-query-hints" class="vue-combobox-hints"> <button v-for="[sample, destination] in multilingualSearchPrompts" :key="sample" :id="`vue-query-sample-${sample}`" type="button" v-bind="getQueryTriggerProps(sample)" > <code>{{ sample }}</code><span>→ {{ destination }}</span> </button> </div>
<p id="vue-query-status" class="sr-only" aria-live="polite"> {{ statusText }} </p>
<code>useSearch() + useQuery() + <FloatingList navigation></code> </section> </article></template>What this demo shows
Section titled “What this demo shows”- Local typo-tolerant matching across city, country, aliases, and local scripts.
- Query presets, virtual focus, keyboard navigation, and an ARIA live region.
- Activation callbacks, virtual focus, and default ARIA combobox semantics.
- The same
createSearch()/useSearch()andcreateQuery()/useQuery()phases in both renderers.
The search controller owns query, loading, error, empty, results, IME, cancellation, stale-response protection, and pagination state. The renderer owns the result markup and accessible names.
Search configuration
Section titled “Search configuration”query.configure({ search: { source: createFuzzySearchSource(destinations, {keys: searchKeys}), getItemKey: (item) => item.id, }, getItemLabel: (item) => item.label,});Use getItemLabel() for visible copy. The usage recipes
cover controlled search state, activation handlers, and renderer-specific
composition. Use the deprecated Combobox API only for native selected-value
form submission.