Skip to content
llms.txt
llms.txt

Query — server search

The server-search Query demo passes an application-owned async source to the shared search controller. The demo’s MSW endpoint simulates debounce, AbortSignal cancellation, stale-response protection, loading/error/empty phases, and cursor pagination.

Live demo

Query · server search

Source for this preview

AsyncComboboxExample.astro
---
import {FAKE_SERVER_DESTINATION_TOTAL} from '../../fake-server-destinations';
---
<article class="demo-card combobox-card">
<section id="async-combobox-demo" class="combobox-panel combobox-server-panel">
<h3>Server-side search</h3>
<p>
MSW simulates a cursor API over {FAKE_SERVER_DESTINATION_TOTAL} distinct countries with debounce,
cancellation, and page loading.
</p>
<div class="combobox-shell">
<label class="combobox-label" for="remote-destination-search">
Remote destination
</label>
<floating-root id="async-combobox-root" placement="bottom-start">
<floating-list navigation loop allow-escape>
<floating-query id="async-combobox-query">
<floating-reference>
<input
id="remote-destination-search"
class="combobox-input"
type="text"
autocomplete="off"
spellcheck="false"
aria-describedby="remote-combobox-status"
placeholder="Try korea, japan, china…"
/>
<span class="combobox-loading-indicator" aria-hidden="true"></span>
</floating-reference>
<template slot="content">
<floating-results
class="combobox-popup async-combobox-popup"
aria-label="Remote destination suggestions"
>
<floating-results-status type="loading">
<div
class="combobox-empty"
role="option"
aria-disabled="true"
>
Querying remote endpoint…
</div>
</floating-results-status>
<floating-results-status type="error">
<div
class="combobox-empty"
role="option"
aria-disabled="true"
>
Remote search failed.
</div>
</floating-results-status>
<floating-results-status type="empty">
<div
class="combobox-empty"
role="option"
aria-disabled="true"
>
The server found no match 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">API</span>
</div>
</floating-list-item>
</floating-results-item>
<floating-results-more>
<div class="combobox-pagination" role="status">
<span class="combobox-pagination-progress">
<strong data-search-text="$count"></strong> of
<span data-search-text="$total"></span> loaded
</span>
<button
class="combobox-load-more"
type="button"
data-search-load-more
>
<span class="combobox-load-more-label">Show next 8</span>
<span class="combobox-load-more-pending">Loading…</span>
</button>
</div>
</floating-results-more>
</floating-results>
</template>
<p
id="remote-combobox-status"
class="sr-only"
aria-live="polite"
>
Remote destination suggestions closed
</p>
</floating-query>
</floating-list>
</floating-root>
<span class="combobox-icon" aria-hidden="true">⌕</span>
</div>
<div class="combobox-server-meta" aria-label="Server search behavior">
<code>MSW Service Worker</code>
<span>{FAKE_SERVER_DESTINATION_TOTAL} countries</span>
<span>8 / page</span>
<span>AbortSignal</span>
<span>cursor API</span>
</div>
<code>application source + &lt;floating-query&gt;</code>
</section>
</article>
<script>
import {
FloatingRootElement,
dismiss,
flip,
offset,
size,
shift,
type FloatingQueryElement,
} from '@floating-ui-plus/web-components';
import type {MultilingualDestination} from '../../multilingual-destinations';
import {FAKE_SERVER_DESTINATION_PAGE_SIZE} from '../../fake-server-destinations';
import {searchDestinationsOnServer} from '../../server-destination-search';
import {initializeExample} from './initialize-example';
const SERVER_QUERY_MAX_HEIGHT = 24 * 16;
initializeExample('async-combobox', (scope) => {
const floating = FloatingRootElement.query(
scope,
'#async-combobox-root',
);
const query = scope.querySelector<FloatingQueryElement>(
'#async-combobox-query',
);
if (!query) return;
query.configure<MultilingualDestination>({
search: {
source: searchDestinationsOnServer,
getItemKey: (item) => item.id,
debounceMs: 180,
cacheTtlMs: 5_000,
limit: FAKE_SERVER_DESTINATION_PAGE_SIZE,
},
getItemLabel: (item) => item.label,
status: {
closed: 'Remote destination suggestions closed',
idle: 'Remote destination search is idle',
loading: 'Querying remote destinations',
error: 'Remote destination search failed',
empty: ({search}) => `No remote matches for ${search.query}`,
results: ({search}) =>
`${search.items.length} remote destinations available`,
},
});
const activate = (event: Event) => {
const item = (
event as CustomEvent<{item: MultilingualDestination}>
).detail.item;
const input = scope.querySelector<HTMLInputElement>(
'#remote-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,
Math.min(availableHeight, SERVER_QUERY_MAX_HEIGHT),
)}px`;
elements.floating.style.setProperty(
'--async-combobox-popup-max-height',
maxHeight,
);
elements.floating.style.maxHeight = maxHeight;
},
}),
],
plugins: [dismiss()],
});
return () => query.removeEventListener('queryactivate', activate);
});
</script>

Open the full demo ↗

  • An application-owned request source with query and cursor inputs.
  • Debounced requests with cancellation when the query changes.
  • Stale-response protection and explicit idle, loading, error, empty, and results phases.
  • Cursor-based loading of the next page.
  • A size() middleware constraint that keeps the popup inside the available viewport and the result list scrollable.

The package does not choose a transport or cache library. The same composition works with fetch, TanStack Query, or another request layer.

query.configure({
search: {
source: ({query, signal, cursor}) =>
fetchDestinationPage({query, signal, cursor}),
getItemKey: (item) => item.id,
debounceMs: 200,
},
getItemLabel: (item) => item.label,
});

The Web Components and Vue previews show only the selected server component, so the source tab below each preview maps directly to the running card. See the fuzzy-search demo for local matching.