Skip to content
llms.txt
llms.txt

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.

Live demo

Query · fuzzy search

Source for this preview

FuzzyComboboxExample.astro
---
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>
&lt;floating-query&gt; + &lt;floating-results&gt; +
&lt;floating-list-item&gt;
</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>

Open the full demo ↗

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.

multilingual-destinations.ts
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;
initialize-example.ts (Web Components)
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);
}
}
  • 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() and createQuery()/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.

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.