Skip to content
Inputs

Inputs

Every component, with its variants, sizes and states. Change the appearance here and every sample follows.

Text field

A label, the control, and an error or hint the control points at.

Used in the demo at /settings

States

Enabled
Empty

Shown on invoices and in emails.

With a hint

Required

Error
Disabled
Focus

Sizes

Regular
Small
Search
<div class="or-field">
  <label class="or-label" for="store-name">Store name</label>
  <input class="or-input" type="text" id="store-name" aria-describedby="store-name-error" aria-invalid="true">
  <p class="or-error" id="store-name-error">
    <svg class="or-i or-i-16" aria-hidden="true"><use href="#or-i-circle-alert"></use></svg>
    Required
  </p>
</div>
<Field label={t('settings.general.organisation')} error={form.errors.organisation}>
  {({ id, describedBy, invalid }) => (
    <TextInput
      id={id}
      describedBy={describedBy}
      invalid={invalid}
      field="organisation"
      value={form.values.organisation}
      onChange={(value) => form.set('organisation', value)}
      onBlur={() => form.blur('organisation')}
    />
  )}
</Field>
<Field v-slot="slot" :label="t('settings.general.organisation')" :error="form.errors.value.organisation">
  <TextInput
    v-bind="slot"
    field="organisation"
    :model-value="form.values.value.organisation"
    @update:model-value="form.set('organisation', $event)"
    @blur="form.blur('organisation')"
  />
</Field>
<Field label={t('settings.general.organisation')} error={form.errors.organisation}>
  {({ id, describedBy, invalid }) => (
    <TextInput
      id={id}
      describedBy={describedBy}
      invalid={invalid}
      field="organisation"
      value={form.values.organisation}
      onChange={(value) => form.set('organisation', value)}
      onBlur={() => form.blur('organisation')}
    />
  )}
</Field>
<FormField v-slot="slot" :label="t('settings.general.organisation')" :error="form.errors.value.organisation">
  <FormTextInput
    v-bind="slot"
    field="organisation"
    :model-value="form.values.value.organisation"
    @update:model-value="form.set('organisation', $event)"
    @blur="form.blur('organisation')"
  />
</FormField>

Select

One value from a short, fixed list.

States

Enabled

That value is not allowed

Error
Disabled
Focus

Sizes

Regular
Small
<select class="or-select" id="status">
  <option value="paid">Paid</option>
  <option value="pending">Pending</option>
</select>
<Field label={t('table.status')} error={form.errors.status}>
  {({ id, describedBy, invalid }) => (
    <SelectInput id={id} describedBy={describedBy} invalid={invalid} field="status" value={form.values.status} options={STATUS_OPTIONS} onChange={(value) => form.set('status', value)} />
  )}
</Field>
<Field v-slot="slot" :label="t('table.status')" :error="form.errors.value.status">
  <SelectInput v-bind="slot" field="status" :model-value="form.values.value.status" :options="STATUS_OPTIONS" @update:model-value="form.set('status', $event)" />
</Field>
<Field label={t('table.status')} error={form.errors.status}>
  {({ id, describedBy, invalid }) => (
    <SelectInput id={id} describedBy={describedBy} invalid={invalid} field="status" value={form.values.status} options={STATUS_OPTIONS} onChange={(value) => form.set('status', value)} />
  )}
</Field>
<FormField v-slot="slot" :label="t('table.status')" :error="form.errors.value.status">
  <FormSelectInput v-bind="slot" field="status" :model-value="form.values.value.status" :options="STATUS_OPTIONS" @update:model-value="form.set('status', $event)" />
</FormField>

Text area

Several lines of plain text.

Used in the demo at /profile

States

Enabled

Required

Error
Disabled
Focus
<textarea class="or-textarea" rows="4" id="bio" aria-describedby="bio-hint"></textarea>
<p class="or-hint" id="bio-hint">280 characters left</p>
<textarea className="or-textarea" rows="4" id="bio" aria-describedby="bio-hint"></textarea>
<p className="or-hint" id="bio-hint">280 characters left</p>
<textarea class="or-textarea" rows="4" id="bio" aria-describedby="bio-hint"></textarea>
<p class="or-hint" id="bio-hint">280 characters left</p>
<textarea className="or-textarea" rows="4" id="bio" aria-describedby="bio-hint"></textarea>
<p className="or-hint" id="bio-hint">280 characters left</p>
<textarea class="or-textarea" rows="4" id="bio" aria-describedby="bio-hint"></textarea>
<p class="or-hint" id="bio-hint">280 characters left</p>

Checkbox

Three states, because a header checkbox has to say "some of this page".

Used in the demo at /orders

States

Unchecked
Checked
Some
Disabled
Focus
<label class="or-check">
  <input type="checkbox" aria-label="Select #10482">
  <span class="or-check__box" aria-hidden="true">
    <svg class="or-i or-i-16" aria-hidden="true"><use href="#or-i-check"></use></svg>
  </span>
</label>
<!-- Half-selected is a property, not an attribute: input.indeterminate = true -->
<Check
  checked={state === 'all'}
  indeterminate={state === 'some'}
  label={t('table.selectPage')}
  onChange={() => selection.toggleAll(rows)}
/>
<Check
  :checked="selection.selected.value.has(order.id)"
  :label="t('table.selectRow', { label: order.number })"
  @change="selection.toggle(order.id)"
/>
{/* Selection is client state; the rows around it are server-rendered. */}
<HeaderCheck label={t('table.selectPage')} />
<RowCheck id={order.id} label={t('table.selectRow', { label: order.number })} />
<TableCheck
  :checked="selection.selected.value.has(order.id)"
  :label="t('table.selectRow', { label: order.number })"
  @change="selection.toggle(order.id)"
/>

Switch

A yes or no that takes effect when the form is saved.

Used in the demo at /settings?tab=notifications

States

Off
On

Once a day, before 9 a.m.

With a hint
Disabled
Focus
<label class="or-switch" for="digest">
  <input type="checkbox" id="digest" checked>
  <span class="or-switch__track"><span class="or-switch__thumb"></span></span>
  <span>Weekly digest</span>
</label>
<SwitchField
  field="productUpdates"
  label={t('settings.notifications.productUpdates')}
  checked={form.values.productUpdates}
  onChange={(value) => form.set('productUpdates', value)}
/>
<SwitchField
  field="productUpdates"
  :label="t('settings.notifications.productUpdates')"
  :model-value="form.values.value.productUpdates"
  @update:model-value="form.set('productUpdates', $event)"
/>
<SwitchField
  field="productUpdates"
  label={t('settings.notifications.productUpdates')}
  checked={form.values.productUpdates}
  onChange={(value) => form.set('productUpdates', value)}
/>
<FormSwitchField
  field="productUpdates"
  :label="t('settings.notifications.productUpdates')"
  :model-value="form.values.value.productUpdates"
  @update:model-value="form.set('productUpdates', $event)"
/>

Typeahead

Several values from a long list, found by typing.

Used in the demo at /products

States

Empty
GiftsHome
Filled
Home
Open
Error
Home
Focus
<!-- shared/ui/listbox.mjs holds the keyboard rules; page-product-edit.js wires them. -->
<div class="or-combo" data-or-combobox="tags">
  <div class="or-combo__box">
    <input class="or-combo__input" type="text" role="combobox" aria-expanded="false" aria-controls="tags-list" aria-autocomplete="list">
  </div>
  <div class="or-listbox" id="tags-list" role="listbox" hidden></div>
</div>
<Combobox
  id={id}
  describedBy={describedBy}
  field="tags"
  invalid={invalid}
  values={form.values.tags}
  options={options('tag')}
  onChange={(values) => form.set('tags', values)}
/>
<Combobox
  v-bind="slot"
  field="tags"
  :model-value="form.values.value.tags"
  :options="options('tag')"
  @update:model-value="form.set('tags', $event)"
/>
<Combobox
  id={id}
  describedBy={describedBy}
  field="tags"
  invalid={invalid}
  values={form.values.tags}
  options={options('tag')}
  onChange={(values) => form.set('tags', values)}
/>
<FormCombobox
  v-bind="slot"
  field="tags"
  :model-value="form.values.value.tags"
  :options="options('tag')"
  @update:model-value="form.set('tags', $event)"
/>

Date picker

Typed as YYYY-MM-DD or chosen from a month grid the arrow keys can walk.

Used in the demo at /products

States

Closed
February 2026
Open

Required

Error
Focus
<!-- shared/ui/calendar.mjs builds the grid; page-product-edit.js opens it. -->
<div class="or-combo" data-or-date="publishAt">
  <div class="or-row">
    <input class="or-input" type="text" inputmode="numeric" placeholder="YYYY-MM-DD">
    <button type="button" class="or-btn or-btn--icon" aria-label="Choose a date" aria-expanded="false">
      <svg class="or-i or-i-18" aria-hidden="true"><use href="#or-i-calendar"></use></svg>
    </button>
  </div>
</div>
<DateField
  id={id}
  describedBy={describedBy}
  field="publishAt"
  value={form.values.publishAt}
  today={TODAY}
  onChange={(value) => form.set('publishAt', value)}
/>
<DateField
  v-bind="slot"
  field="publishAt"
  :model-value="form.values.value.publishAt"
  :today="TODAY"
  @update:model-value="form.set('publishAt', $event)"
/>
<DateField
  id={id}
  describedBy={describedBy}
  field="publishAt"
  value={form.values.publishAt}
  today={TODAY}
  onChange={(value) => form.set('publishAt', value)}
/>
<FormDateField
  v-bind="slot"
  field="publishAt"
  :model-value="form.values.value.publishAt"
  :today="TODAY"
  @update:model-value="form.set('publishAt', $event)"
/>

File upload

A drop zone with a real file input inside, so it works from a keyboard.

Used in the demo at /products

States

Drop images here

PNG or JPG, up to 2 MB each, 6 at most

Empty

Drop images here

PNG or JPG, up to 2 MB each, 6 at most

Dragging over

Drop images here

PNG or JPG, up to 2 MB each, 6 at most

  • lamp-front.jpg412 kB
  • lamp-detail.png1280 kB
Filled
<div class="or-drop">
  <svg class="or-i or-i-24" aria-hidden="true"><use href="#or-i-image-plus"></use></svg>
  <p class="or-muted">Drop images here</p>
  <input type="file" id="images" accept="image/png,image/jpeg" multiple>
  <button type="button" class="or-btn or-btn--sm">Choose files</button>
</div>
<FileField
  id={id}
  describedBy={describedBy}
  field="images"
  files={form.values.images}
  onChange={(files) => form.set('images', files)}
  onReject={(failure) => push(t(failure.key, failure.values), 'danger')}
/>
<FileField
  v-bind="slot"
  field="images"
  :model-value="form.values.value.images"
  @update:model-value="form.set('images', $event)"
  @reject="(failure) => toasts.push(t(failure.key, failure.values), 'danger')"
/>
<FileField
  id={id}
  describedBy={describedBy}
  field="images"
  files={form.values.images}
  onChange={(files) => form.set('images', files)}
  onReject={(failure) => push(t(failure.key, failure.values), 'danger')}
/>
<FormFileField
  v-bind="slot"
  field="images"
  :model-value="form.values.value.images"
  @update:model-value="form.set('images', $event)"
  @reject="(failure) => toasts.push(t(failure.key, failure.values), 'danger')"
/>

Rich text

Bold, italic, lists and links. Pasted text arrives clean.

Used in the demo at /products

States

Head of Operations. Runs the Lisbon warehouse and answers the difficult emails.

Filled
Empty

Head of Operations. Runs the Lisbon warehouse and answers the difficult emails.

Focus
<!-- shared/ui/richtext.mjs runs the commands and cleans what is pasted. -->
<div class="or-rte" data-or-richtext="description">
  <div class="or-rte__bar" role="toolbar" aria-label="Formatted text"></div>
  <div class="or-rte__body" role="textbox" aria-multiline="true" contenteditable="true"></div>
</div>
<RichText
  id={id}
  describedBy={describedBy}
  label={t('product.descriptionIn', { language: t(`language.${locale}`) })}
  value={form.values.description[locale]}
  onChange={(html) => form.set(`description.${locale}`, html)}
/>
<RichText
  v-bind="slot"
  :label="t('product.descriptionIn', { language: t(`language.${locale}`) })"
  :model-value="form.values.value.description[locale]"
  @update:model-value="form.set(`description.${locale}`, $event)"
/>
<RichText
  id={id}
  describedBy={describedBy}
  label={t('product.descriptionIn', { language: t(`language.${locale}`) })}
  value={form.values.description[locale]}
  onChange={(html) => form.set(`description.${locale}`, html)}
/>
<FormRichText
  v-bind="slot"
  :label="t('product.descriptionIn', { language: t(`language.${locale}`) })"
  :model-value="form.values.value.description[locale]"
  @update:model-value="form.set(`description.${locale}`, $event)"
/>

One-time code

Six boxes that behave like one field: typing moves on, pasting fills them all.

Used in the demo at /verify

States

Verification code
Empty
Verification code
Filled
Verification code
Error
<!-- shared/ui/otp.mjs decides what typing, pasting and Backspace do. -->
<fieldset class="or-field" data-or-code>
  <legend class="or-label">Verification code</legend>
  <div class="or-code">
    <input class="or-input or-code__box" type="text" inputmode="numeric" maxlength="1" autocomplete="one-time-code" aria-label="Digit 1 of 6">
    <!-- … five more -->
  </div>
</fieldset>
<CodeInput code={code} legend={t('auth.verify.legend')} error={form.errors.code} onChange={setCode} />
<CodeInput :code="code" :legend="t('auth.verify.legend')" :error="errors.code" @update:code="onCode" />
<CodeInput code={code} legend={t('auth.verify.legend')} error={form.errors.code} onChange={setCode} />
<AuthCodeInput :code="code" :legend="t('auth.verify.legend')" :error="errors.code" @update:code="onCode" />

Password strength

Advice, never a gate. Only the length can refuse a password.

Used in the demo at /register

Variants

Too short
Too short
Weak
Weak
Fair
Fair
Good
Good
Strong
Strong
<div class="or-meter" data-or-score="3" aria-label="Password strength">
  <span class="or-meter__track">
    <span class="or-meter__step is-on"></span><span class="or-meter__step is-on"></span>
    <span class="or-meter__step is-on"></span><span class="or-meter__step"></span>
  </span>
  <span class="or-meter__label" aria-live="polite">Good</span>
</div>
<StrengthMeter password={form.values.password} />
<StrengthMeter :password="values.password" />
<StrengthMeter password={form.values.password} />
<AuthStrengthMeter :password="values.password" />