# Input

Enter a single line of text or a native input value.

Project name input

[Complete TSX file](/examples/input/default.tsx)

```tsx
"use client";

import { Field, FieldLabel } from "@/siteplane/components/field";
import { Input } from "@/siteplane/components/input";

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Project name</FieldLabel>
      <Input />
    </Field>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/input
```

With npm: `npx shadcn@4.18.0 add @siteplane/input`. [Registry JSON](/registry/input.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/action-icons @siteplane/button @siteplane/input-group
```

## Usage

```tsx
import { Input } from "@/siteplane/components/input";

<Input aria-label="Description" />;
```

## Variants

Choose the control surface without changing its behavior. `elevated` lifts the field with the hairline and soft shadow of an elevated Card, so it reads on a white surface too. `soft` replaces the border with a subtle fill.

Input surface variants

[Complete TSX file](/examples/input/variants.tsx)

```tsx
"use client";

import { Field, FieldLabel } from "@/siteplane/components/field";
import { Input } from "@/siteplane/components/input";

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      {(["default", "elevated", "inset", "soft"] as const).map((variant) => (
        <Field key={variant}>
          <FieldLabel>Project name, {variant}</FieldLabel>
          <Input variant={variant} placeholder="Website redesign" />
        </Field>
      ))}
    </div>
  );
}
```

## Sizes

Omit `size` to inherit the provider's control size. The text is 14px, or 16px at `xl`; `textSize` sets it at any size. Touch screens always use 16px text.

Input sizes

[Complete TSX file](/examples/input/sizes.tsx)

```tsx
"use client";

import { Field, FieldLabel } from "@/siteplane/components/field";
import { Input } from "@/siteplane/components/input";

const sizes = [
  { size: "sm", label: "small" },
  { size: "md", label: "medium" },
  { size: "lg", label: "large" },
  { size: "xl", label: "extra large" },
] as const;

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      {sizes.map(({ size, label }) => (
        <Field key={size}>
          <FieldLabel>Project name, {label}</FieldLabel>
          <Input size={size} defaultValue="Website redesign" />
        </Field>
      ))}
    </div>
  );
}
```

## Disabled, read-only and invalid

`disabled` blocks editing; `readOnly` keeps the value focusable and submitted. Set `invalid` on Field and explain the problem with FieldError.

Disabled, read-only and invalid inputs

[Complete TSX file](/examples/input/states.tsx)

```tsx
"use client";

import { Field, FieldError, FieldLabel } from "@/siteplane/components/field";
import { Input } from "@/siteplane/components/input";

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      <Field disabled>
        <FieldLabel>Billing email</FieldLabel>
        <Input type="email" defaultValue="finance@example.com" />
      </Field>
      <Field>
        <FieldLabel>Project ID</FieldLabel>
        <Input readOnly defaultValue="PRJ-2048" />
      </Field>
      <Field invalid>
        <FieldLabel>Email</FieldLabel>
        <Input type="email" defaultValue="mara@example" />
        <FieldError match>Enter a complete email address.</FieldError>
      </Field>
    </div>
  );
}
```

## File input

`type="file"` keeps the native file picker. Its button and file name sit on the center of the field, with the height and start padding of any other input at each size. Uploading the selected file is up to your application.

Attachment file input

[Complete TSX file](/examples/input/file.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";
import { Input } from "@/siteplane/components/input";

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Attachment</FieldLabel>
      <Input type="file" accept=".pdf,image/*" />
      <FieldDescription>PDF or image, up to 10 MB.</FieldDescription>
    </Field>
  );
}
```

## Password

`type="password"` masks the value as larger, evenly spaced dots at the field’s own text size. They come from a font with a bigger bullet, Verdana or DejaVu Sans on Linux; on a system with neither, such as Android, the dots keep the text font’s size and only the spacing opens up. The placeholder and the revealed text keep the normal text style.

To let people show or hide the value, put the input in an InputGroup with an icon Button, as below. [Password visibility](/docs/components/input-group/#password-visibility) describes the pattern.

Password with show and hide toggle

[Complete TSX file](/examples/input/password.tsx)

```tsx
"use client";

import { RevealIcon } from "@/siteplane/components/action-icons";
import { Button } from "@/siteplane/components/button";
import { Field, FieldLabel } from "@/siteplane/components/field";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/siteplane/components/input-group";
import { useState } from "react";

export default function Example() {
  const [visible, setVisible] = useState(false);

  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Password</FieldLabel>
      <InputGroup>
        <InputGroupInput
          autoComplete="current-password"
          defaultValue="correct horse battery"
          type={visible ? "text" : "password"}
        />
        <InputGroupAddon align="inline-end">
          <Button
            aria-label="Show password"
            aria-pressed={visible}
            profile="field-action"
            shape="square"
            onClick={() => setVisible((current) => !current)}
          >
            <RevealIcon revealed={visible} />
          </Button>
        </InputGroupAddon>
      </InputGroup>
    </Field>
  );
}
```

## Controlled value

Pass `value` and `onValueChange` to own the text, for example to show how many characters remain. `maxLength` limits entry natively.

Display name with character count

[Complete TSX file](/examples/input/controlled.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";
import { Input } from "@/siteplane/components/input";
import { useState } from "react";

const maxLength = 32;

export default function Example() {
  const [value, setValue] = useState("");

  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Display name</FieldLabel>
      <Input maxLength={maxLength} value={value} onValueChange={setValue} />
      <FieldDescription>
        <span className="tabular-nums">{maxLength - value.length}</span>{" "}
        characters left
      </FieldDescription>
    </Field>
  );
}
```

## Floating label

Put the Input in a Field with `labelPlacement="floating"` and its label sits inside the field until it has focus or a value; [Field](/docs/components/field/#floating-label) lists the controls that support it. Floating fields take no leading icon, so an Input with `insetStart` keeps its label above it.

Email with a floating label

[Complete TSX file](/examples/input/floating-label.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";
import { Input } from "@/siteplane/components/input";

export default function Example() {
  return (
    <Field className="w-full max-w-sm" labelPlacement="floating">
      <FieldLabel>Email</FieldLabel>
      <Input autoComplete="email" type="email" />
      <FieldDescription>We send the receipt here.</FieldDescription>
    </Field>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| size | sm \| md \| lg \| xl \| number | provider / lg | Visual control size. A number sets native input size instead. |
| textSize | sm \| base | unset | Text size at any control size: sm is 14px, base is 16px. Unset uses 14px, or 16px at xl; touch screens use 16px. |
| variant | default \| elevated \| inset \| soft | default | Surface treatment. |
| unstyled | boolean | false | Removes the outer surface recipe; preserves control anatomy. |
| className | string | unset | Styles the outer span, not the native control. |
| nativeInput | boolean | false | Renders a native input element with the same Field wiring as the default input. |
| renderInput | ReactElement<input props> | unset | Renders the supplied input element with the input classes; takes precedence over nativeInput. Inside a Field it is connected like the default input. |
| hidePickerIndicator | boolean | false | Hides the native picker affordance; supply another understandable interaction when needed. |
| insetStart / insetEnd | boolean | false | Marks an edge as inset for a composed adjoining element. |
| onValueChange | (value, eventDetails) => void | unset | Text-change callback; native onChange also fires. |

`InputProps` and `InputPrimitive` are exported. `style` reaches the actual input, including callback styles. `renderInput` preserves its own className alongside the input classes; props set on the supplied element take precedence over Input props.

Inside a Field, `nativeInput` and `renderInput` are wired like the default input: FieldLabel labels the rendered input, FieldDescription and FieldError describe it, and Field state such as invalid and disabled reaches it.

Other input, form and validation props follow [Base UI Input](https://base-ui.com/react/components/input).

[Read the installed source](/source/input).

## Usage notes

Always provide a visible label or an accessible name. Placeholder text is not a label. Use Field for related help and errors. `type="search"` hides the browser's built-in clear button, `type="date"` keeps the native picker (`hidePickerIndicator` hides its icon), and `type="hidden"` renders only the native input. A controlled input does not reset with its form; reset your state in `onReset`. The control does not persist data or implement request loading. A plain native form and the kit Form have different validation presentation; see [Forms](/docs/guides/forms/).
