The field is a controlled input built on Input Group: the magnifier sits in a leading addon and the clear button in a trailing one, so nothing is absolutely positioned over the text and the caret never runs under an icon.
What this adds over Input Group
InputGroup with a SearchIcon addon gets you the layout. Use it directly if that is all you need. SearchInput adds two behaviours:
A clear button wired to the value. It appears only while value is non-empty, carries the accessible name “Clear search”, and puts focus back in the input after clearing. Hand-rolled ones routinely miss that last part and drop a keyboard user at the top of the document.
A cycling placeholder that respects reduced motion. The global reduced-motion stylesheet cannot reach a JavaScript typewriter, so the gate lives in the component.
It renders a native type="search", which carries role="searchbox" for free, and sets autoComplete="off" so the browser’s own history dropdown does not cover your results. WebKit’s built-in cancel button is suppressed: otherwise you get two clear affordances side by side.
Clearing
The button exists only while value is non-empty. Override its “Clear search” name with clearLabel, and pass onClear to reset anything alongside the query.
Cycling placeholder
Pass placeholders to type and delete a list of phrases in the placeholder.
Loading...
"use client";import { useState } from "react";import { SearchInput } from "@/components/ui/search-input";// Hoisted so the array identity is stable across renders.const PLACEHOLDERS = [ "Search invoices", "Search customers", "Search subscriptions", "Search payouts",];export const SearchInputCycling = () => { const [query, setQuery] = useState(""); return ( <div className="w-full max-w-sm"> <SearchInput onValueChange={setQuery} placeholder="Search your workspace" placeholders={PLACEHOLDERS} value={query} /> </div> );};
The animation stops while the input has focus, stops as soon as the field holds a value, and never starts under reduced motion. The component subscribes to matchMedia("(prefers-reduced-motion: reduce)") itself and reacts to changes in it, rather than reading the preference once at mount.
Assistive technology gets the stable string: placeholder is exposed through aria-placeholder whatever the animation is doing. It also becomes the input’s accessible name when you supply no aria-label, aria-labelledby, or id. An id is taken to mean a visible <label htmlFor> already names the field, and overriding a visible label would break voice control.