Input
When to use
Section titled “When to use”- Names, email addresses, passwords, numbers
- A search box, with an icon in the slot before the text
- Any single line of free text in a form
Not for text that runs over several lines — use a Textarea — or a choice from known options — use a Select or a Combobox.
Choosing between entering text:
| Component | Use it for | For example |
|---|---|---|
| Input | One line of text: a name, an email, a search | Email address, search box |
| OTP input | A code of fixed length, typed or pasted one character per box | SMS code, two-factor code, PIN |
| Textarea | Several lines of free text that may wrap | Comment, description, message |
| Combobox | Typing to find and pick from a list of known options | Choose a city, add existing tags |
| Field | Not a control: wraps any of these to tie a label, a hint and an error to it | Any form control with validation |
Input is a real <input>. Typing, selection, autofill, validation and form submission are all
the platform’s, so there is no headless half and nothing to keep in sync. It works with
react-hook-form’s register() as-is.
import { Input } from '@arun-dev/ui';
const field = { display: 'grid', gap: 'var(--space-3xs)', maxInlineSize: '20rem' };
export default function InputBasics() { return ( <> {/* The <label> names the <input>, which is where id lands — the box is only a frame. */} <div style={field}> <label htmlFor="input-name">Name</label> <Input id="input-name" placeholder="Ada Lovelace" /> </div>
<div style={field}> <label htmlFor="input-email">Email</label> <Input id="input-email" type="email" defaultValue="not-an-email" aria-invalid aria-describedby="input-email-error" /> <small id="input-email-error" style={{ color: 'var(--color-status-error)' }}> Enter a valid email address. </small> </div>
<div style={field}> <label htmlFor="input-disabled">Disabled</label> <Input id="input-disabled" defaultValue="Read me, don't edit me" disabled /> </div> </> );}Playground
Section titled “Playground”<Input placeholder="Type here…" />Two elements, one rule
Section titled “Two elements, one rule”The <input> sits inside a <div class="input"> that draws the border, and the box is always
there, with or without slots. That gives one rule for where props go:
| Prop | Lands on |
|---|---|
className | the box |
startSlot, endSlot | inside the box |
everything else, including id, ref, render | the <input> |
So a <label htmlFor>, aria-describedby, register() and a focus ref all reach the control,
and className is where you set width or margins.
startSlot and endSlot hold anything: an icon, a prefix or unit, a keyboard hint, or a
button. The box sizes itself around them, so wide content like https:// fits without tuning.
import { useState } from 'react';import { Input, Kbd } from '@arun-dev/ui';
const field = { display: 'grid', gap: 'var(--space-3xs)', maxInlineSize: '20rem' };
/** A slot's button is the consumer's own element — styled, labelled and focused by them. */const slotButton = { display: 'inline-flex', padding: 'var(--space-3xs)', border: 'none', borderRadius: 'var(--radius-sm)', background: 'none', color: 'inherit', font: 'inherit', cursor: 'pointer',};
export default function InputSlots() { const [visible, setVisible] = useState(false);
return ( <> <div style={field}> <label htmlFor="input-search">Search</label> <Input id="input-search" type="search" placeholder="Components…" startSlot={<SearchIcon />} endSlot={<Kbd>⌘K</Kbd>} /> </div>
<div style={field}> <label htmlFor="input-weight">Weight</label> <Input id="input-weight" type="number" inputMode="decimal" endSlot="kg" /> </div>
<div style={field}> <label htmlFor="input-password">Password</label> <Input id="input-password" type={visible ? 'text' : 'password'} defaultValue="correct horse" endSlot={ <button type="button" style={slotButton} aria-pressed={visible} aria-controls="input-password" onClick={() => setVisible((v) => !v)} > {visible ? 'Hide' : 'Show'} </button> } /> </div> </> );}
/** Decorative: the label already says "Search", so the icon is hidden from assistive tech. */function SearchIcon() { return ( <svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" > <circle cx="11" cy="11" r="7" /> <path d="m20 20-3.5-3.5" /> </svg> );}Slot content is yours
The slot renders what you pass and adds nothing. Mark a decorative icon aria-hidden, since the
label already names the field. Give a slot button type="button", its own accessible name and
its own focus style. The box’s focus ring follows the text control only, so a focused slot
button draws its own ring and doesn’t light up the whole field.
Clicking a decorative icon does not move focus into the input. Moving it would take JavaScript
in @arun-dev/ui, which decision 3 rules out. Click targets are the text and the label.
States
Section titled “States”All of them come from the native control, read with :has(). There is no data-* attribute
to set:
| State | Set it with |
|---|---|
| focus | the platform (:focus-visible) |
| invalid | aria-invalid on the input |
| disabled | disabled on the input |
Point aria-describedby at the error text too, so it is read out with the field.
| Prop | Type | Default | Description |
|---|---|---|---|
startSlot | ReactNode | Content before the text, inside the box: an icon, prefix or button. Mark decorative icons aria-hidden. | |
endSlot | ReactNode | Content after the text, inside the box: a unit, hint, or clear or reveal button. | |
className | string | Goes on the box that frames the input and its slots, not on the <input>. Every other prop, ref and render included, goes on the <input>. | |
render | ReactElement | Element or component to render instead of the default. Props, className, event handlers and ref are merged onto it. | |
ref | Ref<HTMLElement> | Ref to the rendered element. Merged with any ref on the render element. | |
…rest | HTMLAttributes | Anything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM. |
- For text-like types only:
text,email,password,search,tel,url,number. Use Checkbox, Radio group or Switch for the others. - Restyle every input with the
--input-*tokens, e.g.--input-heightor--input-border-color-focus.