# Input

> A polished text field for fast, accessible forms.

A shadcn/ui block from the [@wensity](https://ui.wensity.com/components) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/wensity/input).

- Page, with a live preview: https://shadcn.exchange/r/wensity/input
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/wensity/input-light-c901e1964aeda06b.png
- Registry item: https://raw.githubusercontent.com/wensity/registry/main/input.json
- Categories: UI Primitives, wensity, free
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://raw.githubusercontent.com/wensity/registry/main/input.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@base-ui/react`, `@tabler/icons-react`, `clsx`, `tailwind-merge`

## Notes from the registry

Free Wensity component. Installs to @components/wensity/input.tsx. For Pro components and updates, use pnpm dlx wensity@latest add input.

## Files

### `registry/wensity/lib/utils.ts`

```ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
```

### `registry/wensity/input.tsx`

```tsx
"use client";

import * as React from "react";
import { Input as BaseInput } from "@base-ui/react/input";
import { IconX } from "@tabler/icons-react";
import { cn } from "@/lib/utils";

export type InputSize = "sm" | "md" | "lg";

export interface InputProps
  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
  label?: React.ReactNode;
  hint?: React.ReactNode;
  error?: React.ReactNode;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
  clearable?: boolean;
  onClear?: () => void;
  inputSize?: InputSize;
  fullWidth?: boolean;
  containerClassName?: string;
  inputClassName?: string;
}

const fieldSizes: Record<InputSize, string> = {
  sm: "h-[var(--primitive-control-height-sm)] min-h-[var(--primitive-control-height-sm)]",
  md: "h-[var(--primitive-control-height-md)] min-h-[var(--primitive-control-height-md)]",
  lg: "h-[var(--primitive-control-height-lg)] min-h-[var(--primitive-control-height-lg)]",
};

const inputSizes: Record<InputSize, string> = {
  sm: "h-full px-3 text-xs",
  md: "h-full px-3.5 text-sm",
  lg: "h-full px-4 text-[15px]",
};

const iconBoxSizes: Record<InputSize, string> = {
  sm: "w-8",
  md: "w-9",
  lg: "w-10",
};

const trailingPad: Record<InputSize, string> = {
  sm: "pr-2",
  md: "pr-2.5",
  lg: "pr-3",
};

const fieldBase = cn(
  "wensity-input-shell group/input relative isolate flex w-full items-center overflow-hidden",
  "border border-[var(--border)] bg-[var(--background)]",
  "rounded-[var(--primitive-radius-control)]",
  "transition-[border-color,background-color,box-shadow] duration-150 ease-[var(--primitive-ease)]",
  "motion-reduce:transition-none",
);

const fieldInteractive = cn(
  "hover:border-[color-mix(in_srgb,var(--foreground)_14%,transparent)]",
  "focus-within:border-[color-mix(in_srgb,var(--foreground)_24%,transparent)]",
  "focus-within:ring-2 focus-within:ring-[var(--primitive-ring)] focus-within:ring-offset-2 focus-within:ring-offset-[var(--background)]",
);

const fieldError = cn(
  "border-[var(--primitive-destructive-border)]",
  "hover:border-[color-mix(in_srgb,var(--primitive-destructive)_65%,transparent)]",
  "focus-within:border-[var(--primitive-destructive)]",
  "focus-within:ring-2 focus-within:ring-[color-mix(in_srgb,var(--primitive-destructive)_45%,transparent)] focus-within:ring-offset-2 focus-within:ring-offset-[var(--background)]",
);

const fieldDisabled = cn(
  "cursor-not-allowed opacity-45",
  "hover:border-[var(--border)]",
);

const fieldReadOnly = cn(
  "border-[var(--border)] bg-[var(--muted)]",
  "hover:border-[var(--border)]",
);

const iconSlot = cn(
  "wensity-input-icon inline-flex self-stretch shrink-0 items-center justify-center",
  "bg-[color-mix(in_srgb,var(--foreground)_4%,var(--background))]",
  "text-[color-mix(in_srgb,var(--muted-foreground)_68%,transparent)]",
  "transition-[background-color,color,border-color] duration-150 ease-[var(--primitive-ease)]",
  "motion-reduce:transition-none",
  "group-hover/input:bg-[color-mix(in_srgb,var(--foreground)_5.5%,var(--background))]",
  "group-focus-within/input:text-[var(--muted-foreground)]",
  "group-data-[filled]/input:text-[var(--muted-foreground)]",
  "[&_svg]:size-4 [&_svg]:shrink-0",
);

const leadingIconSlot = cn(
  iconSlot,
  "border-r border-[color-mix(in_srgb,var(--foreground)_8%,transparent)]",
);

const trailingIconSlot = cn(
  iconSlot,
  "border-l border-[color-mix(in_srgb,var(--foreground)_8%,transparent)]",
);

export const Input = React.forwardRef<HTMLInputElement, InputProps>(
  (
    {
      id,
      className,
      containerClassName,
      inputClassName,
      label,
      hint,
      error,
      leftIcon,
      rightIcon,
      clearable = false,
      onClear,
      inputSize = "md",
      fullWidth = true,
      disabled,
      readOnly,
      value,
      defaultValue,
      onChange,
      "aria-describedby": ariaDescribedBy,
      ...props
    },
    ref,
  ) => {
    const reactId = React.useId();
    const inputId = id ?? `wensity-input-${reactId}`;
    const hintId = hint ? `${inputId}-hint` : undefined;
    const errorId = error ? `${inputId}-error` : undefined;
    const inputRef = React.useRef<HTMLInputElement | null>(null);
    const isControlled = value !== undefined;
    const [uncontrolledValue, setUncontrolledValue] = React.useState(
      defaultValue?.toString() ?? "",
    );
    const currentValue = isControlled ? value : uncontrolledValue;
    const hasValue = currentValue != null && String(currentValue).length > 0;
    const showClear = clearable && hasValue && !disabled && !readOnly;
    const describedBy =
      [ariaDescribedBy, errorId, hintId].filter(Boolean).join(" ") || undefined;

    const setRefs = React.useCallback(
      (node: HTMLInputElement | null) => {
        inputRef.current = node;
        if (typeof ref === "function") {
          ref(node);
        } else if (ref) {
          ref.current = node;
        }
      },
      [ref],
    );

    const handleChange = React.useCallback(
      (event: React.ChangeEvent<HTMLInputElement>) => {
        if (!isControlled) setUncontrolledValue(event.target.value);
        onChange?.(event);
      },
      [isControlled, onChange],
    );

    const handleClear = React.useCallback(() => {
      if (!isControlled) setUncontrolledValue("");
      if (inputRef.current) {
        inputRef.current.value = "";
      }
      onClear?.();
      if (typeof window !== "undefined") {
        window.requestAnimationFrame(() => inputRef.current?.focus());
      } else {
        inputRef.current?.focus();
      }
    }, [isControlled, onClear]);

    const handleClearPointerDown = React.useCallback(
      (event: React.PointerEvent<HTMLButtonElement>) => {
        event.preventDefault();
      },
      [],
    );

    return (
      <div data-wensity-primitive=""
        className={cn(
          "flex flex-col gap-1.5",
          fullWidth ? "w-full" : "w-fit",
          containerClassName,
        )}
      >
        {label ? (
          <label
            htmlFor={inputId}
            className="text-[13px] font-medium leading-none tracking-[-0.01em] text-[var(--foreground)]"
          >
            {label}
          </label>
        ) : null}

        <div
          className={cn(
            "relative",
            fullWidth ? "w-full" : "w-fit",
            className,
          )}
        >
          <div
            data-wensity-input=""
            data-filled={hasValue ? "" : undefined}
            data-error={error ? "" : undefined}
            data-readonly={readOnly ? "" : undefined}
            data-disabled={disabled ? "" : undefined}
            className={cn(
              fieldBase,
              !disabled && !readOnly && !error && fieldInteractive,
              error && fieldError,
              disabled && fieldDisabled,
              readOnly && fieldReadOnly,
              fieldSizes[inputSize],
            )}
          >
            {leftIcon ? (
              <span
                aria-hidden="true"
                className={cn(leadingIconSlot, iconBoxSizes[inputSize])}
              >
                {leftIcon}
              </span>
            ) : null}

            <BaseInput
              ref={setRefs}
              id={inputId}
              value={value}
              defaultValue={isControlled ? undefined : defaultValue}
              disabled={disabled}
              readOnly={readOnly}
              aria-invalid={error ? true : undefined}
              aria-describedby={describedBy}
              onChange={handleChange}
              className={cn(
                "wensity-input-control min-w-0 flex-1 border-0 bg-transparent text-[var(--foreground)] outline-none",
                "placeholder:text-[var(--primitive-text-placeholder)]",
                "disabled:cursor-not-allowed",
                "read-only:cursor-default read-only:text-[var(--muted-foreground)]",
                inputSizes[inputSize],
                inputClassName,
              )}
              {...props}
            />

            {showClear ? (
              <span
                className={cn(
                  "inline-flex shrink-0 items-center",
                  trailingPad[inputSize],
                )}
              >
                <button
                  type="button"
                  aria-label="Clear"
                  onPointerDown={handleClearPointerDown}
                  onClick={handleClear}
                  className={cn(
                    "grid size-6 place-items-center rounded-[var(--primitive-radius-control-sm)]",
                    "text-[color-mix(in_srgb,var(--muted-foreground)_70%,transparent)]",
                    "transition-colors duration-150 ease-[var(--primitive-ease)]",
                    "motion-reduce:transition-none",
                    "hover:text-[var(--foreground)]",
                    "active:scale-[0.96] motion-reduce:active:scale-100",
                    "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--primitive-ring)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--background)]",
                    "focus-visible:text-[var(--foreground)]",
                  )}
                >
                  <IconX aria-hidden="true" className="size-3.5" />
                </button>
              </span>
            ) : rightIcon ? (
              <span
                aria-hidden="true"
                className={cn(trailingIconSlot, iconBoxSizes[inputSize])}
              >
                {rightIcon}
              </span>
            ) : null}
          </div>
        </div>

        {error ? (
          <p
            id={errorId}
            className="text-[12px] leading-[1.45] text-[var(--primitive-destructive)]"
          >
            {error}
          </p>
        ) : hint ? (
          <p
            id={hintId}
            className="text-[12px] leading-[1.45] text-[var(--primitive-text-secondary)]"
          >
            {hint}
          </p>
        ) : null}
      </div>
    );
  },
);

Input.displayName = "Input";
```
