Component
Search Input With Actions
Search input with clear and submit controls.
- 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-with-actions/ fluid · no fixed width 100%
Install
Add to your project
npx devsnips add React/Components/Inputs/search-input-with-actions React/Components/Inputs/search-input-with-actions 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 SearchInputWithActionsProps 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;
clearLabel?: string;
onClear?: () => void;
onSearch?: (value: string) => void;
searchLabel?: string;
}
export function SearchInputWithActions({ label = "Search with Actions", helperText, error, success, size = "md", tone = "default", id, className, value, defaultValue = "", onChange, clearLabel = "Clear input", onClear, onSearch, searchLabel = "Search", ...props }: SearchInputWithActionsProps) {
const generatedId = useId();
const inputId = id ?? `input-${generatedId}`;
const messageId = `${inputId}-message`;
const [internalValue, setInternalValue] = useState(String(defaultValue ?? ""));
const currentValue = value === undefined ? internalValue : String(value);
const message = error ?? success ?? helperText;
const resolvedTone: InputTone = error ? "error" : success ? "success" : tone;
function handleChange(event: ChangeEvent<HTMLInputElement>) { if (value === undefined) setInternalValue(event.target.value); onChange?.(event); }
function clearInput() { if (value === undefined) setInternalValue(""); onClear?.(); document.getElementById(inputId)?.focus(); }
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"><input id={inputId} type="search" className={cx(inputClasses(size, resolvedTone, false, Boolean(currentValue)), className)} value={currentValue} onChange={handleChange} aria-invalid={error ? true : undefined} aria-describedby={message ? messageId : undefined} {...props} />{currentValue ? <button type="button" aria-label={clearLabel} onClick={clearInput} className="absolute right-2 inline-flex size-6 items-center justify-center rounded-[var(--ds-radius-xs)] text-[var(--ds-color-muted-foreground)] transition-colors hover:bg-[var(--ds-color-surface-hover)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none">×</button> : null}</div>{message ? <p id={messageId} className="mt-2 text-xs text-[var(--ds-color-muted-foreground)]">{message}</p> : null}
<button type="button" onClick={() => onSearch?.(currentValue)} className="mt-2 h-9 rounded-[var(--ds-radius-sm)] bg-[var(--ds-color-primary)] px-3 text-[13px] font-medium text-[var(--ds-color-primary-foreground)] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none">{searchLabel}</button></div>;
}
export default SearchInputWithActions; /* 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 SearchInputWithActions({ label = "Search with Actions", helperText, error, success, size = "md", tone = "default", id, className, value, defaultValue = "", onChange, clearLabel = "Clear input", onClear, onSearch, searchLabel = "Search", ...props }) {
const generatedId = useId();
const inputId = id ?? `input-${generatedId}`;
const messageId = `${inputId}-message`;
const [internalValue, setInternalValue] = useState(String(defaultValue ?? ""));
const currentValue = value === undefined ? internalValue : String(value);
const message = error ?? success ?? helperText;
const resolvedTone = error ? "error" : success ? "success" : tone;
function handleChange(event) { if (value === undefined) setInternalValue(event.target.value); onChange?.(event); }
function clearInput() { if (value === undefined) setInternalValue(""); onClear?.(); document.getElementById(inputId)?.focus(); }
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"><input id={inputId} type="search" className={cx(inputClasses(size, resolvedTone, false, Boolean(currentValue)), className)} value={currentValue} onChange={handleChange} aria-invalid={error ? true : undefined} aria-describedby={message ? messageId : undefined} {...props} />{currentValue ? <button type="button" aria-label={clearLabel} onClick={clearInput} className="absolute right-2 inline-flex size-6 items-center justify-center rounded-[var(--ds-radius-xs)] text-[var(--ds-color-muted-foreground)] transition-colors hover:bg-[var(--ds-color-surface-hover)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none">×</button> : null}</div>{message ? <p id={messageId} className="mt-2 text-xs text-[var(--ds-color-muted-foreground)]">{message}</p> : null}
<button type="button" onClick={() => onSearch?.(currentValue)} className="mt-2 h-9 rounded-[var(--ds-radius-sm)] bg-[var(--ds-color-primary)] px-3 text-[13px] font-medium text-[var(--ds-color-primary-foreground)] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none">{searchLabel}</button></div>;
}
export default SearchInputWithActions; # Search Input With Actions
Search input with clear and submit controls.
## Usage
```tsx
import { SearchInputWithActions } from './code';
<SearchInputWithActions label="Workspace name" placeholder="Acme design" />
```
## JavaScript
```jsx
import { SearchInputWithActions } from './code';
<SearchInputWithActions 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. 73 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-with-actions I want to use this DevSnips resource in production: Page: https://devsnips.site/library/react/components/inputs/search-input-with-actions/ Install: npx devsnips add React/Components/Inputs/search-input-with-actions This is the Search Input With Actions — Search input with clear and submit controls. 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-with-actions