Text input
StableA one-line field in the inset lux-input finish, in two sizes, with an optional icon inside and a slot on the right.
A plain <input> with the field finish: a hairline ring that darkens on hover and a firm ring with a soft halo on focus. Add an icon for the kind of value, or a trailing button, unit or key hint. Takes every <input> attribute except size, and the ref reaches the input.
Icons and trailing slots
import { useState } from 'react'import { Field, IconButton, TextInput } from '@rchursin/product/ui'
/** A fresh sample secret, made in the click (never during render) */function newSecret() { const bytes = crypto.getRandomValues(new Uint8Array(8)) return `whsec_${Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')}`}
/** * icon draws inside the field on the left; trailing holds a button, a unit or a key hint on the * right. className styles the wrapper when either is set; inputClassName always styles the input. */export default function TextInputBasic() { const [query, setQuery] = useState('') const [secret, setSecret] = useState('whsec_4f9c2e71b8a05d36')
return ( <div className="flex w-full max-w-sm flex-col gap-5"> <Field label="Email">{(p) => <TextInput {...p} type="email" icon="mail" autoComplete="email" spellCheck={false} placeholder="name@example.com" />}</Field>Usage
import { TextInput, type TextInputProps } from '@rchursin/product/ui'<Field label="Email"> {(p) => <TextInput {...p} type="email" icon="mail" autoComplete="email" />}</Field>Examples
Sizes
md (32px) next to a md button for settings; lg (40px) next to an xl button for sign-in screens.
import { Button, Field, TextInput } from '@rchursin/product/ui'
/** * md (32px) for dense settings forms, next to a md button; lg (40px) for sign-in and other * single-purpose screens, next to an xl button. Phones always get 16px text, so iOS never zooms. */export default function TextInputSizes() { return ( <div className="flex w-full max-w-sm flex-col gap-6"> <Field label="Workspace name" hint="md · 32 pixels"> {(p) => ( <div className="flex gap-2"> <TextInput {...p} defaultValue="Platform team" /> <Button>Rename</Button> </div> )} </Field> <Field label="Email" hint="lg · 40 pixels"> {(p) => ( <div className="flex gap-2"> <TextInput {...p} size="lg" type="email" autoComplete="email" spellCheck={false} placeholder="name@example.com" /> <Button size="xl">Continue</Button> </div> )} </Field> </div> )}States
Hint, error, disabled and read-only. Shape and words carry each state, never colour.
import { Field, TextInput } from '@rchursin/product/ui'
/** * Error is a firmer ring plus words, never a colour. Disabled fades to half strength and says * why; read-only sinks into the page and can still be focused, selected and copied. */export default function TextInputStates() { return ( <div className="grid w-full grid-cols-1 gap-x-8 gap-y-6 sm:grid-cols-2"> <Field label="Skill name" hint="Lowercase words joined by dashes."> {(p) => <TextInput {...p} defaultValue="pr-review" spellCheck={false} />} </Field> <Field label="Short link" error="Short link must only include lowercase letters, numbers and dashes"> {(p) => <TextInput {...p} defaultValue="Q3 plan!" spellCheck={false} />} </Field> <Field label="Owner" hint="Only admins can change the owner."> {(p) => <TextInput {...p} defaultValue="Alex Rivera" disabled />} </Field> <Field label="Skill ID" hint="It never changes. Select it to copy."> {(p) => <TextInput {...p} defaultValue="skl_7f3k29" readOnly inputClassName="font-mono" />} </Field> </div> )}API
Text input props
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'md' | 'lg' | 'md' | Height 32 or 40 pixels. Text is 16px on phones in both sizes, so iOS never zooms in; from 640px wide it is 13 or 14px. |
icon | IconName | – | A decorative icon inside the field on the left. The input gets room for it. |
trailing | ReactNode | – | Something inside the field on the right: an IconButton, a short unit or a Kbd. The input gets 40px of right padding, so keep it small. |
className | string | – | Classes on the <input>, or on the wrapper <div> when icon or trailing is set. |
inputClassName | string | – | Classes that always land on the <input>, e.g. font-mono. |
CSS
| Class or variable | What it does |
|---|---|
lux-input | The field finish on any input, select or textarea: white (or 3.5% white in dark mode), an inset shadow and a 1px ring that darkens on hover. |
lux-input:focus-visible | Outline removed; a 1px ring at 70% and a 4px halo at 7% take its place. |
lux-input[aria-invalid="true"] | A 1.5px ring in the text colour (from product.css), plus the halo when focused. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves into the field, then on to a trailing button if there is one. |
| Esc | In a type="search" field, Chromium browsers and Safari clear the text on their own. The example also clears it in onKeyDown, so it works everywhere. |
- It is a native <input>, so labels, autofill, spell check and the phone keyboards for type="email", "tel" and "url" all work.
- The icon is aria-hidden and does not take pointer events; name the field with a label, never with the icon.
- Disabled fields drop to 50% opacity and leave the tab order; read-only fields keep focus, can be selected and copied, and sink to bg-sunken.
- A trailing IconButton needs its own label, such as “Make a new secret”.
Guidelines
Do
- Set type, autoComplete and inputMode for what the field holds, so phones show the right keyboard and browsers can fill it.
- Size the field to the value it takes when the layout allows.
- Use read-only, not disabled, for values people may want to copy.
Don’t
- Don’t use a placeholder as the label or the only hint.
- Don’t put long text in the trailing slot: the input reserves only 40px for it.
In use
Used on this site
Related
- FieldLabel above, hint under it, error in words between the hint and the control, with the ids and aria wiring done for you.
- Password inputA password field with a Show / Hide button and a caps-lock warning, where paste and password managers always work.
- Number fieldA number field with − and + steppers, following the APG spinbutton: arrows step, Page keys step ×10, Home and End jump.
- ComboboxA text field that suggests options as you type, for long lists like time zones or skills (APG list autocomplete).
Source
The component’s own files, exactly as they run on this site.
/** * Form fields. Labels sit above inputs, hints under the label, errors between the hint and the * input with an icon and the word "Error:" for screen readers (never colour alone). * Optional fields say "(optional)"; required ones carry no mark. * * <Field label="Email" hint="We send the sign-in code here." error={err}> * {(p) => <TextInput {...p} type="email" autoComplete="email" />} * </Field> */import { forwardRef, useId, useLayoutEffect, useRef, useState, type InputHTMLAttributes, type KeyboardEvent, type ReactNode, type SelectHTMLAttributes, type TextareaHTMLAttributes,