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.
The Web Components example imports its elements, Query types, and fuzzy source
from @floating-ui-plus/web-components. The Vue example imports its components,
composables, and fuzzy source from @floating-ui-plus/vue; Vue itself remains
the application’s peer dependency. Both examples also import the demo-local
destination data shown below, and the Web Components example uses a demo-local
initialization helper.
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>Demo-local support files
Section titled “Demo-local support files”Both implementations use this application-owned destination data and search-key
configuration. The Web Components example also waits for element registration
with the small initializeExample() helper shown below. The preview runs inside
the workspace demo, where these relative imports resolve to app source files;
the snippets document that example’s imports and are not a test of an npm
tarball in an independent application.
export interface MultilingualDestination { countryKeywords: readonly string[]; id: string; label: string; keywords: readonly string[]; language: string; region: string; value: string;}
export const multilingualDestinations: readonly MultilingualDestination[] = [ { countryKeywords: ['대한민국', '한국', 'south korea', 'korea', 'kr'], id: 'seoul', label: '서울', keywords: ['seoul', '서울', 'seol'], language: '한국어', region: 'South Korea', value: 'seoul', }, { countryKeywords: ['日本', '일본', 'japan', 'jp'], id: 'tokyo', label: '東京', keywords: ['とうきょう', 'tokyo', 'toukyou'], language: '日本語', region: 'Japan', value: 'tokyo', }, { countryKeywords: ['中国', '중국', 'china', 'cn'], id: 'beijing', label: '北京', keywords: ['北京', 'beijing', 'peking'], language: '中文', region: 'China', value: 'beijing', }, { countryKeywords: ['Deutschland', '독일', 'germany', 'de'], id: 'munich', label: 'München', keywords: ['munich', 'muenchen'], language: 'Deutsch', region: 'Germany', value: 'munich', },];
export const multilingualSearchPrompts = [ ['서을', '서울'], ['とうきょ', '東京'], ['bejing', '北京'], ['munchen', 'München'],] as const;
export const multilingualSearchKeys = [ {name: 'label', weight: 1}, {name: 'keywords', weight: 0.7}, {name: 'countryKeywords', weight: 0.65}, {name: 'region', weight: 0.5}, {name: 'value', weight: 0.5},] as const;export type ExampleScope = HTMLElement;
export function initializeExample( name: string, setup: (scope: ExampleScope) => void,) { if (document.documentElement.dataset.framework === 'vue') return; const initialize = () => { const scope = document.getElementById(`${name}-demo`) as ExampleScope | null; if (!scope || scope.dataset.initialized === 'true') return; setup(scope); scope.dataset.initialized = 'true'; };
// Layout-level registration is intentionally loaded before examples, but it // is a dynamic module import. Wait for the custom-element upgrade before a // demo calls element APIs such as `configure()` or `query()`. if (customElements.get('floating-root')) { initialize(); } else { void customElements.whenDefined('floating-root').then(initialize); }}What this demo shows
Section titled “What this demo shows”- Local typo-tolerant matching across city, country, aliases, and local scripts.
- Query presets, keyboard navigation, activation callbacks, and an ARIA live region.
- 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.