# Input

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

> For the complete documentation index, see [llms.txt](/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](/.well-known/agent-skills/site-skill.md).





## Playground [#playground]

```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 [#installation]

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

```bash
npx shadcn@latest add https://vandor-ui.vercel.app/r/input.json
```

## Dependencies [#dependencies]

Installed automatically by the registry CLI. For manual setup:

```bash
npm install cn
```

## Usage [#usage]

```tsx
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 [#examples]

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

<ComponentPreview name="input-demo" title="input-demo.tsx">
  <InputDemo />
</ComponentPreview>

### Optional Storybook Stories [#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 [#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 [#props]

| Prop               | Type                                        | Default      | Description                                            |
| ------------------ | ------------------------------------------- | ------------ | ------------------------------------------------------ |
| aria-invalid       | boolean                                     | false        | Input: exposes invalid state to assistive technology.  |
| containerClassName | string                                      | undefined    | Input: class applied to the label/control wrapper.     |
| defaultValue       | string \| number                            | undefined    | Input: initial uncontrolled native value.              |
| disabled           | boolean                                     | false        | Input: prevents editing and focus.                     |
| icon               | ReactNode                                   | undefined    | Input: decorative inline-start adornment.              |
| label              | string                                      | undefined    | Input: visible label associated with the native field. |
| labelClassName     | string                                      | undefined    | Input: class applied to the label.                     |
| labelStyle         | "floating" \| "static"                      | "floating"   | Input: label presentation.                             |
| name               | string                                      | undefined    | Input: native form field name.                         |
| onChange           | React.ChangeEventHandler\<HTMLInputElement> | undefined    | Input: native input change callback.                   |
| placeholder        | string                                      | undefined    | Input: native placeholder.                             |
| readOnly           | boolean                                     | false        | Input: prevents editing while retaining focus.         |
| value              | string \| number                            | uncontrolled | Input: controlled native value.                        |

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 [#source]

```tsx
"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>
  );
};

```
