For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to any URL or sending an Accept: text/markdown header. An agent skill is available at /.well-known/agent-skills/site-skill.md.
1
Support

Input

A native input with optional floating or static labels and icon adornments.

Playground

Live preview
Customize input
Customize
input-demo.tsx
"use client";

import { Input } from "@/components/ui/input";

export function InputDemo() {
  return (
    <Input
        label={"Full name"}
        labelStyle={"floating"}
        placeholder={"Your name"}
        aria-invalid={false}
    />
  );
}

Installation

Initialize shadcn in your React project, then install this component from the Vandor UI registry:

$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/input.json

Dependencies

Installed automatically by the registry CLI. For manual setup:

Dependencies1 required packages
  • cn
$ pnpm add cn

Usage

import { Input } from "@/components/ui/input";
 
<Input label="Full name" placeholder="Your name" />;

Input forwards native input props and its ref. Labels default to floating; set labelStyle="static" for a conventional label. An icon is decorative and does not replace the accessible name.

Examples

The preview contrasts floating and static labels while keeping each field natively labeled.

input-demo.tsx
import { Input } from "@/components/ui/input";

export const InputDemo = () => (
  <div className="grid max-w-sm gap-4">
    <Input label="Your name" placeholder="Ada Lovelace" />
    <Input label="Work email" labelStyle="static" type="email" required />
  </div>
);

Optional Storybook Stories

Install Input first, then run npx shadcn@latest add https://vandor-ui.vercel.app/r/input-stories.json. The stories use relative imports and install beside the component under your configured UI alias. They cover label styles, icons, controlled state, and disabled/read-only/invalid fields.

Use existing, matching @storybook/react packages (verified with 10.6.1), discover **/*.stories.tsx in your UI directory, and load your global Tailwind/theme stylesheet. No addons, extra packages, core overwrites, or Storybook config edits are required. Default-value Controls remount uncontrolled examples; controlled examples manage local demo state. Storybook UI is not verified because this project has no Storybook server.

Behavior & Accessibility

Labels remain associated with the native field. Floating fields are 40px tall: labels rise to a notch in the top border while focused, autofilled, or nonempty. The decorative fieldset/legend opens the border without an opaque background patch or a second accessible label. Their horizontal position stays fixed when values are cleared, including fields with icons. Use aria-invalid and aria-describedby to connect validation feedback. Native disabled and read-only behavior is preserved.

Props

Component props, types, defaults, and descriptions
PropType / DescriptionDefault
aria-invalidboolean

Input: exposes invalid state to assistive technology.

false
containerClassNamestring

Input: class applied to the label/control wrapper.

undefined
defaultValuestring | number

Input: initial uncontrolled native value.

undefined
disabledboolean

Input: prevents editing and focus.

false
iconReactNode

Input: decorative inline-start adornment.

undefined
labelstring

Input: visible label associated with the native field.

undefined
labelClassNamestring

Input: class applied to the label.

undefined
labelStyle"floating" | "static"

Input: label presentation.

"floating"
namestring

Input: native form field name.

undefined
onChangeReact.ChangeEventHandler<HTMLInputElement>

Input: native input change callback.

undefined
placeholderstring

Input: native placeholder.

undefined
readOnlyboolean

Input: prevents editing while retaining focus.

false
valuestring | number

Input: controlled native value.

uncontrolled

The reference lists component-specific props and selected native props, not the complete React API. Other native attributes are forwarded to the rendered element.

Source

"use client";
 
import { cn } from "cn";
import type { ComponentProps } from "react";
import * as React from "react";
 
export type InputProps = Omit<ComponentProps<"input">, "children"> & {
  label?: string;
  labelStyle?: "floating" | "static";
  icon?: React.ReactNode;
  containerClassName?: string;
  labelClassName?: string;
};
 
export const fieldSurfaceClassName =
  "bg-background bg-linear-to-b from-white/10 to-black/10 dark:bg-input/30";
 
const floatingLabelClassName =
  "pointer-events-none absolute top-1/2 left-3 z-10 max-w-[calc(100%-1.5rem)] origin-left -translate-y-1/2 truncate text-sm text-muted-foreground transition-[top,scale,color] duration-150 peer-focus:top-0 peer-focus:scale-75 peer-focus:text-foreground peer-autofill:top-0 peer-autofill:scale-75 peer-autofill:text-foreground data-[floating=true]:top-0 data-[floating=true]:scale-75 data-[floating=true]:text-foreground motion-reduce:transition-none";
 
const fieldClassName =
  "h-9 w-full min-w-0 rounded-md border border-input px-3 text-sm text-foreground shadow-xs outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 motion-reduce:transition-none";
 
const InputIcon = ({
  icon,
  staticLabel,
}: {
  icon: React.ReactNode;
  staticLabel: boolean;
}) =>
  icon ? (
    <span
      aria-hidden="true"
      className={cn(
        "pointer-events-none absolute left-3 z-10 -translate-y-1/2 text-muted-foreground",
        staticLabel ? "bottom-4.5 translate-y-1/2" : "top-1/2"
      )}
    >
      {icon}
    </span>
  ) : null;
 
const InputLabel = ({
  label,
  labelStyle,
  inputId,
  hasValue,
  icon,
  className,
}: {
  label?: string;
  labelStyle: InputProps["labelStyle"];
  inputId: string;
  hasValue: boolean;
  icon: React.ReactNode;
  className?: string;
}) => {
  if (!label) {
    return null;
  }
  if (labelStyle === "static") {
    return (
      <label htmlFor={inputId} className={cn("text-sm font-medium", className)}>
        {label}
      </label>
    );
  }
  return (
    <label
      data-floating={hasValue ? "true" : "false"}
      htmlFor={inputId}
      className={cn(floatingLabelClassName, icon && "left-10", className)}
    >
      {label}
    </label>
  );
};
 
const InputOutline = ({
  label,
  hasValue,
  icon,
}: {
  label?: string;
  hasValue: boolean;
  icon: React.ReactNode;
}) => (
  <fieldset
    aria-hidden="true"
    data-slot="input-outline"
    className={cn(
      "pointer-events-none absolute inset-x-0 -top-1.5 bottom-0 min-w-0 rounded-md border border-input px-2 peer-focus-visible:border-2 peer-focus-visible:border-ring peer-aria-invalid:border-destructive peer-disabled:opacity-50",
      "[&>legend]:max-w-0 peer-focus:[&>legend]:max-w-full peer-autofill:[&>legend]:max-w-full",
      hasValue && "[&>legend]:max-w-full",
      icon && "pl-9"
    )}
  >
    <legend className="invisible block h-3 overflow-hidden whitespace-nowrap p-0 text-[10.5px] leading-3 transition-[max-width] duration-150 motion-reduce:transition-none">
      <span className="px-1">{label}</span>
    </legend>
  </fieldset>
);
 
export const Input = ({
  className,
  containerClassName,
  defaultValue,
  icon,
  id,
  label,
  labelClassName,
  labelStyle = "floating",
  value,
  onChange,
  ...props
}: InputProps) => {
  const generatedId = React.useId();
  const inputId = id ?? generatedId;
  const [internalValue, setInternalValue] = React.useState(defaultValue);
  const floating = label && labelStyle === "floating";
  const staticLabel = Boolean(label) && labelStyle === "static";
  const hasValue =
    String((value === undefined ? internalValue : value) ?? "").length > 0;
 
  const input = (
    <input
      data-slot="input"
      {...props}
      id={label ? inputId : id}
      {...(value === undefined ? { defaultValue } : { value })}
      onChange={(event) => {
        if (value === undefined) {
          setInternalValue(event.currentTarget.value);
        }
        onChange?.(event);
      }}
      aria-label={props["aria-label"] ?? label}
      placeholder={props.placeholder}
      className={cn(
        fieldClassName,
        fieldSurfaceClassName,
        floating &&
          "peer h-10 border-0 px-3 py-2 placeholder:text-transparent focus:placeholder:text-muted-foreground focus-visible:ring-0 aria-invalid:ring-0",
        icon && "pl-10",
        className
      )}
    />
  );
 
  if (!label && !icon && !containerClassName) {
    return input;
  }
 
  return (
    <div
      data-slot={floating ? "floating-input" : "input-field"}
      data-label-style={label ? labelStyle : undefined}
      className={cn(
        "relative min-w-0",
        staticLabel && "grid gap-1.5",
        containerClassName
      )}
    >
      <InputIcon icon={icon} staticLabel={staticLabel} />
      {staticLabel && (
        <InputLabel
          label={label}
          labelStyle="static"
          inputId={inputId}
          hasValue={hasValue}
          icon={icon}
          className={labelClassName}
        />
      )}
      {input}
      {floating && (
        <>
          <InputOutline label={label} hasValue={hasValue} icon={icon} />
          <InputLabel
            label={label}
            labelStyle="floating"
            inputId={inputId}
            hasValue={hasValue}
            icon={icon}
            className={labelClassName}
          />
        </>
      )}
    </div>
  );
};