oriUI

Field

The shared shell for form controls — one source of truth for the label / hint / error / required contract that Input, Select, Textarea, Combobox, Slider, RadioGroup, and ColorPicker otherwise each wire by hand. Wrap a control in OriField and it adopts the field's id, aria-describedby, aria-invalid, required, disabled, and size — and stops rendering its own label and helper, so there is exactly one of each, wired identically every time. Group and composite controls (RadioGroup, Combobox's listbox, ColorPicker) name themselves via aria-labelledby pointing at the field's label.

Any control works: an Ori control nested inside wires up automatically (via provide/inject); a raw <input> or htmx markup wires up through the scoped-slot controlAttrs. The standalone HTML / @oriui/css path is the same .ori-field shell around any .ori-* field element.

Classes

The field is a wrapper that owns the label, the required asterisk, and the hint / error line; the control sits in between. One class repoints one token — the ori-font-size_* scale drives the label and helper text (and is inherited by a nested control).

ClassTypeDescription
ori-fieldBlockRequired base class (wrapper div).
ori-font-size_*Fontxs · sm · md · lg · xl · xxl — label + hint + error scale (and the nested control).
ori-field__label · ori-field__required · ori-field__hint · ori-field__errorPartlabel / required-asterisk / helper / error elements.
ori-field_fluidLayoutfull-width (stretches the field to 100 %).
for · id · aria-describedby · aria-invalidStatereal attributes on the control, not classes — shared with the label and helper.

With a control

Wrap any Ori control. The field renders the label and helper; the control renders just its field element and inherits the wiring.

We never share it.

html
<div class="ori-field ori-font-size_md">
    <label class="ori-field__label" for="email">Email</label>
    <input
        id="email"
        type="email"
        placeholder="you@example.com"
        aria-describedby="email-hint"
        class="ori-input ori-input__field ori-size-radius_md"
    />
    <p id="email-hint" class="ori-field__hint">We never share it.</p>
</div>
vue
<OriField label="Email" hint="We never share it.">
    <OriInput v-model="email" type="email" placeholder="you@example.com" />
</OriField>

Required & error

required adds the asterisk (hidden from assistive tech) and the native required on the control. error renders a role="alert" message, flips the control to aria-invalid, and supersedes the hint — there is never a dangling aria-describedby.

html
<div class="ori-field ori-font-size_md">
    <label class="ori-field__label" for="email">
        Email<span class="ori-field__required" aria-hidden="true">*</span>
    </label>
    <input
        id="email"
        type="email"
        required
        aria-invalid="true"
        aria-describedby="email-error"
        class="ori-input ori-input__field ori-size-radius_md"
    />
    <p id="email-error" class="ori-field__error" role="alert">Enter a valid email address.</p>
</div>
vue
<OriField label="Email" required error="Enter a valid email address.">
    <OriInput v-model="email" type="email" placeholder="you@example.com" />
</OriField>

Any control

The same shell wraps a Select or Textarea — each drops its own label and helper and reads the field's wiring instead.

Pick one.

A short introduction.

vue
<OriField label="Favourite fruit" hint="Pick one.">
    <OriSelect v-model="fruit" :options="options" placeholder="Choose…" />
</OriField>

<OriField label="Bio" hint="A short introduction.">
    <OriTextarea v-model="bio" :rows="3" placeholder="Tell us about yourself" />
</OriField>

Sizes

size lives on the field and propagates to controls that have a size scale (Input, Select, Textarea, Combobox, RadioGroup), so the label, helper, and control share one scale. xsxxl. Size-less controls (Slider, ColorPicker) keep their fixed dimensions — only the label + helper scale.

size = sm

size = lg

vue
<OriField label="Small" size="sm" hint="size = sm">
    <OriInput v-model="a" placeholder="sm" />
</OriField>

<OriField label="Large" size="lg" hint="size = lg">
    <OriInput v-model="b" placeholder="lg" />
</OriField>

Raw controls (CSS layer / htmx)

Without Vue, the field is still just markup: the .ori-field shell around any .ori-* field element, with the for / id / aria-describedby written by hand. In Vue, a non-Ori control reads the same values from the scoped-slot controlAttrs.

Letters and numbers only.

html
<div class="ori-field ori-font-size_md">
    <label class="ori-field__label" for="username">Username</label>
    <input id="username" aria-describedby="username-hint" class="ori-input ori-input__field ori-size-radius_md" />
    <p id="username-hint" class="ori-field__hint">Letters and numbers only.</p>
</div>
vue
<!-- a non-Ori control: spread the field's wiring with v-bind -->
<OriField v-slot="field" label="Username" hint="Letters and numbers only.">
    <input v-bind="field.controlAttrs" class="ori-input ori-input__field ori-size-radius_md" />
</OriField>

Accessibility

The contract holds across every layer — the standalone classes and the Vue component render the same attributes and ARIA wiring.

  • Labelable controls (Input, Select, Textarea, the Combobox input, the Slider range) are tied to the label by for / id; the id is auto-generated (useId) when you don't pass one, and the nested control adopts it. Group / composite controls (RadioGroup, ColorPicker, the Combobox listbox) can't be targeted by for, so they name themselves via aria-labelledby pointing at the field's label instead.
  • hint and error are wired through aria-describedby, referencing only the element actually rendered (error supersedes hint). Pass extra ids with describedby for a shared note.
  • error sets aria-invalid="true" and announces via role="alert"; invalid flips aria-invalid on its own for external validation.
  • required and disabled set the real native attributes on the control.
  • A control nested in a field renders no label or helper of its own — exactly one of each, so there is never a duplicate label or a dangling description.
KeyAction
TabMoves focus to / away from the control.

Framework API

The props and slots of the Vue component. The standalone CSS layer has no component API — its surface is the classes above. (Svelte bindings are planned.)

Props

PropTypeDefaultDescription
describedbystringExtra element id(s) appended to aria-describedby (e.g. a shared form note).
disabledbooleanfalseDrives the nested control's native disabled.
errorstringError message rendered below the control (role="alert"); sets aria-invalid="true".
fluidbooleanfalseFull-width — stretches the field to 100 % of its container.
hintstringHelper text below the control; hidden while error is shown.
idstringExplicit shared id; auto-generated via useId when omitted.
invalidbooleanfalseSets aria-invalid="true" without an error message (for external validation).
labelstringVisible <label> text, wired to the control via for / id.
requiredbooleanfalseRenders the asterisk and sets the control's native required.
sizeActionSize'md'Label + helper scale, propagated to the nested control (xs · sm · md · lg · xl · xxl).

Slots

The label, hint, and error slots each override the matching prop with custom content while keeping the full wiring — the label slot preserves the for / id association and the trailing required asterisk, and a slot-only #error still sets role="alert" and flips the field to aria-invalid (exactly as the prop does). The default slot is the control.

SlotFalls back toDescription
defaultThe control. Receives the scoped props below for wiring a non-Ori control.
labellabel propLabel content, inside the <label> — keeps the for / id link and the required asterisk.
hinthint propHelper text below the control; hidden while an error is shown.
errorerror propError message below the control; sets aria-invalid and role="alert", superseding hint.

The default slot receives scoped props for wiring a non-Ori control — Ori controls ignore them and read the context directly.

Slot propTypeDescription
controlAttrsobjectReady-to-v-bind { id, aria-describedby, aria-invalid, disabled, required }.
idstringThe shared field id.
describedbystringThe resolved aria-describedby (or undefined).
invalidbooleanWhether the field is in an invalid state.