Search field
Un campo de búsqueda de 48px con ícono y botón para borrar, a la altura del botón de al lado.
Instalación
$ pnpm dlx shadcn@latest add @ssntl/search-fieldUso
import { SearchField } from "@/components/ui/search-field"
<form role="search" className="flex gap-2">
<SearchField name="q" aria-label="Buscar pedidos" placeholder="Número, cliente o producto" clearLabel="Borrar búsqueda" className="flex-1" />
<Button type="submit" variant="secondary">Buscar</Button>
</form>- 48px, la altura de Button e Input, así un campo y su botón coinciden arriba, abajo y en el texto.
- type="search" dentro de un formulario role="search"; Enter lo envía.
- El botón para borrar aparece solo con texto, mantiene el foco en el campo y tiene su propio nombre (clearLabel).
Código
"use client"
import * as React from "react"
import { Search, X } from "lucide-react"
import { cn } from "@/lib/utils"
// A 48px search input, the height of Button and Input, so a field and its
// button line up when they sit side by side. The icon says what it is; the
// clear button appears once there is something to clear and keeps focus in
// the field. Enter submits the surrounding form.
function SearchField({
value,
defaultValue,
onChange,
onClear,
clearLabel = "Clear search",
className,
...props
}: Omit<React.ComponentProps<"input">, "type" | "value" | "defaultValue"> & {
value?: string
defaultValue?: string
/** Called after the clear button empties the field. */
onClear?: () => void
/** The clear button's accessible name. */
clearLabel?: string
}) {
const inputRef = React.useRef<HTMLInputElement>(null)
const [inner, setInner] = React.useState(defaultValue ?? "")
const current = value ?? inner
return (
<div
data-slot="search-field"
className={cn(
"flex h-12 min-w-0 items-center gap-3 rounded-sm border border-input bg-card ps-4 pe-1",
"transition-[border-color] duration-(--duration-fast) ease-out hover:border-muted-foreground",
"focus-within:border-foreground has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-ring",
className
)}
>
<Search
aria-hidden
strokeWidth={1.6}
className="size-5 shrink-0 text-muted-foreground"
/>
<input
ref={inputRef}
type="search"
value={current}
onChange={(event) => {
setInner(event.target.value)
onChange?.(event)
}}
className="h-full min-w-0 flex-1 bg-transparent text-body outline-none placeholder:text-muted-foreground [&::-webkit-search-cancel-button]:appearance-none"
{...props}
/>
{current && (
<button
type="button"
aria-label={clearLabel}
onClick={() => {
setInner("")
onClear?.()
inputRef.current?.focus()
}}
className="grid size-10 shrink-0 cursor-pointer place-items-center rounded-xs text-muted-foreground transition-[background-color,color] duration-(--duration-fast) ease-out hover:bg-secondary hover:text-foreground"
>
<X aria-hidden strokeWidth={1.6} className="size-4" />
</button>
)}
</div>
)
}
export { SearchField }