Search Input
Search Input for a useful, keyboard-friendly product interface.
Make it yours.
Install the editable source and its dependencies into your React + Tailwind project.
Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/search-input.jsonUse it
Scroll horizontally for long lines
"use client";
import {useState} from 'react';
import {SearchInput} from '@/components/jez-ui/ui/search-input';
import {Folder} from 'lucide-react';
import {ArrowUpRight} from 'lucide-react';
export default function Example(){
const [searchTerm,setSearchTerm]=useState('');
const [notice,setNotice]=useState('');
return <><div className="w-full max-w-sm"><div className="mb-7"><h3 className="text-2xl tracking-tight">Find the thing.</h3><p className="mt-2 text-sm leading-relaxed text-muted-foreground">Projects, notes, and the details you nearly forgot.</p></div><div><SearchInput value={searchTerm} onValueChange={setSearchTerm} placeholder="Search projects…"/><div className="mt-3 overflow-hidden rounded-lg border border-border">{["Website refresh","Brand guidelines","Component library"].filter(t=>t.toLowerCase().includes(searchTerm.toLowerCase())).map(t=><button key={t} onClick={()=>setNotice(t+" opened.")} className="flex w-full items-center gap-3 border-b border-border/50 px-3 py-3 text-left text-sm last:border-0 hover:bg-muted"><Folder size={15} className="text-muted-foreground"/>{t}<ArrowUpRight size={13} className="ml-auto text-muted-foreground"/></button>)}{!["Website refresh","Brand guidelines","Component library"].some(t=>t.toLowerCase().includes(searchTerm.toLowerCase()))&&<p className="p-5 text-sm text-muted-foreground">No projects found.</p>}</div></div></div><p role="status">{notice}</p></>;
}Props & types
Scroll horizontally for long lines
Omit<
React.ComponentProps<typeof Input>,
"value" | "defaultValue" | "onChange"
> & {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
loading?: boolean;
}Source
View 4 source files
registry/ui/search-input.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { Search, X, LoaderCircle } from "lucide-react";
import { Input } from "./input";
import { useControllable } from "./use-controllable";
import { cn } from "./utils";
export function SearchInput({
value,
defaultValue = "",
onValueChange,
className,
loading = false,
...props
}: Omit<
React.ComponentProps<typeof Input>,
"value" | "defaultValue" | "onChange"
> & {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
loading?: boolean;
}) {
const [query, setQuery] = useControllable(value, defaultValue, onValueChange);
const ref = React.useRef<HTMLInputElement>(null);
return (
<div role="search" className={cn("group relative w-full", className)}>
{loading ? (
<LoaderCircle
size={16}
className="absolute left-3 top-3 animate-spin text-primary"
/>
) : (
<Search
size={16}
className="pointer-events-none absolute left-3 top-3 text-muted-foreground transition-colors group-focus-within:text-primary"
/>
)}
<Input
ref={ref}
type="search"
aria-label="Search"
placeholder="Search…"
{...props}
value={query}
onChange={(e) => setQuery(e.target.value)}
className="px-10 [&::-webkit-search-cancel-button]:appearance-none"
/>
{query && (
<button
type="button"
disabled={props.disabled}
aria-label="Clear search"
onClick={() => {
setQuery("");
ref.current?.focus();
}}
className="absolute right-1 top-1 grid size-8 place-items-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground"
>
<X size={14} />
</button>
)}
</div>
);
}
registry/ui/input.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "./utils";
export function Input({ className, ...props }: React.ComponentProps<"input">) {
return (
<input
className={cn(
"block h-10 w-full min-w-0 rounded-lg border border-border bg-background px-3 text-sm leading-normal shadow-[0_1px_2px_#00000004] transition-[border-color,box-shadow] placeholder:text-muted-foreground/75 hover:border-foreground/25 focus:border-primary focus:outline-none focus:ring-3 focus:ring-primary/10 aria-invalid:border-danger aria-invalid:ring-danger/10 disabled:cursor-not-allowed disabled:bg-muted/50 disabled:opacity-50",
className,
)}
{...props}
/>
);
}
registry/ui/utils.ts
Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
return twMerge(clsx(values));
}
registry/ui/use-controllable.ts
Scroll horizontally for long lines
"use client";
import { useState } from "react";
export function useControllable<T>(
value: T | undefined,
initial: T,
onChange?: (value: T) => void,
) {
const [local, setLocal] = useState(initial);
return [
value === undefined ? local : value,
(next: T) => {
if (value === undefined) setLocal(next);
onChange?.(next);
},
] as const;
}
Dependencies
lucide-react@0.577.0 · clsx@2.1.1 · tailwind-merge@3.5.0
Accessibility & behaviour
Provide meaningful labels and preserve keyboard focus styles. Check contrast when customising theme colours.
Read the accessibility and performance guide →