Input
A native text input in the theme's control chrome, with a two-way value.
Basic
value is two-way: type here and the state below follows.
Code
<Label htmlFor="name">Name</Label>
<Input value={state.name} attrs={{ id: "name" }} />Usage
import { Input } from "@elements/ui/input";
<Input value={form.email} type="email" placeholder="you@example.com" />
<Input value={form.name} invalid={!!form.error} size="sm" />
<Input type="file" multiple files={form.photos} attrs={{ accept: "image/*" }} />Examples
Value change
onValueChange gets the new value and details before the value changes; details.cancel() keeps the old one. This one takes digits only.
Code
<Input value={state.pin} attrs={{ inputmode: "numeric" }} onValueChange={(v, d) => digitsOnly(state, v, d)} />
function digitsOnly(state: State, value: string, details: ChangeDetails): void {
if (/\D/.test(value)) {
details.cancel();
}
}API key
A read only value with a description and a Copy button beside it. The label swaps in place.
Keep it secret. Rotate it if it leaks.
Code
<Label htmlFor="api-key">API Key</Label>
<div class="row">
<Input value="sk_live_51H8x2eKq9" readonly attrs={{ id: "api-key", "aria-describedby": "api-key-hint" }} />
<Button onclick={() => copy(state)}>
<span class="swap-label" data-state={state.copied ? "on" : "off"}>
<span class="swap-off">Copy</span><span class="swap-on">Copied</span>
</span>
</Button>
</div>
<p class="hint" id="api-key-hint">Keep it secret. Rotate it if it leaks.</p>Types
The native type drives the keyboard, the picker and validation.
Code
<Input type="email" placeholder="you@example.com" />
<Input type="password" placeholder="Password" />
<Input type="number" placeholder="Quantity" />
<Input type="search" placeholder="Search" />
<Input type="date" />Sizes
Heights come from the theme's control tokens.
Code
<Input size="sm" placeholder="Small" />
<Input placeholder="Medium" />
<Input size="lg" placeholder="Large" />Disabled, read only, invalid
States change color, never size.
Code
<Input disabled value="Disabled" />
<Input readonly value="Read only" />
<Input invalid value="not-an-email" />File
type=file binds the picked files; the picker button follows the theme.
Code
<Input type="file" multiple files={state.photos} attrs={{ accept: "image/*" }} />Inline with a button
A required search: an empty submit is blocked by the browser.
Code
<form onsubmit={() => search(state)}>
<Input type="search" name="q" value={state.search} placeholder="Search" required />
<Button type="submit" variant="primary">Search</Button>
</form>Required
Inside a Field, required adds the asterisk and the browser's check.
The asterisk marks it; the browser blocks an empty submit.
Code
<Field label="Username" required description="The asterisk marks it; the browser blocks an empty submit.">
<Input value={state.username} />
</Field>Badge
A FieldLabel part takes a Badge beside the text.
Code
<Field>
<FieldLabel>Webhook URL{" "}<Badge variant="outline">Beta</Badge></FieldLabel>
<Input type="url" value={state.webhook} placeholder="https://api.example.com/webhook" />
</Field>Field group form
Fields in a FieldGroup; the form validates each one natively.
Code
<form onsubmit={() => send(state)}>
<FieldGroup>
<Field label="Name"><Input value={state.name} autocomplete="name" /></Field>
<Field label="Email" required><Input type="email" value={state.email} /></Field>
<Button type="submit" variant="primary">Submit</Button>
</FieldGroup>
</form>Form
Name, email, phone, country and address in one form; Cancel resets it.
Code
<form onsubmit={() => send(contact)}>
<FieldGroup>
<Field label="Name"><Input value={contact.name} placeholder="Jane Doe" /></Field>
<Field label="Email" description="We'll never share your email with anyone.">
<Input type="email" value={contact.email} placeholder="john@example.com" />
</Field>
<div class="grid">
<Field label="Phone"><Input type="tel" value={contact.phone} placeholder="+1 (555) 123-4567" /></Field>
<Field label="Country">
<Select value={contact.country} items={COUNTRIES} placeholder="Select a country" block />
</Field>
</div>
<Field label="Address"><Input value={contact.address} placeholder="123 Main St" /></Field>
<div class="actions">
<Button type="reset">Cancel</Button>
<Button type="submit" variant="primary">Submit</Button>
</div>
</FieldGroup>
</form>Input group and button group
An Input takes addons in an InputGroup, or joins buttons in a ButtonGroup.
Code
<InputGroup>
<InputGroupAddon><Icon name="search" /></InputGroupAddon>
<Input value={state.query} placeholder="Search" />
</InputGroup>
<ButtonGroup>
<Input value={state.url} placeholder="https://" />
<Button>Go</Button>
</ButtonGroup>Grid
Inputs fill their cell; two columns collapse to one on a phone.
Code
<Label htmlFor="first">First name</Label>
<Input attrs={{ id: "first" }} />
<Label htmlFor="last">Last name</Label>
<Input attrs={{ id: "last" }} />Right to left
Text, placeholder and the file button follow the inline direction.