Component
Search Input
Search field with native search semantics.
- Component
- React
- TSX
- Tailwind CSS
- MIT
What it does
Capabilities
- Semantic HTML
- ARIA
- Associated labels
Preview
Live component
9:41 100%
devsnips.site/library/react/components/inputs/search-input/ fluid · no fixed width 100%
Install
Add to your project
npx devsnips add React/Components/Inputs/search-input React/Components/Inputs/search-input import type { ChangeEvent, InputHTMLAttributes, ReactNode } from "react";
import { useId, useState } from "react";
export type InputSize = "sm" | "md" | "lg";
export type InputTone = "default" | "error" | "success";
function cx(...parts: Array<string | false | null | undefined>): string {
return parts.filter(Boolean).join(" ");
}
const INPUT_SIZES: Record<InputSize, string> = {
sm: "h-8 px-2.5 text-[13px]",
md: "h-9 px-3 text-sm",
lg: "h-11 px-3.5 text-sm",
};
const ICON_SIZES: Record<InputSize, string> = {
sm: "[&_svg]:size-[14px]",
md: "[&_svg]:size-4",
lg: "[&_svg]:size-[18px]",
};
function inputClasses(size: InputSize, tone: InputTone, hasLeading = false, hasTrailing = false): string {
return cx(
"w-full rounded-[var(--ds-radius-sm)] border bg-[var(--ds-color-input)] text-[var(--ds-color-foreground)] shadow-none transition-colors duration-150 ease-out placeholder:text-[var(--ds-color-muted-foreground)] hover:bg-[var(--ds-color-input-hover,var(--ds-color-input))] focus:bg-[var(--ds-color-input-focus,var(--ds-color-input))] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] disabled:pointer-events-none disabled:bg-[var(--ds-color-muted)] disabled:text-[var(--ds-color-muted-foreground)] disabled:opacity-60 read-only:bg-[var(--ds-color-surface-subtle)] read-only:text-[var(--ds-color-muted-foreground)] motion-reduce:transition-none",
tone === "error" ? "border-[var(--ds-color-destructive)]" : tone === "success" ? "border-[var(--ds-color-success)]" : "border-[var(--ds-color-border)] focus:border-[var(--ds-color-border-strong)]",
INPUT_SIZES[size],
hasLeading && "pl-9",
hasTrailing && "pr-9",
);
}
function Spinner() {
return (
<svg className="size-4 animate-spin motion-reduce:animate-none" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="3" className="opacity-25" />
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
</svg>
);
}
export interface SearchInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "prefix"> {
label?: string;
helperText?: string;
error?: string;
success?: string;
size?: InputSize;
tone?: InputTone;
leadingIcon?: ReactNode;
trailingIcon?: ReactNode;
prefix?: ReactNode;
suffix?: ReactNode;
}
export function SearchInput({
label = "Search",
helperText,
error,
success,
size = "md",
tone = "default",
leadingIcon,
trailingIcon,
prefix,
suffix,
id,
className,
type = "search",
disabled,
readOnly,
...props
}: SearchInputProps) {
const generatedId = useId();
const inputId = id ?? `input-${generatedId}`;
const messageId = `${inputId}-message`;
const message = error ?? success ?? helperText;
const resolvedTone: InputTone = error ? "error" : success ? "success" : tone;
const hasLeading = Boolean(leadingIcon ?? prefix);
const hasTrailing = Boolean(trailingIcon ?? suffix);
return (
<div className="w-full">
<label htmlFor={inputId} className="mb-2 block text-[13px] font-medium leading-5 text-[var(--ds-color-foreground)]">
{label}
</label>
<div className="relative flex items-center">
{hasLeading ? (
<span className={cx("pointer-events-none absolute left-3 text-[var(--ds-color-muted-foreground)]", ICON_SIZES[size])}>
{prefix ?? leadingIcon}
</span>
) : null}
<input
id={inputId}
type={type}
className={cx(inputClasses(size, resolvedTone, hasLeading, hasTrailing), className)}
disabled={disabled}
readOnly={readOnly}
aria-invalid={error ? true : undefined}
aria-describedby={message ? messageId : undefined}
{...props}
/>
{hasTrailing ? (
<span className={cx("pointer-events-none absolute right-3 text-[var(--ds-color-muted-foreground)]", ICON_SIZES[size])}>
{suffix ?? trailingIcon}
</span>
) : null}
</div>
{message ? (
<p id={messageId} className={cx("mt-2 text-xs", error ? "text-[var(--ds-color-destructive)]" : success ? "text-[var(--ds-color-success)]" : "text-[var(--ds-color-muted-foreground)]")}>
{error ? `Error: ${error}` : success ? `Success: ${success}` : helperText}
</p>
) : null}
</div>
);
}
export default SearchInput; /* DevSnips React — JavaScript parity build. Same API and behavior as code.tsx; TypeScript types removed. */
import { useId, useState } from "react";
function cx(...parts) {
return parts.filter(Boolean).join(" ");
}
const INPUT_SIZES = {
sm: "h-8 px-2.5 text-[13px]",
md: "h-9 px-3 text-sm",
lg: "h-11 px-3.5 text-sm",
};
const ICON_SIZES = {
sm: "[&_svg]:size-[14px]",
md: "[&_svg]:size-4",
lg: "[&_svg]:size-[18px]",
};
function inputClasses(size, tone, hasLeading = false, hasTrailing = false) {
return cx(
"w-full rounded-[var(--ds-radius-sm)] border bg-[var(--ds-color-input)] text-[var(--ds-color-foreground)] shadow-none transition-colors duration-150 ease-out placeholder:text-[var(--ds-color-muted-foreground)] hover:bg-[var(--ds-color-input-hover,var(--ds-color-input))] focus:bg-[var(--ds-color-input-focus,var(--ds-color-input))] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] disabled:pointer-events-none disabled:bg-[var(--ds-color-muted)] disabled:text-[var(--ds-color-muted-foreground)] disabled:opacity-60 read-only:bg-[var(--ds-color-surface-subtle)] read-only:text-[var(--ds-color-muted-foreground)] motion-reduce:transition-none",
tone === "error" ? "border-[var(--ds-color-destructive)]" : tone === "success" ? "border-[var(--ds-color-success)]" : "border-[var(--ds-color-border)] focus:border-[var(--ds-color-border-strong)]",
INPUT_SIZES[size],
hasLeading && "pl-9",
hasTrailing && "pr-9",
);
}
function Spinner() {
return (
<svg className="size-4 animate-spin motion-reduce:animate-none" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="3" className="opacity-25" />
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
</svg>
);
}
export function SearchInput({
label = "Search",
helperText,
error,
success,
size = "md",
tone = "default",
leadingIcon,
trailingIcon,
prefix,
suffix,
id,
className,
type = "search",
disabled,
readOnly,
...props
}) {
const generatedId = useId();
const inputId = id ?? `input-${generatedId}`;
const messageId = `${inputId}-message`;
const message = error ?? success ?? helperText;
const resolvedTone = error ? "error" : success ? "success" : tone;
const hasLeading = Boolean(leadingIcon ?? prefix);
const hasTrailing = Boolean(trailingIcon ?? suffix);
return (
<div className="w-full">
<label htmlFor={inputId} className="mb-2 block text-[13px] font-medium leading-5 text-[var(--ds-color-foreground)]">
{label}
</label>
<div className="relative flex items-center">
{hasLeading ? (
<span className={cx("pointer-events-none absolute left-3 text-[var(--ds-color-muted-foreground)]", ICON_SIZES[size])}>
{prefix ?? leadingIcon}
</span>
) : null}
<input
id={inputId}
type={type}
className={cx(inputClasses(size, resolvedTone, hasLeading, hasTrailing), className)}
disabled={disabled}
readOnly={readOnly}
aria-invalid={error ? true : undefined}
aria-describedby={message ? messageId : undefined}
{...props}
/>
{hasTrailing ? (
<span className={cx("pointer-events-none absolute right-3 text-[var(--ds-color-muted-foreground)]", ICON_SIZES[size])}>
{suffix ?? trailingIcon}
</span>
) : null}
</div>
{message ? (
<p id={messageId} className={cx("mt-2 text-xs", error ? "text-[var(--ds-color-destructive)]" : success ? "text-[var(--ds-color-success)]" : "text-[var(--ds-color-muted-foreground)]")}>
{error ? `Error: ${error}` : success ? `Success: ${success}` : helperText}
</p>
) : null}
</div>
);
}
export default SearchInput; # Search Input
Search field with native search semantics.
## Usage
```tsx
import { SearchInput } from './code';
<SearchInput label="Workspace name" placeholder="Acme design" />
```
## JavaScript
```jsx
import { SearchInput } from './code';
<SearchInput label="Billing email" placeholder="[email protected]" />
```
## Props
| Name | Type | Default | Description |
|---|---|---:|---|
| `label` | `string` | component label | Visible label associated with the input. |
| `helperText` | `string` | `undefined` | Supporting text announced through `aria-describedby`. |
| `error` | `string` | `undefined` | Error message; sets `aria-invalid`. |
| `success` | `string` | `undefined` | Success message associated with the control. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Control height and text sizing. |
| `tone` | `"default" \| "error" \| "success"` | `"default"` | Visual state when no message prop overrides it. |
| `className` | `string` | `undefined` | Adds layout classes to the input element. |
| native input props | `InputHTMLAttributes<HTMLInputElement>` | — | Supports `value`, `defaultValue`, `onChange`, `name`, `id`, `placeholder`, `disabled`, `readOnly`, `required`, and `aria-*`. |
## States
Supports default, hover, focus-visible, disabled, readonly, error, success, and loading where applicable.
## Accessibility
Uses a native `input`, a visible `label`, associated helper/error text, `aria-invalid` for errors, and keyboard-accessible buttons for interactive controls.
## Styling
Built with React, Tailwind CSS, and DevSnips design tokens. No component-specific CSS file is required.
## Design Tokens
Visual decisions reference `React/DESIGN_TOKENS.md` rather than duplicating the token system. 117 lines UTF-8 · LF · Spaces: 2
DevSnips AI ready Claude · Cursor · Windsurf
Agent skill & Model Context Protocol
Pass this component straight into your AI workflow. DevSnips exposes each resource as a structured, addressable registry entry, so your agent installs the real files instead of approximating them.
devsnips://components/inputs/search-input I want to use this DevSnips resource in production: Page: https://devsnips.site/library/react/components/inputs/search-input/ Install: npx devsnips add React/Components/Inputs/search-input This is the Search Input — Search field with native search semantics. Use this DevSnips resource as the starting point for my project. Adapt it to my project's stack and conventions, fix any issues, and make it production-ready while preserving its intended design and behavior.
Paste into ChatGPT, Claude or your agent — it already carries the page URL and the exact install path.
Resource URI
devsnips://components/inputs/search-input