Components · Forms
Search field
A search input in a role=search form, with a / shortcut and a clear button.
9.7% of the 2,062 sites in The UI Index have a search (200 sites).
Both previews render the same markup; the HTML tab below is that markup without a framework.
Install
- React
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ search-field.json - Astro
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ astro/ search-field.json
Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS and the script.
// Search field behaviour, shared by the Astro, React and HTML versions: "/" (outside any field) focuses the page's
// first visible [data-search] input; the clear button empties the field, fires input, and puts focus back in it.
declare global {
interface Window {
__caseroomSearch?: boolean;
}
}
if (typeof window !== 'undefined' && !window.__caseroomSearch) {
window.__caseroomSearch = true;
document.addEventListener('keydown', (e) => {
if (e.key !== '/' || e.metaKey || e.ctrlKey || e.altKey) return;
const t = e.target as HTMLElement;
if (t.closest('input, textarea, select, [contenteditable=""], [contenteditable=true]')) return;
// The first one you can see (a page may keep others in a closed panel or menu).
const input = [...document.querySelectorAll<HTMLInputElement>('input[data-search]')].find((i) => i.getClientRects().length > 0);
if (!input) return;
e.preventDefault();
input.focus();
});
document.addEventListener('click', (e) => {
const clear = (e.target as Element).closest<HTMLButtonElement>('[data-search-clear]');
if (!clear) return;
const input = clear.parentElement?.querySelector<HTMLInputElement>('input[data-search]');
if (!input) return;
input.value = '';
input.dispatchEvent(new Event('input', { bubbles: true }));
input.focus();
});
}
export {};import { cn } from '@/lib/utils';
import './search-field.client';
import { CLEAR_PATH, search, SEARCH_PATH } from './search-field.variants';
export interface SearchFieldProps {
id: string;
/** The accessible name, e.g. "Search the docs". */
label: string;
placeholder?: string;
/** Where the form goes (GET), e.g. "/search". */
action?: string;
name?: string;
defaultValue?: string;
className?: string;
}
/** A search field in a role=search form, with a "/" shortcut and a clear button. */
export function SearchField({ id, label, placeholder = 'Search', action, name = 'q', defaultValue, className }: SearchFieldProps) {
return (
<form role="search" action={action} className={cn(search.root, className)}>
<label htmlFor={id} className="sr-only">{label}</label>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true" className={search.icon}><path d={SEARCH_PATH} /></svg>
<input type="search" id={id} name={name} defaultValue={defaultValue} placeholder={placeholder} autoComplete="off" data-search="" className={search.input} />
<kbd className={search.hint} aria-hidden="true">/</kbd>
<button type="button" data-search-clear="" aria-label="Clear search" className={search.clear}>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true"><path d={CLEAR_PATH} /></svg>
</button>
</form>
);
}import { fieldClasses } from '../input/input.variants';
/** A search field in a role=search form: magnifier, a native search input, a "/" shortcut hint that gives way while
* you type, and a clear button that appears once there is text. "/" anywhere outside a field focuses it. */
export const search = {
root: 'relative w-full',
icon: 'pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground',
input: `${fieldClasses} peer h-9 py-1 pr-10 pl-9 [&::-webkit-search-cancel-button]:appearance-none`,
hint: 'pointer-events-none absolute top-1/2 right-2 inline-flex h-5 min-w-5 -translate-y-1/2 items-center justify-center rounded border bg-muted px-1 font-sans text-[11px] text-muted-foreground peer-focus:hidden peer-[:not(:placeholder-shown)]:hidden',
clear:
'absolute top-1/2 right-1.5 hidden size-7 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed peer-[:not(:placeholder-shown)]:inline-flex [&_svg]:size-3.5',
};
export const SEARCH_PATH = 'M7 12.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM11 11l3.5 3.5';
export const CLEAR_PATH = 'M4 4l8 8M12 4l-8 8';---
import { cn } from '@/lib/utils';
import { CLEAR_PATH, search, SEARCH_PATH } from './search-field.variants';
interface Props {
id: string;
/** The accessible name, e.g. "Search the docs". */
label: string;
placeholder?: string;
/** Where the form goes (GET), e.g. "/search". */
action?: string;
name?: string;
value?: string;
class?: string;
}
const { id, label, placeholder = 'Search', action, name = 'q', value, class: className } = Astro.props;
---
<form role="search" action={action} class={cn(search.root, className)}>
<label for={id} class="sr-only">{label}</label>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true" class={search.icon}><path d={SEARCH_PATH} /></svg>
<input type="search" id={id} name={name} value={value} placeholder={placeholder} autocomplete="off" data-search class={search.input} />
<kbd class={search.hint} aria-hidden="true">/</kbd>
<button type="button" data-search-clear aria-label="Clear search" class={search.clear}>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d={CLEAR_PATH} /></svg>
</button>
</form>
<script>
import './search-field.client';
</script>// Search field behaviour, shared by the Astro, React and HTML versions: "/" (outside any field) focuses the page's
// first visible [data-search] input; the clear button empties the field, fires input, and puts focus back in it.
declare global {
interface Window {
__caseroomSearch?: boolean;
}
}
if (typeof window !== 'undefined' && !window.__caseroomSearch) {
window.__caseroomSearch = true;
document.addEventListener('keydown', (e) => {
if (e.key !== '/' || e.metaKey || e.ctrlKey || e.altKey) return;
const t = e.target as HTMLElement;
if (t.closest('input, textarea, select, [contenteditable=""], [contenteditable=true]')) return;
// The first one you can see (a page may keep others in a closed panel or menu).
const input = [...document.querySelectorAll<HTMLInputElement>('input[data-search]')].find((i) => i.getClientRects().length > 0);
if (!input) return;
e.preventDefault();
input.focus();
});
document.addEventListener('click', (e) => {
const clear = (e.target as Element).closest<HTMLButtonElement>('[data-search-clear]');
if (!clear) return;
const input = clear.parentElement?.querySelector<HTMLInputElement>('input[data-search]');
if (!input) return;
input.value = '';
input.dispatchEvent(new Event('input', { bubbles: true }));
input.focus();
});
}
export {};import { fieldClasses } from '../input/input.variants';
/** A search field in a role=search form: magnifier, a native search input, a "/" shortcut hint that gives way while
* you type, and a clear button that appears once there is text. "/" anywhere outside a field focuses it. */
export const search = {
root: 'relative w-full',
icon: 'pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground',
input: `${fieldClasses} peer h-9 py-1 pr-10 pl-9 [&::-webkit-search-cancel-button]:appearance-none`,
hint: 'pointer-events-none absolute top-1/2 right-2 inline-flex h-5 min-w-5 -translate-y-1/2 items-center justify-center rounded border bg-muted px-1 font-sans text-[11px] text-muted-foreground peer-focus:hidden peer-[:not(:placeholder-shown)]:hidden',
clear:
'absolute top-1/2 right-1.5 hidden size-7 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed peer-[:not(:placeholder-shown)]:inline-flex [&_svg]:size-3.5',
};
export const SEARCH_PATH = 'M7 12.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM11 11l3.5 3.5';
export const CLEAR_PATH = 'M4 4l8 8M12 4l-8 8';<div class="grid w-full max-w-sm gap-4">
<form role="search" action="#" class="relative w-full">
<label for="ex-search" class="sr-only">Search the docs</label>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true" class="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground"><path d="M7 12.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM11 11l3.5 3.5"></path></svg>
<input type="search" id="ex-search" name="q" placeholder="Search the docs" autocomplete="off" data-search class="w-full min-w-0 rounded-md border border-input bg-background px-3 text-sm text-foreground transition-colors placeholder:text-muted-foreground enabled:hover:border-foreground/85 enabled:active:bg-muted aria-invalid:enabled:hover:border-destructive aria-invalid:enabled:hover:bg-[color-mix(in_oklab,var(--destructive)_6%,var(--background))] aria-invalid:enabled:active:bg-[color-mix(in_oklab,var(--destructive)_10%,var(--background))] focus-visible:border-ring focus-visible:outline-2 focus-visible:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:outline-destructive peer h-9 py-1 pr-10 pl-9 [&::-webkit-search-cancel-button]:appearance-none">
<kbd class="pointer-events-none absolute top-1/2 right-2 inline-flex h-5 min-w-5 -translate-y-1/2 items-center justify-center rounded border bg-muted px-1 font-sans text-[11px] text-muted-foreground peer-focus:hidden peer-[:not(:placeholder-shown)]:hidden" aria-hidden="true">
/
</kbd>
<button type="button" data-search-clear aria-label="Clear search" class="absolute top-1/2 right-1.5 hidden size-7 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed peer-[:not(:placeholder-shown)]:inline-flex [&_svg]:size-3.5">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"></path></svg>
</button>
</form>
<form role="search" action="#" class="relative w-full">
<label for="ex-search-filled" class="sr-only">Search deploys</label>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true" class="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground"><path d="M7 12.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM11 11l3.5 3.5"></path></svg>
<input type="search" id="ex-search-filled" name="q" value="pricing" placeholder="Search" autocomplete="off" data-search class="w-full min-w-0 rounded-md border border-input bg-background px-3 text-sm text-foreground transition-colors placeholder:text-muted-foreground enabled:hover:border-foreground/85 enabled:active:bg-muted aria-invalid:enabled:hover:border-destructive aria-invalid:enabled:hover:bg-[color-mix(in_oklab,var(--destructive)_6%,var(--background))] aria-invalid:enabled:active:bg-[color-mix(in_oklab,var(--destructive)_10%,var(--background))] focus-visible:border-ring focus-visible:outline-2 focus-visible:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:outline-destructive peer h-9 py-1 pr-10 pl-9 [&::-webkit-search-cancel-button]:appearance-none">
<kbd class="pointer-events-none absolute top-1/2 right-2 inline-flex h-5 min-w-5 -translate-y-1/2 items-center justify-center rounded border bg-muted px-1 font-sans text-[11px] text-muted-foreground peer-focus:hidden peer-[:not(:placeholder-shown)]:hidden" aria-hidden="true">
/
</kbd>
<button type="button" data-search-clear aria-label="Clear search" class="absolute top-1/2 right-1.5 hidden size-7 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed peer-[:not(:placeholder-shown)]:inline-flex [&_svg]:size-3.5">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"></path></svg>
</button>
</form>
</div><script type="module" src="/caseroom/r/html/search-field.js"></script>