# Search query field

> A search box in a role=search GET form: magnifier, query, a clear button once there is text, an optional / shortcut hint and an inset submit, all one shape. Submits ?q= without JS; optional debounced live filtering.

- ID: `cmp_search_query_field_01`
- Slug: `search-query-field-01`
- Version: `1.0.0` (current)
- Status: published
- Published: 2026-10-22T03:38:37Z
- Updated: 2026-10-22
- Available versions: `1.0.0`
- Kind: control
- Primary category: `text-inputs`
- Detail page: https://pagesugar.com/components/search-query-field-01?variant=blue
- Preview: https://pagesugar.com/preview/search-query-field-01

## Variants

| Variant | Label | Default | Artifact digest |
| --- | --- | --- | --- |
| `neutral` | Neutral | yes | `sha256-7f9cb682be8cbf226e84d8198d7e809f63591719645db5a7f7dc76638d1dce59` |
| `blue` | Blue accent | no | `sha256-3b6823ca324e626640f856b502836eec72cae2afaa4d492cc072c717718a91e7` |

## Runtime and compatibility

- Runtime: svelte
- Svelte: 5
- SvelteKit required: no (portable Svelte component)
- Imports: The source imports its own files by relative path only. Examples import from $lib/...; SvelteKit 3 projects have no $lib alias by default, so import from your #lib subpath (with the .svelte extension) or a relative path instead.
- Tailwind CSS: 4
- SSR: supported
- Requires client-side JavaScript: no
- Integration level: local-interaction
- Appearance modes: light
- Suggested directory: `src/lib/components/search-query-field-01`

## Dependencies

No third-party runtime packages.

## Services

No external services required.

## Usage

Give it a label and an action and it renders a search form that submits GET ?q= to that page, with or without JavaScript. Pass onSearch to handle submission yourself, or onInput for live filtering. It does not fetch or show results, suggest queries, or keep search history; those are yours.

Required props: `label`

```svelte
<script lang="ts">
	import SearchQueryField from '$lib/components/search-query-field-01/SearchQueryField.svelte';
</script>

<header class="flex items-center gap-6 px-4 py-3">
	<a href="/" class="font-semibold">Halcyon</a>
	<SearchQueryField
		label="Search Halcyon docs"
		placeholder="Search the docs"
		action="/docs/search"
		shortcut="/"
		class="ms-auto w-full max-w-72"
	/>
</header>
```

Limitations:

- The form is a size container, so it takes its width from its parent. In a flex row give it a width or flex-1 through class, or it collapses.
- The clear button appears once the page has hydrated; before that the field still submits and can be edited, but not cleared with one press.
- shortcut takes one printable key such as '/'. Combinations such as Ctrl+K are not handled; bind those yourself and point them at the input.
- A single-key shortcut such as '/' must be switchable off by the person using the site (WCAG 2.1.4). The component turns it off when shortcut is undefined; the setting that drives that is yours.
- onSearch receives the raw query. Trim it, and decide what an empty search means, in your handler or on the results page.
- It has no results, suggestion list or combobox behaviour. Announce result counts from your own results region, not on every keystroke.

## Usage guide

### Search query field

A search box that is a plain GET form first. With no script at all, typing and pressing Enter
goes to `action?q=…`, which is how site and docs search should work. JavaScript adds the clear
button, Escape to clear, the shortcut and the two callbacks.

```svelte
<SearchQueryField
	label="Search Halcyon docs"
	placeholder="Search the docs"
	action="/docs/search"
	shortcut="/"
	class="w-full max-w-72"
/>
```

#### Give it a width

The form is a container (its submit button collapses to an arrow under 24rem), so it sizes from
its parent rather than its content. In a flex header pass `class="flex-1"` or a width.

#### Handling the search yourself

`onSearch` turns off navigation and hands you the query. Keep it off when a results page exists:
the GET form already works, survives a failed script and gives every search a URL.

```svelte
<SearchQueryField
	label="Search orders"
	onSearch={(q) => goto(`/orders?q=${encodeURIComponent(q)}`)}
/>
```

#### Live filtering

`onInput` fires once typing pauses for `debounceMs` (250 by default), never on mount, and straight
away with `''` when the field is cleared. Use it to filter a list that is already on the page,
and announce the count from your own status region:

```svelte
<script lang="ts">
	let filter = $state('');
	const shown = $derived(boards.filter((b) => b.name.toLowerCase().includes(filter.toLowerCase())));
</script>

<SearchQueryField label="Filter boards" size="sm" onInput={(q) => (filter = q.trim())} />
<p role="status">{shown.length} of {boards.length} boards</p>
```

If both callbacks are set, pressing Enter flushes a pending `onInput` before `onSearch`, so the
two never disagree.

#### Shortcut

`shortcut="/"` focuses the field when `/` is pressed anywhere on the page, unless focus is in
another field or a modifier key is held. The chip showing it hides on focus, once there is a
query, and on touch screens. Only single keys are handled.

A single-character shortcut gets in the way of people using speech input or switching keyboards,
so WCAG 2.1.4 asks for a way to turn it off. Drive the prop from a setting:

```svelte
<SearchQueryField label="Search Halcyon docs" shortcut={prefs.shortcuts ? '/' : undefined} />
```

#### Retoning

On a `#09090b` header:

```css
.header {
	background: #09090b;
	color-scheme: dark;
	--search-query-field-accent: #fafafa;
	--search-query-field-on-accent: #09090b;
	--search-query-field-ink: #fafafa;
	--search-query-field-muted: #a1a1aa;
	--search-query-field-placeholder: #a1a1aa;
	--search-query-field-hairline: rgb(255 255 255 / 0.35);
	--search-query-field-surface: #18181b;
}
```

## Props

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `label` | `string` | yes |  | Accessible label for the input, also the search landmark's name, for example 'Search articles'. Visually hidden unless labelVisible. |
| `value` | `string` | no | `''` | Bindable query. |
| `labelVisible` | `boolean` | no | `false` | Show the label above the field. Off, it stays in the markup for assistive technology. |
| `action` | `string` | no |  | Form action for GET submission. Omitted, the form submits to the current page. |
| `name` | `string` | no | `'q'` | Query parameter name. |
| `onSearch` | `(query: string) => void` | no |  | When supplied, submission is intercepted and this is called with the query instead of navigating. A pending onInput fires first. |
| `onInput` | `(query: string) => void` | no |  | Called with the query once typing pauses for debounceMs, and straight away with '' when the field is cleared. Never called on mount. |
| `debounceMs` | `number` | no | `250` | Pause after the last keystroke before onInput fires, in milliseconds. |
| `showSubmit` | `boolean` | no | `false` | Render a submit button inset at the end of the field. Under 24rem wide it shows an arrow and keeps submitLabel as its name. |
| `submitLabel` | `string` | no | `'Search'` | Submit button text, and its accessible name when it collapses to an arrow. |
| `clearLabel` | `string` | no | `'Clear search'` | Accessible name of the clear button. |
| `shortcut` | `string` | no |  | One key, such as '/', that focuses the field from anywhere on the page unless someone is typing in another field. Shown as a hint chip on keyboard devices and set as aria-keyshortcuts. Pass undefined to turn it off; offer people a setting for that. |
| `shape` | 'rounded' \| 'pill' | no | `'rounded'` | 'rounded' for app toolbars and docs, 'pill' for consumer sites. The submit button and hint chip follow the shape. |
| `size` | 'sm' \| 'md' \| 'lg' | no | `'md'` | 32, 36 or 40 px tall with 13, 14 or 16 px text. On touch screens every size is 46 px with 16 px text, so the input inside the border clears 44 px. |
| `disabled` | `boolean` | no | `false` | Disables the input and its clear and submit buttons, and the shortcut. |
| `onkeydown` | `KeyboardEventHandler<HTMLInputElement>` | no |  | Keydown on the input, called before the field's own Escape handling; call preventDefault to keep Escape from clearing. |
| `id` | `string` | no |  | Input id. Defaults to one generated per instance; keep a supplied one unique on the page. |
| `class` | `string` | no |  | Extra classes on the form, for placement and width. |

## Customization

Change the copy through props and retone the field through seven --search-query-field-\* CSS variables. Hover, the hint chip and the submit hover are mixed from those, so they follow any retone.

- Accent: --search-query-field-accent fills the submit button and colours the focus edge, its halo and the button focus rings; --search-query-field-on-accent is the submit text. Keep the accent at 3:1 against the page.
- Neutrals: --search-query-field-ink is the query, --search-query-field-muted the magnifier, clear cross and hint, --search-query-field-placeholder the placeholder, --search-query-field-hairline the resting edge (keep it at 3:1, since it is the only thing marking an empty field) and --search-query-field-surface the fill.
- Worked retone for a #09090b header: --search-query-field-accent: #fafafa; --search-query-field-on-accent: #09090b; --search-query-field-ink: #fafafa; --search-query-field-muted: #a1a1aa; --search-query-field-placeholder: #a1a1aa; --search-query-field-hairline: rgb(255 255 255 / 0.35); --search-query-field-surface: #18181b. Add color-scheme: dark on the header.
- Cream page such as a bakery's #fbf7f0: leave the hairline alone (it is alpha black, so it tints with the page), set --search-query-field-surface: #fffdf9 and use shape 'pill'.
- Sizes: sm in toolbars and list filters, md in app and docs headers, lg in help-centre heroes and marketing pages.
- Width: the form fills its container. In a header, pass class 'w-full max-w-72' or 'flex-1'; on a results page let it run to the column width.
- Behaviour: leave onSearch off to keep the plain GET navigation that works without JavaScript. Use onInput for filtering a list already on the page, and raise debounceMs if each call hits a server.
- Translate label, placeholder, submitLabel and clearLabel with the page; the icons and buttons follow dir=rtl.

| Token | Public CSS variable |
| --- | --- |
| `accent` | `--search-query-field-accent` |
| `onAccent` | `--search-query-field-on-accent` |
| `ink` | `--search-query-field-ink` |
| `muted` | `--search-query-field-muted` |
| `placeholder` | `--search-query-field-placeholder` |
| `hairline` | `--search-query-field-hairline` |
| `surface` | `--search-query-field-surface` |

## Accessibility

- The form has role=search and is named by the label through aria-labelledby. With two searches on one page (site and list filter), give them labels that tell them apart.
- A native \<label for\> names the input even when it is visually hidden. The placeholder is a hint, never the label.
- The clear button is named by clearLabel ('Clear search'). It empties the field and moves focus back to the input, so keyboard and screen reader users stay where they were.
- Escape clears a non-empty field. In an empty field it is left alone, so a dialog or menu around the field can still close on it.
- Nothing is announced while typing. When onInput filters a list, announce the result count from your own status region, as the live-filter fixture does.
- The input sets enterkeyhint=search, autocomplete=off and spellcheck=false; pass your own values to override them. The native WebKit clear cross is hidden so there is only one clear button.
- The shortcut is ignored while focus is in another field or a modifier is held, and the hint chip is hidden from assistive technology; aria-keyshortcuts carries it instead. A single-character shortcut needs a way for people to turn it off (WCAG 2.1.4): bind shortcut to a user setting and pass undefined when it is off.
- A readonly query can be focused, selected and submitted, but the clear button and Escape leave it alone. Escape pressed during IME composition goes to the input method, not the field.
- Focus draws a two-pixel accent edge around the whole shape, kept while focus is on the clear or submit button, which also get their own two-pixel ring. In forced-colours mode focus becomes a Highlight outline.

## License

- Declared source: MIT
- Default license approval is pending. See https://pagesugar.com/docs/license.

## Source

- Palette: Blue accent (`blue`)
- Entry: `SearchQueryField.svelte`
- Suggested directory: `src/lib/components/search-query-field-01`
- Files: 1
- Artifact digest: `sha256-3b6823ca324e626640f856b502836eec72cae2afaa4d492cc072c717718a91e7`

Paths below are relative to the suggested directory. Copy the files as they are;
they import nothing from this site.

#### `SearchQueryField.svelte`

Role: entry · 14025 bytes · SHA-256 `c9e0531bd002b5fdab76a235c881c0daf9fc16178447736d211ecf70c70c4d21`

```svelte
<!--
	A search box: a search landmark form holding a label, a leading magnifier, a type=search input,
	a clear button once there is a query, an optional shortcut hint and an optional submit button,
	all inside one bordered shape. It is a plain GET form, so before any script runs Enter submits
	?q= to the action. onSearch intercepts submission; onInput reports typing after a debounce.
-->
<script lang="ts" module>
	export type SearchQueryFieldSize = 'sm' | 'md' | 'lg';
	export type SearchQueryFieldShape = 'rounded' | 'pill';
</script>

<script lang="ts">
	import type { HTMLInputAttributes, KeyboardEventHandler } from 'svelte/elements';

	type Passthrough = Omit<
		HTMLInputAttributes,
		| 'type'
		| 'value'
		| 'id'
		| 'name'
		| 'size'
		| 'class'
		| 'disabled'
		| 'children'
		| 'oninput'
		| 'onkeydown'
		| 'form'
	>;

	interface Props extends Passthrough {
		/** Accessible label, also the landmark's name. Visually hidden unless labelVisible. */
		label: string;
		/** Bindable query. */
		value?: string;
		/** Show the label above the field instead of only to assistive technology. */
		labelVisible?: boolean;
		/** Form action for GET submission. Defaults to the current page. */
		action?: string;
		/** Query parameter name. */
		name?: string;
		/** Called on submit instead of navigating. */
		onSearch?: (query: string) => void;
		/** Called with the query once typing pauses for debounceMs, and with '' on clear. */
		onInput?: (query: string) => void;
		/** Pause before onInput fires, in milliseconds. */
		debounceMs?: number;
		/** Render a submit button inside the field. */
		showSubmit?: boolean;
		/** Submit button text; the accessible name when it collapses to an icon. */
		submitLabel?: string;
		/** Accessible name of the clear button. */
		clearLabel?: string;
		/** Single key that focuses the field from anywhere on the page, shown as a hint. */
		shortcut?: string;
		/** 'rounded' for app toolbars and docs; 'pill' for consumer sites. */
		shape?: SearchQueryFieldShape;
		/** sm 32 px, md 36 px, lg 40 px tall; 46 px on touch screens. */
		size?: SearchQueryFieldSize;
		/** Disable the field and its buttons. */
		disabled?: boolean;
		/** Keydown on the input, called before the field's own Escape handling. */
		onkeydown?: KeyboardEventHandler<HTMLInputElement>;
		/** Input id. Defaults to one generated per instance. */
		id?: string;
		/** Extra classes on the form, for placement. */
		class?: string;
	}

	let {
		label,
		value = $bindable(''),
		labelVisible = false,
		action,
		name = 'q',
		onSearch,
		onInput,
		debounceMs = 250,
		showSubmit = false,
		submitLabel = 'Search',
		clearLabel = 'Clear search',
		shortcut,
		shape = 'rounded',
		size = 'md',
		disabled = false,
		onkeydown,
		id,
		class: className,
		...rest
	}: Props = $props();

	const uid = $props.id();
	const inputId = $derived(id ?? `${uid}-input`);
	const labelId = `${uid}-label`;

	let input: HTMLInputElement | undefined = $state();
	/* The clear button only works with script, so it appears once the page has hydrated. */
	let hydrated = $state(false);
	let timer: ReturnType<typeof setTimeout> | undefined;
	/* A read-only query can be selected and submitted, but not cleared. */
	const readonly = $derived(!!rest.readonly);
	const clearable = $derived(hydrated && !!value && !readonly && !disabled);

	$effect(() => {
		hydrated = true;
		return () => clearTimeout(timer);
	});

	function handleInput(event: Event & { currentTarget: HTMLInputElement }) {
		if (!onInput) return;
		const query = event.currentTarget.value;
		clearTimeout(timer);
		timer = setTimeout(() => {
			timer = undefined;
			/* Re-read: the parent may have removed the callback while the timer ran. */
			onInput?.(query);
		}, debounceMs);
	}

	function clear() {
		clearTimeout(timer);
		timer = undefined;
		value = '';
		onInput?.('');
		input?.focus();
	}

	function handleKeydown(event: KeyboardEvent & { currentTarget: HTMLInputElement }) {
		onkeydown?.(event);
		if (event.defaultPrevented || event.key !== 'Escape') return;
		/* Escape during IME composition dismisses the candidates, not the query. */
		if (event.isComposing) return;
		/* An empty field lets Escape through, so a dialog or menu around it can still close. */
		if (!value || readonly) return;
		event.preventDefault();
		clear();
	}

	function handleSubmit(event: SubmitEvent) {
		if (!onSearch) return;
		event.preventDefault();
		/* A pending onInput fires first, so a live filter and the search agree on the query. */
		if (timer !== undefined) {
			clearTimeout(timer);
			timer = undefined;
			onInput?.(value);
		}
		onSearch(value);
	}

	/* The shortcut focuses the field unless someone is typing elsewhere or holds a modifier. */
	function handleShortcut(event: KeyboardEvent) {
		if (!shortcut || disabled || !input || event.defaultPrevented) return;
		if (event.key !== shortcut || event.metaKey || event.ctrlKey || event.altKey) return;
		const target = event.target as HTMLElement | null;
		if (
			target?.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])')
		)
			return;
		event.preventDefault();
		input.focus();
	}

	/* A press on the icon or the border focuses the input, as a press inside one box should. */
	function focusInput(event: PointerEvent) {
		if (!input || disabled || event.button !== 0 || event.target === input) return;
		if ((event.target as Element).closest('button')) return;
		event.preventDefault();
		input.focus();
	}
</script>

<svelte:window onkeydown={handleShortcut} />

<form
	role="search"
	method="get"
	{action}
	aria-labelledby={labelId}
	class={['search-query-field @container grid min-w-0 gap-2', className]}
	data-size={size}
	data-shape={shape}
	onsubmit={handleSubmit}
>
	<label
		id={labelId}
		for={inputId}
		class={[
			labelVisible
				? 'block text-sm leading-5 font-medium text-pretty break-words text-(--_ink)'
				: 'sr-only'
		]}
	>
		{label}
	</label>

	<!-- svelte-ignore a11y_no_static_element_interactions -->
	<div
		class={[
			'search-query-field__control group flex h-(--_h) min-w-0 items-stretch rounded-(--_r) border',
			'border-(--_edge) bg-(--_surface) text-(--_ink)'
		]}
		onpointerdown={focusInput}
	>
		<span
			class={['flex shrink-0 items-center text-(--_muted)', shape === 'pill' ? 'ps-4' : 'ps-3']}
			aria-hidden="true"
		>
			<svg class={size === 'sm' ? 'size-3.5' : 'size-4'} viewBox="0 0 16 16" fill="none">
				<circle cx="7" cy="7" r="4.75" stroke="currentColor" stroke-width="1.75" />
				<path d="m10.5 10.5 3 3" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" />
			</svg>
		</span>

		<input
			bind:this={input}
			autocomplete="off"
			enterkeyhint="search"
			spellcheck={false}
			{...rest}
			bind:value
			type="search"
			{name}
			{disabled}
			id={inputId}
			aria-keyshortcuts={shortcut || undefined}
			oninput={handleInput}
			onkeydown={handleKeydown}
			class={[
				'search-query-field__input min-w-0 flex-1 bg-transparent ps-2 text-(--_ink) outline-none',
				'placeholder:text-(--_placeholder) disabled:cursor-not-allowed',
				'pointer-coarse:text-base',
				size === 'sm' && 'text-[13px] leading-5',
				size === 'md' && 'text-sm leading-5',
				size === 'lg' && 'text-base leading-6',
				!clearable && !shortcut && !showSubmit && 'pe-3'
			]}
		/>

		{#if clearable || shortcut || showSubmit}
			<div class={['flex shrink-0 items-stretch', showSubmit ? 'pe-1' : 'pe-2']}>
				{#if clearable}
					<button
						type="button"
						class="search-query-field__clear group/clear grid w-8 place-items-center text-(--_muted) outline-none disabled:cursor-not-allowed pointer-coarse:w-11"
						aria-label={clearLabel}
						{disabled}
						onclick={clear}
					>
						<span
							class={[
								'grid place-items-center rounded-full transition-[background-color,color] duration-150 ease-(--_ease) motion-reduce:transition-none',
								'group-hover/clear:bg-(--_fill-hover) group-hover/clear:text-(--_ink) group-disabled/clear:bg-transparent',
								'group-focus-visible/clear:outline-2 group-focus-visible/clear:outline-offset-2 group-focus-visible/clear:outline-(--_accent)',
								size === 'sm' ? 'size-5' : 'size-6'
							]}
						>
							<svg class="size-3.5" viewBox="0 0 16 16" fill="none" aria-hidden="true">
								<path
									d="m4.5 4.5 7 7m0-7-7 7"
									stroke="currentColor"
									stroke-width="1.75"
									stroke-linecap="round"
								/>
							</svg>
						</span>
					</button>
				{:else if shortcut}
					<!-- A hint for keyboards: gone once the field has focus, a query, or a touch screen. -->
					<span
						class="flex items-center group-focus-within:hidden pointer-coarse:hidden"
						aria-hidden="true"
					>
						<kbd
							class="grid h-5 min-w-5 place-items-center rounded-(--_r-inner) border border-(--_hairline-soft) px-1 font-sans text-xs leading-none font-medium text-(--_muted)"
							>{shortcut}</kbd
						>
					</span>
				{/if}

				{#if showSubmit}
					<button
						type="submit"
						class={[
							'search-query-field__submit group/submit flex justify-end py-1 ps-1 outline-none disabled:cursor-not-allowed pointer-coarse:min-w-11',
							clearable ? '' : 'ms-1'
						]}
						{disabled}
					>
						<span
							class={[
								'flex items-center justify-center rounded-(--_r-inner) bg-(--_accent) font-medium text-(--_on-accent)',
								'transition-[background-color,scale] duration-150 ease-(--_ease) motion-reduce:transition-none',
								'group-hover/submit:bg-(--_accent-hover) group-disabled/submit:bg-(--_accent)',
								'group-active/submit:scale-[.98] motion-reduce:group-active/submit:scale-100',
								'group-focus-visible/submit:outline-2 group-focus-visible/submit:outline-offset-2 group-focus-visible/submit:outline-(--_accent)',
								'aspect-square @sm:aspect-auto',
								shape === 'pill' ? '@sm:px-4' : '@sm:px-3',
								'pointer-coarse:text-base',
								size === 'sm' && 'text-[13px] leading-5',
								size === 'md' && 'text-sm leading-5',
								size === 'lg' && 'text-base leading-6'
							]}
						>
							<svg
								class="size-4 @sm:hidden rtl:-scale-x-100"
								viewBox="0 0 16 16"
								fill="none"
								aria-hidden="true"
							>
								<path
									d="M3 8h10m-4-4 4 4-4 4"
									stroke="currentColor"
									stroke-width="1.75"
									stroke-linecap="round"
									stroke-linejoin="round"
								/>
							</svg>
							<span class="sr-only @sm:not-sr-only">{submitLabel}</span>
						</span>
					</button>
				{/if}
			</div>
		{/if}
	</div>
</form>

<style>
	/* Public tokens: set --search-query-field-* on the form or any ancestor to retone it. */
	.search-query-field {
		--_accent: var(--search-query-field-accent, #1d4ed8);
		--_on-accent: var(--search-query-field-on-accent, #ffffff);
		--_ink: var(--search-query-field-ink, #18181b);
		--_muted: var(--search-query-field-muted, #52525b);
		--_placeholder: var(--search-query-field-placeholder, #71717a);
		/* 3:1 against white: the edge is the only thing marking an empty field (WCAG 1.4.11). */
		--_hairline: var(--search-query-field-hairline, rgb(0 0 0 / 0.44));
		--_surface: var(--search-query-field-surface, #ffffff);

		/* Formulas mixed from the tokens, so hover and the hint chip follow any retone. */
		--_hairline-soft: color-mix(in oklab, var(--_hairline) 45%, transparent);
		--_fill-hover: color-mix(in oklab, var(--_surface), var(--_ink) 7%);
		--_accent-hover: color-mix(in oklab, var(--_accent), var(--_surface) 15%);
		--_ease: cubic-bezier(0.2, 0, 0, 1);

		/* Height per size. The inner radius sits 5 px in (the border and the submit's inset), so
		   the submit button and hint chip nest concentrically inside the field. */
		--_h: 2.25rem;
		--_r: 0.5rem;
		--_r-inner: max(2px, calc(var(--_r) - 5px));
		--_edge: var(--_hairline);
	}
	.search-query-field[data-size='sm'] {
		--_h: 2rem;
		--_r: 0.375rem;
	}
	.search-query-field[data-size='lg'] {
		--_h: 2.5rem;
	}
	.search-query-field[data-shape='pill'] {
		--_r: 9999px;
		--_r-inner: 9999px;
	}
	@media (pointer: coarse) {
		/* The border sits inside the height, so the input and buttons inside still clear 44 px. */
		.search-query-field[data-size] {
			--_h: calc(2.75rem + 2px);
		}
	}

	/*
	 * The edge and halo change through the :has() rules below, which follow the input inside, so
	 * the transition that animates them lives here with them.
	 */
	.search-query-field__control {
		transition-property: border-color, box-shadow;
		transition-duration: 150ms;
		transition-timing-function: var(--_ease);
	}
	@media (prefers-reduced-motion: reduce) {
		.search-query-field__control {
			transition: none;
		}
	}

	/* Hover only where typing is possible: one step darker edge. */
	@media (hover: hover) {
		.search-query-field__control:hover:not(:has(input:disabled)) {
			--_edge: color-mix(in oklab, var(--_hairline), var(--_ink) 25%);
		}
	}

	/* Focus: a two-pixel edge in the accent (border plus one ring) around the whole shape. It stays
	   while focus is on the clear or submit button, which draw their own ring inside it. */
	.search-query-field__control:has(:focus-visible) {
		border-color: var(--_accent);
		box-shadow:
			0 0 0 1px var(--_accent),
			0 0 0 4px color-mix(in oklab, var(--_accent) 14%, transparent);
	}

	/* Disabled: raised fill, faded, not interactive. */
	.search-query-field__control:has(input:disabled) {
		background-color: color-mix(in oklab, var(--_surface), var(--_ink) 4%);
		opacity: 0.55;
		cursor: not-allowed;
	}

	/* WebKit and Blink draw their own clear cross on type=search; the field has its own. */
	.search-query-field__input::-webkit-search-cancel-button,
	.search-query-field__input::-webkit-search-decoration {
		-webkit-appearance: none;
		appearance: none;
	}

	/* Forced colours drop shadows and repaint borders, so focus becomes a system-colour outline. */
	@media (forced-colors: active) {
		.search-query-field__control:has(:focus-visible) {
			outline: 2px solid Highlight;
			outline-offset: 2px;
		}
	}
</style>
```

## Artifacts

### Blue accent (`blue`)

- Artifact digest: `sha256-3b6823ca324e626640f856b502836eec72cae2afaa4d492cc072c717718a91e7`
- Entry: `SearchQueryField.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_search_query_field_01/1.0.0/blue/sha256-3b6823ca324e626640f856b502836eec72cae2afaa4d492cc072c717718a91e7/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_search_query_field_01/1.0.0/blue/sha256-3b6823ca324e626640f856b502836eec72cae2afaa4d492cc072c717718a91e7/bundle.zip (8593 bytes, sha256 `89e5d971057cf7d54b9c0a04462b4fc33a1e393d58f738256769ad8cd7f62d69`)

Files:

- `SearchQueryField.svelte` (entry, 14025 bytes): https://pagesugar.com/artifacts/cmp_search_query_field_01/1.0.0/blue/sha256-3b6823ca324e626640f856b502836eec72cae2afaa4d492cc072c717718a91e7/source/SearchQueryField.svelte
