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
Shown on invoices and in emails.
Required
Sizes
<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
That value is not allowed
Sizes
<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
Required
<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
<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
Once a day, before 9 a.m.
<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
<!-- 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
Required
<!-- 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
Drop images here
PNG or JPG, up to 2 MB each, 6 at most
Drop images here
PNG or JPG, up to 2 MB each, 6 at most
- lamp-front.jpg412 kB
- lamp-detail.png1280 kB
<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.
Head of Operations. Runs the Lisbon warehouse and answers the difficult emails.
<!-- 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
<!-- 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
<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" />