Glow Search
A search field rather than a text field: leading icon, a clear button, a ⌘K hint and a debounced onSearch.
InputsClient componentlucide-react
Preview
⌘K
typed —filtered —
- Reedbank Way
- Mallard Flats
- Heron Cut
- Teal Basin
- Coot Hollow
- Widgeon Reach
Installation
The CLI writes the source into your project and pulls in whatever it depends on.
$
pnpm dlx shadcn@latest add @duck/glow-searchPulls in @duck/theme, @duck/glow-input, @duck/sticker-kbd. Already installed items are skipped.
Usage
import { GlowSearch } from "@/components/ui/glow-search"The file lands in components/ui/glow-search.tsx and belongs to you from that point on. Edit it in place rather than wrapping it.
Props
Everything not listed here is forwarded to the underlying element.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | - | Controlled query. Pair with onChange, as with any input. |
| defaultValue | string | "" | Uncontrolled starting query. |
| onSearch | (value: string) => void | - | The debounced query, after the typing stops. Hang the fetch or the filter off this one; onChange still fires per keystroke. |
| debounce | number | 250 | Milliseconds of quiet before onSearch fires. 0 fires on every keystroke. |
| kbd | ReactNode | - | Keycap hint drawn in a StickerKbd while the field is empty — "⌘K", "/". A hint, not a binding: nothing global is registered here. |
| placeholder | string | "Search" | Also the fallback accessible name when nothing else names the field. |
| clearLabel | string | "Clear search" | Accessible name of the clear button. |
| className | string | - | Styles the frame. Every other prop lands on the field. |
| inputClassName | string | - | Styles the field inside the frame, for the rare case the type has to move. |
Rules
What keeps this component from turning into noise.
- onChange fires on every keystroke, onSearch once the typing stops. Put the query on onSearch — that is the whole reason the component exists.
- Typing debounces; Enter, Escape and the clear button flush. All three are decisions rather than keystrokes on the way to one, and Enter fires even if the timer already ran.
- Escape clears only when there is a value. On an empty field the key goes through to the dialog or palette above, so a search box inside one is never a trap.
- The ⌘K hint is a hint. This component registers no global listener — bind the shortcut where the palette lives, and print the key the platform actually uses (⌘ on Apple, Ctrl elsewhere, decided at runtime).
- The frame is on the wrapper and the field inside it is a frame={false} GlowInput, so the icon, the field and the clear button share one 3px edge and one focus glow. Do not nest it in another bordered box.
- Native type="search" for the searchbox role and the search return key on mobile; the WebKit cancel button is suppressed because the component draws its own clear.
- If this is the page's primary search, wrap it in a <search> element or a form with role="search". The component does not claim a landmark on its own — a filter box in a toolbar is not the site search.
- The clear button clears by dispatching a real input event, so a controlled consumer needs nothing beyond the onChange it already has, and focus goes back to the field.
