Forms
Input
Input wraps a native <input> with label/helpText/error chrome, leading/trailing icon slots, and an optional clear button.
Installation
npx shadcn add https://rck.vibeboxph.com//r/input.jsonLive preview & controls
Controls
<Input label="Email" placeholder="you@example.com" />Anatomy
FieldLabelBuilt-in top field label (re-exported as InputLabel).input-wrapperThe bordered field container (focus ring lives here).inputThe native <input> element.FieldMessageHelp text or error message below the field.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | React.ReactNode | — | Field label. |
| helpText | React.ReactNode | — | Supplementary help text shown when there's no error. |
| error | React.ReactNode | — | Error text; also sets `aria-invalid` and switches to the destructive style. |
| size | "sm" | "md" | "lg" | md | Field height. |
| leadingIcon / trailingIcon | React.ReactNode | — | Icon slots either side of the input. |
| icon | React.ReactNode | — | Shorthand icon slot — any React node (custom SVG, Icon, emoji). Defaults to leading. |
| iconPosition | "leading" | "trailing" | leading | Which side the `icon` prop renders on. |
| clearable | boolean | — | Shows a clear button once the field has a value. |
| value / defaultValue | string | — | Controlled / uncontrolled value. |
| onValueChange | (value: string) => void | — | Convenience change handler (in addition to native onChange). |
| className | string | — | Merged onto the native <input> element. |
| containerClassName | string | — | Merged onto the outer field wrapper. |
| wrapperClassName | string | — | Merged onto the bordered input wrapper. |
| onBlur, onFocus, onKeyDown, onChange, … | InputHTMLAttributes<HTMLInputElement> | — | All native input events and attributes are forwarded to the underlying <input>. |
Accessibility
- Label is always associated to the field via `htmlFor`/`id` (auto-generated with `useId` when not provided).
- `error` sets `aria-invalid` and `aria-describedby` pointing at the message, and the message itself has `role="alert"`.
- The clear button has an explicit `aria-label` and is skipped in the tab order only when hidden (no value).
Source
registry/rck/ui/input.tsx
"use client";
import * as React from "react";
import { X } from "@/lib/icons";
import { cn } from "@/lib/utils";
import {
fieldWrapperVariants,
FieldLabel,
FieldMessage,
type FieldWrapperVariantProps,
} from "@/registry/rck/ui/field-chrome";
export {
fieldWrapperVariants as inputWrapperVariants,
FieldLabel,
FieldLabel as InputLabel,
FieldMessage,
FieldMessage as InputMessage,
} from "@/registry/rck/ui/field-chrome";
export { Textarea, type TextareaProps } from "@/registry/rck/ui/textarea";
export interface InputProps
extends
Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
FieldWrapperVariantProps {
label?: React.ReactNode;
helpText?: React.ReactNode;
error?: React.ReactNode;
/** Shorthand for a leading/trailing icon slot — accepts any React node (SVG, Icon, emoji, etc.). */
icon?: React.ReactNode;
iconPosition?: "leading" | "trailing";
leadingIcon?: React.ReactNode;
trailingIcon?: React.ReactNode;
clearable?: boolean;
onValueChange?: (value: string) => void;
containerClassName?: string;
wrapperClassName?: string;
}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
(
{
className,
containerClassName,
wrapperClassName,
size,
label,
helpText,
error,
icon,
iconPosition = "leading",
leadingIcon,
trailingIcon,
clearable,
disabled,
required,
id: providedId,
value,
defaultValue,
onChange,
onValueChange,
...props
},
ref,
) => {
const generatedId = React.useId();
const id = providedId ?? generatedId;
const helpTextId = helpText ? `${id}-help-text` : undefined;
const errorId = error ? `${id}-error` : undefined;
const invalid = Boolean(error);
const innerRef = React.useRef<HTMLInputElement>(null);
React.useImperativeHandle(ref, () => innerRef.current as HTMLInputElement);
const isControlled = value !== undefined;
const [internalValue, setInternalValue] = React.useState(defaultValue ?? "");
const currentValue = isControlled ? value : internalValue;
const showClear = clearable && !disabled && String(currentValue ?? "").length > 0;
const resolvedLeadingIcon =
leadingIcon ?? (icon && iconPosition === "leading" ? icon : undefined);
const resolvedTrailingIcon =
trailingIcon ?? (icon && iconPosition === "trailing" ? icon : undefined);
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
if (!isControlled) setInternalValue(event.target.value);
onChange?.(event);
onValueChange?.(event.target.value);
}
function handleClear() {
if (!isControlled) setInternalValue("");
onValueChange?.("");
if (innerRef.current) {
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value",
)?.set;
nativeSetter?.call(innerRef.current, "");
innerRef.current.dispatchEvent(new Event("input", { bubbles: true }));
}
innerRef.current?.focus();
}
return (
<div
data-slot="input-container"
className={cn("flex flex-col gap-1.5", containerClassName)}
>
{label && (
<FieldLabel htmlFor={id} required={required}>
{label}
</FieldLabel>
)}
<div
data-slot="input-wrapper"
className={cn(fieldWrapperVariants({ size, invalid }), wrapperClassName)}
>
{resolvedLeadingIcon && (
<span
data-slot="input-icon-leading"
className="text-muted-foreground flex shrink-0 items-center [&_svg]:size-4"
>
{resolvedLeadingIcon}
</span>
)}
<input
ref={innerRef}
id={id}
data-slot="input"
disabled={disabled}
required={required}
aria-invalid={invalid || undefined}
aria-describedby={cn(helpTextId, errorId) || undefined}
className={cn(
"placeholder:text-muted-foreground w-full min-w-0 bg-transparent outline-none disabled:cursor-not-allowed",
className,
)}
value={isControlled ? value : undefined}
defaultValue={isControlled ? undefined : defaultValue}
onChange={handleChange}
{...props}
/>
{showClear && (
<button
type="button"
onClick={handleClear}
className="text-muted-foreground hover:text-foreground focus-ring flex shrink-0 items-center rounded-sm"
aria-label="Clear input"
>
<X className="size-3.5" />
</button>
)}
{resolvedTrailingIcon && (
<span
data-slot="input-icon-trailing"
className="text-muted-foreground flex shrink-0 items-center [&_svg]:size-4"
>
{resolvedTrailingIcon}
</span>
)}
</div>
{helpText && !error && <FieldMessage id={helpTextId}>{helpText}</FieldMessage>}
{error && (
<FieldMessage id={errorId} invalid role="alert">
{error}
</FieldMessage>
)}
</div>
);
},
);
Input.displayName = "Input";
registry/rck/ui/field-chrome.tsx
import * as React from "react";
import { cva, type VariantProps } from "@/lib/cva";
import { cn } from "@/lib/utils";
export const fieldWrapperVariants = cva(
"flex w-full items-center gap-2 rounded-md border bg-background px-3 shadow-sm " +
"transition-colors has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50 " +
"has-[textarea:disabled]:cursor-not-allowed has-[textarea:disabled]:opacity-50",
{
variants: {
size: {
sm: "h-8 text-[10px]",
md: "h-[39px] text-[11px]",
lg: "h-11 text-xs",
},
invalid: {
true: "border-destructive focus-within:ring-2 focus-within:ring-destructive/40",
false:
"border-border-strong focus-within:border-primary focus-within:ring-2 focus-within:ring-ring/30",
},
},
defaultVariants: {
size: "md",
invalid: false,
},
},
);
/** Built-in field label — shared by Input, Textarea, and boolean field wrappers. */
export function FieldLabel({
className,
required,
children,
...props
}: React.LabelHTMLAttributes<HTMLLabelElement> & { required?: boolean }) {
return (
<label
data-slot="field-label"
className={cn("text-foreground text-sm leading-none font-medium", className)}
{...props}
>
{children}
{required && <span className="text-destructive ml-0.5">*</span>}
</label>
);
}
export function FieldMessage({
className,
invalid,
...props
}: React.HTMLAttributes<HTMLParagraphElement> & { invalid?: boolean }) {
return (
<p
data-slot="field-message"
className={cn(
"text-xs",
invalid ? "text-destructive" : "text-muted-foreground",
className,
)}
{...props}
/>
);
}
export type ControlPosition = "start" | "end";
export interface BooleanGroupOption {
value: string;
/** Inline label beside the control. */
text: string;
disabled?: boolean;
helpText?: React.ReactNode;
}
export interface BooleanFieldLayoutProps {
label?: React.ReactNode;
text?: React.ReactNode;
helpText?: React.ReactNode;
error?: React.ReactNode;
required?: boolean;
/** Places the control at inline-start (default) or inline-end — flips automatically in RTL. */
controlPosition?: ControlPosition;
disabled?: boolean;
id?: string;
className?: string;
containerClassName?: string;
children: (args: {
id: string;
describedBy?: string;
disabled?: boolean;
}) => React.ReactNode;
}
export function BooleanFieldLayout({
label,
text,
helpText,
error,
required,
controlPosition = "start",
disabled,
id: providedId,
className,
containerClassName,
children,
}: BooleanFieldLayoutProps) {
const generatedId = React.useId();
const id = providedId ?? generatedId;
const helpTextId = helpText && !error ? `${id}-help-text` : undefined;
const errorId = error ? `${id}-error` : undefined;
const describedBy = cn(helpTextId, errorId) || undefined;
const control = children({ id, describedBy, disabled });
return (
<div
data-slot="boolean-field"
className={cn("flex flex-col gap-1.5", containerClassName)}
>
{label && (
<FieldLabel htmlFor={id} required={required}>
{label}
</FieldLabel>
)}
<div
className={cn(
"flex w-full items-start gap-2",
controlPosition === "end" && "flex-row-reverse justify-between",
className,
)}
>
{control}
{text && (
<label
htmlFor={id}
className={cn(
"text-foreground text-sm leading-none font-medium",
controlPosition === "end" && "flex-1",
disabled && "cursor-not-allowed opacity-50",
)}
>
{text}
</label>
)}
</div>
{helpText && !error && <FieldMessage id={helpTextId}>{helpText}</FieldMessage>}
{error && (
<FieldMessage id={errorId} invalid role="alert">
{error}
</FieldMessage>
)}
</div>
);
}
export type FieldWrapperVariantProps = VariantProps<typeof fieldWrapperVariants>;
Related examples
Labels & help textLabel, help text, and required fields.
ValidationControlled field with a live error message.
Icons & clearLeading/trailing icons and a clearable search field.
SizesSmall, medium and large field heights.
Input typesEmail, password and search field types.
Disabled & read-onlyNon-editable field states.