OperantAI Workflow Design System
ComfortableCompact

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>.

PropTypeDefaultDescription
invalidbooleanfalseAdds .is-invalid and sets aria-invalid (unless you pass your own aria-invalid, which takes precedence).
classNamestring—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

  • invalid wires aria-invalid automatically from the prop, not just the .is-invalid visual class (AGENTS.md #8) — screen readers get the state even if someone only looks at the rendered style.
  • FieldError defaults to role="alert" so validation errors are announced when they appear, matching AGENTS.md #8's "errors via role=\"alert\"" rule. FieldHint has no such role — it's supplementary, not urgent.
  • Always associate FieldLabel's htmlFor with the control's id — an unlabeled Input (bare aria-label aside) 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 be aria-hidden="true" — it's decorative; the label carries the field's accessible name.