Suchfeld
Das Suchfeld mit Tastenkürzel, Leeren-Knopf, Escape zum Leeren, Ladezustand und Absendeknopf.
Eine von 9 Components in Forms und Inputs
Details
Was Suchfeld mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Forms und Inputs
- Bereich
- Elements
- Variants
- 2
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/search-field.tsx
- Bringt mit
- utilskiticoninputloading-button
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 5 eigene Texte
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle SearchFieldProps aus components/ui/search-field.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'onChange' | 'value' | 'defaultValue'> an.
valueoptionalstringdefaultValueoptionalstringonValueChangeoptional(value: string) => voidonSearchoptional(value: string) => voidCalled on Enter and on the submit button
sizeoptionalControlSizeshortcutoptionalstringA key that focuses the field from anywhere, shown as a hint: "k" means Ctrl K or ⌘K, "/" means slash
loadingoptionalbooleanShows a spinner instead of the search icon
submitLabeloptionalstringA submit button at the end
variantoptional'default' | 'soft'soft: filled surface without border, for headers and toolbars
labeloptionalstringAccessible name, when there is no visible label
Eingebaute Texte
Diese Texte schreibt Suchfeld selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 5 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| label | Englisch Search | Deutsch Suche |
| placeholder | Englisch Search | Deutsch Suchen |
| clear | Englisch Clear search | Deutsch Suche leeren |
| submit | Englisch Search | Deutsch Suchen |
| searching | Englisch Searching | Deutsch Sucht |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Suchfeld reagiert selbst auf die Tasten Escape und Leertaste.
- Setzt aria-label, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rolle search.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 3 Stellen im Code schalten Bewegung dann ab.
- Ohne Animationen, sie steht sofort ruhig da.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 165 Zeilen
'use client';
import { useEffect, useRef, useState } from 'react';
import { cn, prefersReducedMotion, useTexts } from '@/lib/kit';
import { Icon } from '@/components/ui/icon';
import { CONTROL_HEIGHT, InlineButton, controlBox, type ControlSize } from '@/components/ui/input';
import { Spinner } from '@/components/ui/loading-button';
const TEXTS = {
en: { label: 'Search', placeholder: 'Search', clear: 'Clear search', submit: 'Search', searching: 'Searching' },
de: { label: 'Suche', placeholder: 'Suchen', clear: 'Suche leeren', submit: 'Suchen', searching: 'Sucht' },
};
export interface SearchFieldProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'onChange' | 'value' | 'defaultValue'> {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
/** Called on Enter and on the submit button */
onSearch?: (value: string) => void;
size?: ControlSize;
/** A key that focuses the field from anywhere, shown as a hint: "k" means Ctrl K or ⌘K, "/" means slash */
shortcut?: string;
/** Shows a spinner instead of the search icon */
loading?: boolean;
/** A submit button at the end */
submitLabel?: string;
/** soft: filled surface without border, for headers and toolbars */
variant?: 'default' | 'soft';
/** Accessible name, when there is no visible label */
label?: string;
labels?: Partial<(typeof TEXTS)['en']>;
className?: string;
}
function Kbd({ children }: { children: React.ReactNode }) {
return <kbd data-cap className="grid h-[22px] min-w-[22px] place-items-center rounded-[5px] border border-border bg-surface px-1.5 font-sans text-[11px] font-medium text-muted-foreground shadow-[0_1px_0_var(--border)]">{children}</kbd>;
}
/** The key caps press down one after the other, as if the shortcut was typed on them */
function pressCaps(root: HTMLElement | null) {
if (!root || prefersReducedMotion()) return;
root.querySelectorAll<HTMLElement>('[data-cap]').forEach((cap, i) => {
if (typeof cap.animate !== 'function') return;
cap.animate(
[
{ transform: 'translateY(0)', boxShadow: '0 1px 0 var(--border)' },
{ transform: 'translateY(1.5px) scale(0.94)', boxShadow: '0 0 0 var(--border)', offset: 0.35 },
{ transform: 'translateY(0)', boxShadow: '0 1px 0 var(--border)' },
],
{ duration: 260, delay: i * 60, easing: 'cubic-bezier(0.2,0.7,0.2,1)' },
);
});
}
/**
* The search box: icon, clear button, Escape to clear, a keyboard shortcut and a loading state.
* <SearchField shortcut="k" placeholder="Search invoices" onSearch={run} />
*/
export function SearchField({ value: valueProp, defaultValue, onValueChange, onSearch, size = 'md', shortcut, loading, submitLabel, variant = 'default', label, labels, className, placeholder, ...rest }: SearchFieldProps) {
const t = useTexts(TEXTS, labels);
const [inner, setInner] = useState(defaultValue ?? '');
const value = valueProp ?? inner;
const ref = useRef<HTMLInputElement>(null);
const hintRef = useRef<HTMLSpanElement>(null);
const [mac, setMac] = useState(true);
useEffect(() => {
setMac(/Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent));
if (!shortcut) return;
const onKey = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
const typing = target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName);
if (shortcut === '/' ? e.key === '/' && !typing : e.key.toLowerCase() === shortcut.toLowerCase() && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
pressCaps(hintRef.current);
ref.current?.focus();
ref.current?.select();
}
};
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [shortcut]);
const set = (v: string) => {
setInner(v);
onValueChange?.(v);
};
return (
<form
role="search"
onSubmit={(e) => {
e.preventDefault();
onSearch?.(value);
}}
className={cn(
// Focus widens the box a hair and lights the icon, the ring fades in from the shared control box
'group/search relative flex w-full min-w-0 items-center focus-within:scale-[1.012] motion-reduce:focus-within:scale-100',
CONTROL_HEIGHT[size],
controlBox,
variant === 'soft' && 'border-transparent bg-muted shadow-none hover:border-transparent hover:bg-[color-mix(in_oklab,var(--foreground)_6%,var(--muted))] focus-within:bg-background',
submitLabel && 'pe-1',
className,
)}
>
<span className={cn('grid shrink-0 place-items-center text-subtle-foreground transition-[color,scale] duration-300 ease-[var(--ease-kit)] group-focus-within/search:scale-110 group-focus-within/search:text-[color-mix(in_oklab,var(--primary)_70%,var(--foreground))]', size === 'sm' ? 'ms-3' : 'ms-3.5')}>
{loading ? <Spinner size={16} label={t.searching} /> : <Icon name="search" size={size === 'lg' ? 18 : 16} />}
</span>
<input
{...rest}
ref={ref}
type="search"
value={value}
placeholder={placeholder ?? t.placeholder}
aria-label={label ?? t.label}
enterKeyHint="search"
onChange={(e) => set(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape' && value) {
e.preventDefault();
set('');
}
rest.onKeyDown?.(e);
}}
className="h-full w-0 min-w-0 flex-1 bg-transparent ps-2.5 pe-2 text-inherit outline-none placeholder:text-subtle-foreground [&::-webkit-search-cancel-button]:appearance-none"
/>
{value ? (
<span className="pe-1.5 transition-[opacity,scale] duration-200 starting:scale-50 starting:opacity-0 motion-reduce:transition-none">
<InlineButton
label={t.clear}
icon="x"
onClick={() => {
set('');
ref.current?.focus();
}}
/>
</span>
) : (
shortcut && (
<span ref={hintRef} aria-hidden className="flex shrink-0 items-center gap-1 pe-2.5 transition-[opacity,translate] duration-300 ease-[var(--ease-kit)] group-focus-within/search:translate-x-1 group-focus-within/search:opacity-40 max-sm:hidden">
{shortcut === '/' ? (
<Kbd>/</Kbd>
) : (
<>
<Kbd>{mac ? '⌘' : 'Ctrl'}</Kbd>
<Kbd>{shortcut.toUpperCase()}</Kbd>
</>
)}
</span>
)
)}
{submitLabel && (
<button
type="submit"
className={cn(
'inline-flex shrink-0 items-center rounded-md bg-primary px-3.5 text-[13px] font-medium text-primary-foreground shadow-[inset_0_1px_0_rgb(255_255_255/0.18)] transition-[background-color,scale] duration-200 hover:bg-[color-mix(in_oklab,var(--primary)_88%,var(--background))] active:scale-[0.96] active:duration-100 motion-reduce:active:scale-100',
size === 'sm' ? 'h-7' : size === 'md' ? 'h-9' : 'h-10',
)}
>
{submitLabel}
</button>
)}
</form>
);
}Häufige Fragen
Wie installiere ich Suchfeld?
Mit aiw add @ai-website/search-field. Der Befehl legt die Datei components/ui/search-field.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon, input und loading-button. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Suchfeld?
2 Varianten, Suchfeld und In einer Kopfzeile.
Kann ich die Texte von Suchfeld übersetzen?
Ja. Suchfeld bringt 5 eigene Texte auf Deutsch und Englisch mit, etwa „Suche“, „Suchen“ und „Suche leeren“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Suchfeld einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Suchfeld mit aiw_components und installieren sie mit aiw_add @ai-website/search-field. Nenn im Prompt die Variante, etwa variant="soft", dann schreibt der Assistent deine Inhalte direkt in die Props.