Input (.ods-input) is the base text-entry control. It's rarely used bare — the rest of this family (Field, FieldLabel, FieldHint, FieldError, InputGroup) composes around it to build a complete, accessible form field: label, control, and either supplementary hint text or a validation error, plus an optional leading icon.
Installation
HTML — already included if you loaded the 3 stylesheets. See the Installation guide.
React
npm install @operant/ui
@operant/ui is currently a monorepo workspace package, not yet published to the public npm registry.
Usage
HTML
<div class="ods-field">
<label class="ods-label" for="workflow-name">Workflow name</label>
<input class="ods-input" id="workflow-name" placeholder="e.g. Incident triage" />
<span class="ods-hint">Shown in the run history.</span>
</div>
React
import { Field, FieldHint, FieldLabel, Input } from "@operant/ui";
export default function Example() {
return (
<Field style={{ width: 320 }}>
<FieldLabel htmlFor="workflow-name">Workflow name</FieldLabel>
<Input id="workflow-name" placeholder="e.g. Incident triage" />
<FieldHint>Shown in the run history.</FieldHint>
</Field>
);
}
Examples
Bare input
<input class="ods-input" placeholder="e.g. Incident triage" />
<Input placeholder="e.g. Incident triage" />
Invalid state
invalid adds .is-invalid (red border) and sets aria-invalid — unless you've already passed your own aria-invalid, in which case yours wins.
<input class="ods-input is-invalid" aria-invalid="true" value="bad value" />
<Input invalid defaultValue="bad value" />
Labeled field with a hint
Field is the vertical stack wrapper (display: flex; flex-direction: column; gap: var(--space-3)) — compose FieldLabel + Input + FieldHint inside it.
<div class="ods-field" style="width: 320px">
<label class="ods-label" for="workflow-name">Workflow name</label>
<input class="ods-input" id="workflow-name" placeholder="e.g. Incident triage" />
<span class="ods-hint">Shown in the run history.</span>
</div>
<Field style={{ width: 320 }}>
<FieldLabel htmlFor="workflow-name">Workflow name</FieldLabel>
<Input id="workflow-name" placeholder="e.g. Incident triage" />
<FieldHint>Shown in the run history.</FieldHint>
</Field>
Labeled field with an error
Swap FieldHint for FieldError once the field has failed validation — FieldError carries role="alert" so the message is announced, not just colored red.
<div class="ods-field" style="width: 320px">
<label class="ods-label" for="prompt-name">Prompt name</label>
<input class="ods-input is-invalid" id="prompt-name" aria-invalid="true" value="triage/v2 " />
<span class="ods-error-text" role="alert">This name is already in use.</span>
</div>
<Field style={{ width: 320 }}>
<FieldLabel htmlFor="prompt-name">Prompt name</FieldLabel>
<Input id="prompt-name" invalid defaultValue="triage/v2 " />
<FieldError>This name is already in use.</FieldError>
</Field>
With a leading icon — InputGroup
InputGroup pairs an icon with an Input; .ods-input-group svg auto-sizes whatever icon you render as the first child, and the nested .ods-input loses its own border (InputGroup's wrapper carries the border/focus ring instead).
<div class="ods-field" style="width: 320px">
<label class="ods-label" for="search-runs">Search runs</label>
<div class="ods-input-group">
<svg aria-hidden="true" viewBox="0 0 16 16"><!-- search icon --></svg>
<input class="ods-input" id="search-runs" placeholder="Search by trace id or citation…" />
</div>
</div>
import { Search } from "react-bootstrap-icons";
import { Field, FieldLabel, Input, InputGroup } from "@operant/ui";
<Field style={{ width: 320 }}>
<FieldLabel htmlFor="search-runs">Search runs</FieldLabel>
<InputGroup>
<Search aria-hidden="true" />
<Input id="search-runs" placeholder="Search by trace id or citation…" />
</InputGroup>
</Field>;
API Reference
<Input>
Extends native InputHTMLAttributes<HTMLInputElement>.
| Prop | Type | Default | Description |
|---|---|---|---|
invalid | boolean | false | Adds .is-invalid and sets aria-invalid (unless you pass your own aria-invalid, which takes precedence). |
className | string | — | Additional classes merged onto the input. |
All other native <input> attributes (type, value, placeholder, disabled, onChange, aria-*, data-*, etc.) are forwarded.
<Field>
Thin wrapper rendering .ods-field. Accepts all native <div> attributes — no props of its own.
<FieldLabel>
Renders a real <label class="ods-label">. Extends native LabelHTMLAttributes<HTMLLabelElement> — pass htmlFor to associate it with its control.
<FieldHint>
Renders <span class="ods-hint">. Accepts all native <span> attributes — supplementary, non-error guidance.
<FieldError>
Renders <span class="ods-error-text"> with role="alert" by default (overridable, though you shouldn't need to). Accepts all native <span> attributes.
<InputGroup>
Renders <div class="ods-input-group">. Accepts all native <div> attributes — render an icon (e.g. from react-bootstrap-icons) followed by an Input as children.
HTML-side modifiers: .ods-input (base) + .is-invalid. .ods-field has no modifiers. .ods-input-group has no modifiers; it relies on descendant selectors (.ods-input-group input, .ods-input-group svg) to style its children.
Accessibility
invalidwiresaria-invalidautomatically from the prop, not just the.is-invalidvisual class (AGENTS.md #8) — screen readers get the state even if someone only looks at the rendered style.FieldErrordefaults torole="alert"so validation errors are announced when they appear, matching AGENTS.md #8's "errors viarole=\"alert\"" rule.FieldHinthas no such role — it's supplementary, not urgent.- Always associate
FieldLabel'shtmlForwith the control'sid— an unlabeledInput(barearia-labelaside) is inaccessible to screen reader users navigating by form field. - When a field has both a hint and an error, prefer swapping the hint for the error rather than showing both — stacking non-error and error text under the same control is confusing to announce.
InputGroup's icon should bearia-hidden="true"— it's decorative; the label carries the field's accessible name.