---
title: Input
description: A text input, and only the drawing of one - state and labels belong to the form and to Field.
source: https://adamjurek.com/components/input
---

## Home


A single-line text field that draws only the control: border, focus ring and
sizing, with no label, hint or error state of its own. Reach for it as the
control inside `Field`, or anywhere a form needs a bare native `<input>`.

<!-- ::start:showcase demo="input" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

An input that manages its own state fights every form library it meets, so
`Input` stays to the drawing: state, validation and labels belong to the form
and to `Field`. The full native `<input>` prop surface passes through
untouched, which is what lets `type="email"` or `type="date"` work without
this component knowing anything about them.

## What it does not do

It has no invalid state of its own - the red border comes from `Field`'s
`error` prop, not from anything `Input` tracks. And it does no formatting or
parsing beyond what the browser's own input types already do; that logic
stays with whoever owns the value.

> [!NOTE] Install commands are not written here
> Anything in `packages/ui/registry.ts` gets its TanStack and shadcn commands
> attached automatically, so there is nothing to keep in sync.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/input.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/input.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | content · Forms |
| Files | `input.tsx` |
| Dependencies | None |
| Tags | form, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { Input } from "@sushindustries/ui";

export function Example() {
	return <Input type="email" placeholder="you@example.com" />;
}
```

## What you should see

A single-line text field with a soft border and a placeholder in a fainter
color. Focus it and the border and a low-opacity accent ring appear together -
no browser-default outline. There is no label above it: `Input` draws only the
control.

## If nothing happens

`Input` forwards every native `<input>` attribute untouched, so `value`
without `onChange` renders a field the browser refuses to let you type into -
the same as any other React controlled input. If the field looks completely
unstyled (square corners, no focus ring), the `field-control` class from
`@sushindustries/atoms` did not load.


## Guides


The Guides tab is for the things that are true after it works. If it belongs in
"how do I install this", it goes in Get Started; if it is a prop table, it goes
in API.

## Composing it

`Input` is the control and nothing else - no label, no hint, no error state.
Pair it with `Field` for those:

```tsx
import { Field, Input } from "@sushindustries/ui";

<Field label="Email" hint="I'll only use this to reply">
	<Input type="email" name="email" />
</Field>;
```

`Field` nests the control inside a `<label>`, so the association needs no
`id` to survive a refactor, and it wires `aria-describedby` to the hint or
error text automatically. Passing `error` to `Field` is what turns on the red
border - `Input` itself has no invalid state of its own to set.

## When not to use it

For anything that is not a single line of text - a multi-line field is
`Textarea`, a fixed set of choices is a select or a radio group. `Input`
passes through the full native attribute surface, so `type="number"` or
`type="date"` both work, but the moment the value needs formatting or
parsing beyond what the browser's own input types do, that logic belongs in
the consumer, not in this component.


## API


<!-- generated:api -->

## Props

Accepts every prop of `InputHTMLAttributes<HTMLInputElement>`.

<!-- /generated:api -->

## Notes

There is no prop surface beyond the native one - no `tone`, no `invalid`,
no `size`. Validation styling comes from an ancestor `Field` writing
`data-invalid`, and `className` is appended after `field-control` rather
than replacing it, so a class passed in adds to the control's style instead
of overriding it outright.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="input" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { Field, Input } from "@sushindustries/ui";
import { useState } from "react";

export function ContactForm() {
	const [email, setEmail] = useState("");
	const invalid = email.length > 0 && !email.includes("@");

	return (
		<form className="flex flex-col gap-4">
			<Field
				label="Email"
				error={invalid ? "That doesn't look like an email" : undefined}
			>
				<Input
					type="email"
					value={email}
					onChange={(event) => setEmail(event.target.value)}
				/>
			</Field>
			<button type="submit" className="btn">
				Send
			</button>
		</form>
	);
}
```

## What this example is not

The validation here is a placeholder check, not a real one - a form that
matters would validate on submit and on blur, not on every keystroke, so the
error does not appear while somebody is still mid-word typing their address.
