Forms

Form field

Wraps a control with its label, helper text and error, so every field reads the same.

Refined portVue source: WildFormField.vueStorybook story: Forms/FormFieldReact component: WFormRow

Try it

Turn each piece on and off, and swap the control underneath.

Threshold
42
Alerts fire when a reading crosses this.
Control
Required
Hint
Help
Error
<WildFormField
  label="Threshold"
  help="Alerts fire when a reading crosses this."
>
  <WildTextInput v-model="threshold" />
</WildFormField>

When to use

  • Use a form field around every input, select and toggle in a form, so each one gets a label and room for help and an error.
  • Use hint for a word or two on the label row, such as Optional.
Don’t use it
  • For a single checkbox. Use the Checkbox’s own label.

Anatomy

A label and optional hint, the control, then help text or an error.

Site name*
Bore 12 — north paddock
ThresholdOptional
42
Alerts fire when a reading crosses this.
<WildFormField label="Threshold" hint="Optional"
  help="Alerts fire when a reading crosses this.">
  <WildTextInput v-model="threshold" />
</WildFormField>
The label is 12px and the help 11px, both smaller than the 14px value, so the data outranks its scaffolding.

States

The error takes the help text’s place, so a field with help keeps its height when validation fires.

Threshold
42
Alerts fire when a reading crosses this.
Threshold
420
Must be between 0 and 100.
A field with no help text grows by one line when an error appears.

Controls

Site name
Bore 12
Region
North paddock
SMS alertsImmediate
Input, select and toggle take the same label, hint, help and error, so a settings column that mixes all three reads as one.

Content

  • Name the setting in the label, as a noun: Threshold, Region, SMS alerts.
  • Say what the setting does in the help: “Alerts fire when a reading crosses this.”
  • Say how to fix it in the error: “Must be between 0 and 100.”

Props

PropTypeDefaultDescription
labelstring—The field’s name. A placeholder isn’t a label.
hintstring—A short note right of the label.
helpstring—Guidance under the control.
errorstring—Replaces the help while it’s set.
requiredbooleanfalseA red asterisk after the label.

Tokens

TokenValueWhere it lands
--font-size-sm12pxLabel.
--font-size-xs11pxHint, help and error.
--font-weight-medium475Label.
--color-grey-900#4F4D55Label, hint and help ink.
--color-error#E12121Required asterisk and error text.
(no token)4pxGap between label, control and message.

Textarea

Takes longer text, growing with its content up to a limit and then scrolling.

Refined portVue source: WildTextareaBase.vueStorybook story: Forms/Textarea

Try it

Type in it. Past 90% of the limit, the counter turns amber.

0/120
Size
Error
Disabled
Max length
<WildTextareaBase
  v-model="notes"
  size="sm"
  :max-length="120"
/>

When to use

  • Use a textarea for free text that runs past one line, such as notes.
  • Set maxLength when the text has a hard limit, so people see the count before they reach it.
Don’t use it
  • For a single line, such as a name. Use Text input.

Sizes

sm is 140px tall with 12px text; lg is 200px with 13px. Both resize vertically only.

sm · 140px
lg · 200px

States

rest
hover
focus
default
error
disabled
Error outranks focus, so a field that’s both keeps its red edge and gains the focus ring outside it. Both draw a 2px edge inside the 1px border, so the text never moves.

Accessibility

It’s a native textarea, so typing and selection work as usual. Give it a name with ariaLabel. Error sets aria-invalid, and the counter is announced politely as it changes.

KeyWhat it does
TabMoves focus into the textarea, and out again.

Props

PropTypeDefaultDescription
modelValuestring—The text.
size'sm' | 'lg''sm'140px or 200px tall.
maxLengthnumber—Adds a counter; it turns amber past 90%.
placeholderstring—A prompt shown while empty.
errorbooleanfalseA 2px red edge; sets aria-invalid.
disabledbooleanfalseGreys the field and blocks typing.
ariaLabelstring—Accessible name.

Tokens

TokenValueWhere it lands
--radius-lg8pxCorners, the same as every field.
--font-size-sm12pxText at sm.
--font-size-md13pxText at lg.
--global-neutral-grey-600#DCDCDEBorder at rest, 1px.
--global-neutral-grey-800#ADACB0Border on hover.
--global-blue-1000#1877F2The 2px edge on focus, and the caret.
--global-red-800#E12121The 2px edge on error.
(no token)0 0 0 3px, 12%Halo on focus.
--global-neutral-grey-300#F1F1F1Ground when disabled.
--global-neutral-grey-1000#4F4D55Placeholder and counter.
--color-warning-active#9D6508Counter past 90%.

Select

Picks one option from a closed list, for choices too many for radios.

Refined portVue source: WildSelect.vueStorybook story: Forms/SelectReact component: WSelect

Try it

Open it, arrow through it and choose. The decommissioned bore can’t be picked.

Size
Error
Disabled
<WildSelect
  v-model="site"
  :options="sites"
  placeholder="Choose a site"
  size="sm"
/>

When to use

  • Use a select to choose one option from a list too long to show at once.
  • Keep an option that can’t be chosen in the list and disable it, so the list doesn’t change shape.
Don’t use it
  • For a few options people should compare side by side. Use a Radio group.
  • For a list long enough to need searching. Use Search select.

Anatomy

A trigger that shows the choice and a chevron, and a list that opens beneath it.

Choose a site
placeholder
Bore 12 — north paddock
chosen
Bore 12 — north paddock
open
<WildSelect v-model="site" placeholder="Choose a site" />
<WildSelect v-model="site" :options="sites" size="lg" />
The placeholder is a prompt, never a label. Give the select a label with Form field.

States

rest
Bore 12
hover
Bore 12
focus
Bore 12
error
Bore 12
disabled
Bore 12
Focus and open share a 2px blue-1000 border and chevron. Open also flips the chevron.

Sizes

Two, matching the input: 36 and 44px.

Bore 12
sm · 36
Bore 12
lg · 44

Accessibility

Screen readers hear a combobox, collapsed or expanded. Focus stays on it while the arrows move through the list, and each option is announced as you reach it.

KeyWhat it does
TabMoves focus to the select.
↓ / ↑Opens the list, then moves through it, skipping disabled options.
Enter / SpaceOpens the list, or chooses the highlighted option.
EscapeCloses the list and keeps focus on the select.

Props

PropTypeDefaultDescription
modelValuestring | number | null—The chosen value.
options{ value, label, disabled? }[]—The choices, in order.
placeholderstring—Shown until something is chosen.
size'sm' | 'lg''sm'36px or 44px tall.
disabledbooleanfalseGreys the select and blocks it.
errorboolean | stringfalseRed border; a string also shows the message.

Tokens

TokenValueWhere it lands
--radius-lg8pxThe trigger’s corners, and the list’s.
--color-border-default#E6E6E6Border at rest.
--global-blue-1000#1877F2Border when focused or open.
--color-error#E12121Border on error.
--font-size-sm12pxValue at sm.
--font-size-md13pxValue at lg, and every option.
--color-grey-200#F1F1F1Highlighted and chosen options.
--global-blue-1100#0C296AThe chosen option’s text, 12.07:1 on the grey row (blue-1000 would be 3.75:1).
--color-primary-active#1877F2Ring on the highlighted option.
--shadow-dropdown0 4px 12px rgba(0,0,0,.08), 0 2px 4px rgba(0,0,0,.04)The open list.

Numeric input

Takes a number, with steppers for small adjustments such as thresholds.

Refined portVue source: WildNumericInput.vueStorybook story: Forms/Numeric Input

Try it

Press the steppers, or tab to one and press Enter.

240
Steppers
Error
Disabled
<WildTextInput
  v-model="threshold"
  size="lg"
  type="quantity"
/>

When to use

  • Use it for a whole number people adjust in small steps, such as an alarm threshold.
Don’t use it

Anatomy

A 44px field with a decrease and an increase stepper inside the frame.

240
lg · 44
<WildTextInput v-model="threshold" type="quantity" size="lg" />
It’s always 44px tall, because the 34px steppers don’t fit the 36px frame.

States

The same five states as the text input.

rest
240
hover
240
focus
240
error
240
disabled
240

Accessibility

The steppers are buttons named Decrease and Increase. When the field is disabled, so are they.

KeyWhat it does
TabMoves to the decrease stepper, then the increase stepper.
Enter / SpaceSteps the number down or up.

Props

PropTypeDefaultDescription
modelValuenumber—The number.
type'quantity'—Adds the steppers.
size'lg''lg'Always 44px; the steppers need it.
errorboolean | stringfalseRed border; a string also shows the message.
disabledbooleanfalseGreys the field and disables the steppers.

Tokens

TokenValueWhere it lands
--radius-field10pxThe frame.
--radius-lg8pxThe steppers.
(no token)34pxStepper size.
--global-blue-1000#1877F2Border on focus, 2px.
(no token)15pxThe number.
--color-grey-800#7F7D83Stepper icon at rest.
--color-grey-1000#2D2B32Stepper icon on hover.

Phone input

Takes a phone number with its country and dialling code.

Refined portVue source: WildPhoneInput.vueStorybook story: Forms/Phone Input

Try it

Open the country list and choose. The flag and dialling code change together, and the number field doesn’t move.

21 555 0134
Country
Content
Error
Disabled
const phone = ref('+64215550134')

<WildPhoneInput
  v-model="phone"
  placeholder="412 345 678"
/>

When to use

  • Use it for any phone number, such as the mobile an SMS alarm is sent to.
  • Prefer it to one box with help saying “include the country code”. Picking the country from a list gets the code right.
Don’t use it
  • For a country on its own. Use Select.

Anatomy

A country selector and a number field, sharing one row and one frame style.

+1
202 555 0134
+64
21 555 0134
+966
55 555 0134
<WildPhoneInput v-model="phone" />
The selector shows the flag and code; the list adds the name. Its width is fixed, so the number field never shifts.

Country list

Nine countries in alphabetical order, with no search.

+64
412 345 678
+54 Argentina
+61 Australia
+880 Bangladesh
+52 Mexico
+264 Namibia
+64 New Zealand
+966 Saudi Arabia
+44 United Kingdom
+1 United States
The dialling code comes before the name. Names are spelled out, because NA and SA are ambiguous as codes.

States

Only the half you’re using changes.

rest
+64
21 555 0134
hover
+64
21 555 0134
focus
+64
21 555 0134
open
+64
21 555 0134
error
+64
21 555 0134
disabled
+64
21 555 0134
Errors frame the number only, because a country picked from a list can’t be wrong. An open list gives the selector the focus edge.

Accessibility

The selector is a combobox named Country calling code, and each country is announced as the arrows reach it. An error is announced as soon as it appears.

KeyWhat it does
TabMoves to the country selector, then the number.
↓ / ↑Opens the list, then moves through the countries.
Enter / SpaceOpens the list, or chooses the highlighted country.
EscapeCloses the list and keeps focus on the selector.

Props

PropTypeDefaultDescription
modelValuestring—The whole number, dialling code first.
placeholderstring'412 345 678'An example number, shown while empty.
errorstring—Message under the pair; frames the number.
disabledbooleanfalseDisables both halves.

Tokens

TokenValueWhere it lands
--radius-lg8pxBoth halves, and the country list.
--font-size-base14pxThe dialling code and the number.
--global-neutral-grey-600#DCDCDEBoth borders at rest.
--global-neutral-grey-800#ADACB0A border on hover.
--global-neutral-grey-500#E6E6E6The list edge, and disabled borders.
--global-blue-1000#1877F2The active half’s border, and the tick.
--color-error#E12121The number’s border, and the message.
--global-neutral-grey-300#F1F1F1The highlighted country.
--global-blue-1100#0C296AThe chosen country’s text.
--shadow-dropdown0 4px 12px rgba(0,0,0,.08), 0 2px 4px rgba(0,0,0,.04)The country list.
--spacing-28pxBetween the halves, and beside each flag.

Password input

Takes a password, with a toggle to show what has been typed.

Refined portVue source: WildPasswordInput.vueReact component: WPasswordInput

Try it

Press the eye to show the password, or tab to it and press Enter.

••••••••••••
Size
Content
Error
Disabled
<WildPasswordInput
  v-model="password"
  size="lg"
/>

When to use

  • Use it wherever someone types a password, such as sign-in.
Don’t use it

Anatomy

The input, with a show-and-hide toggle inside the frame.

••••••••••••
masked
wildeye-2026
revealed
Password
empty
<WildPasswordInput v-model="password" />
<WildPasswordInput v-model="password" :revealed="true" />
Masked dots sit 2px apart so they can be counted. Reveal is a toggle, so the password stays shown until it’s pressed again.

States

The same five states as the text input.

rest
••••••••••••
hover
••••••••••••
focus
••••••••••••
error
••••••••••••
disabled
••••••••••••
The eye holds its place when focus or an error thickens the border.

Errors

Pass the message as error. The field shows it underneath, so it works outside a Form field.

•••••
At least 12 characters.
Say what’s needed: “At least 12 characters.”

Accessibility

The eye is a toggle button named Show password, and screen readers hear whether it’s pressed. It’s disabled with the field.

KeyWhat it does
TabMoves focus to the eye.
Enter / SpaceShows or hides the password.

Props

PropTypeDefaultDescription
modelValuestring—The password.
size'sm' | 'lg''lg'36px or 44px tall.
revealedbooleanfalseShows the password as text.
onReveal() => void—Called when the eye is pressed. Flip revealed here.
errorstring—Red border and a message underneath.
disabledbooleanfalseGreys the field and the eye.

Tokens

TokenValueWhere it lands
--radius-field10pxThe frame.
--global-blue-1000#1877F2Border on focus, 2px.
--color-error#E12121Error border and message.
--font-size-xs11pxThe error message.
--color-grey-800#7F7D83The eye at rest.
--color-grey-1000#2D2B32The eye on hover, and when revealed.
(no token)2pxLetter spacing of the masked dots.

Verification code

Takes a six-digit code in separate boxes that behave like one field, including paste.

Refined portVue source: WildVerificationCodeInput.vueStorybook story: Forms/Verification Code Input

Try it

Type a code, step back with Backspace, move with the arrow keys, or paste six digits.

Error
Disabled
<WildVerificationCodeInput
  v-model="code"
/>

When to use

  • Use it for a six-digit code sent to the user, on login and on any action that needs a second factor.
Don’t use it

States

Six boxes that behave as one field.

empty
4
8
2
filling
4
8
2
9
1
3
complete
4
8
2
9
1
3
error
<WildVerificationCodeInput v-model="code" />
A filled box looks like an empty one, so the digits show how far through the code you are. On error all six turn red, because the code is wrong as a whole.

Content

  • Write the error as what happened, in one short sentence: “That code has expired.”
  • Clear the error as soon as the code changes, because the field keeps it until you do.

Accessibility

Each box is its own input, named “Digit 1 of 6” and so on, inside one group. The error message is announced when it appears.

KeyWhat it does
A digitFills the box and moves to the next one.
BackspaceClears the box. On an empty box, clears the one before and moves there.
← / →Moves to the previous or next box.
PasteFills from the first box, then focuses the next empty one.
TabMoves through the boxes, then out.

Props

PropTypeDefaultDescription
v-modelstring—The code, up to six digits.
errorstring—Message under the boxes. Turns all six red.
disabledbooleanfalseBlocks all six boxes.

Tokens

TokenValueWhere it lands
--radius-lg8pxEach box, the same as every field.
--global-blue-1000#1877F2The 2px edge of the box with the caret, and the caret itself. 4.23:1; the cyan was 2.53:1.
--global-neutral-grey-800#ADACB0Any box on hover.
--global-neutral-grey-600#DCDCDEEvery box at rest, empty or filled, 1px.
--color-error#E12121The 2px edge of all six boxes, and the message.
--global-neutral-grey-300#F1F1F1The ground when disabled.
--font-size-2xl20pxThe digits.
--font-weight-medium475The digits.

Chip input

Turns typed entries, such as email addresses, into removable chips.

Live React portVue source: WildChipInput.vueStorybook story: Forms/Chip InputReact component: WChipInput

Try it

Remove a chip with its ×, or tab to the × and press Enter. Turn on Suggestions to see the match list.

JMJoel Mannsam@orchard.nznot-an-email
Size
Suggestions
Error
Disabled
<WildChipInput
  v-model="recipients"
  size="md"
  placeholder="Add people by email"
/>

When to use

  • Use it to collect several email addresses in one field, such as when inviting people.
  • Check each address as its own chip, so one typo marks that address and not the whole line.
Don’t use it

Anatomy

Chips for what’s been added, then the text being typed. Each chip carries its own state and a × in its own colour, which fills a circle on hover.

JMjoel@mywildeye.comSOsam@orchard.nz
Add people by email
<WildChipInput v-model="recipients"
  placeholder="Add people by email" />

Chip states

Four states show how far an address has got: typed, recognised as an existing user, rejected, or already sent.

JMJoel Mannsam@orchard.nznot-an-emailAKana@mywildeye.com
Recognised resolves an address to a person with initials, so a typo of a colleague’s address stands out before Send.

Field states

rest
joel@mywildeye.com
hover
joel@mywildeye.com
focus
joel@mywildeye.com
error
joel@mywildeye.com
disabled
joel@mywildeye.com
The frame thickens to 2px on focus and error, and the chips hold their place.

Suggestions

JMJoel Mannsa
SOSam Okonkwosam@orchard.nzAdmin
SPSana Patelsana@mywildeye.comViewer
Matches from the account appear as you type, with a role badge so an admin isn’t invited as a viewer by accident.

Accessibility

Each × is a button named after its chip, such as “Remove sam@orchard.nz”, so a screen reader can tell them apart.

KeyWhat it does
TabMoves to each chip’s remove button in turn.
Enter / SpaceRemoves that chip.

Props

PropTypeDefaultDescription
chips{ label, state?, initials? }[][]The chips, in order.
onRemoveChip(label: string) => void—Called when a chip’s × is pressed.
size'sm' | 'md' | 'lg''md'Minimum height: 36, 40 or 44px.
state'rest' | 'hover' | 'focus' | 'error' | 'disabled''rest'The frame. Disabled also disables every ×.
placeholderstring—Shown while there are no chips.
typingstring—Text after the chips, still being typed.
suggestions{ label, sublabel?, initials?, badge?, active? }[]—The match list under the field.
widthnumber | string—Field width.

Tokens

TokenValueWhere it lands
--radius-lg8pxThe field frame.
--global-blue-1000#1877F2Frame on focus.
--color-error#E12121Frame on error.
--global-blue-100 / -1100#EBFAFF / #0C296ARecognised chip ground and ink.
--global-red-200 / -1100#FFE3E3 / #711111Invalid chip ground and ink.
--global-green-100 / -1000#EBFBF1 / #016626Sent chip ground and ink.
--global-{family}-1000 / 1100the chip’s familyThe × at rest, then on hover.
--global-{family}-200 / 300the chip’s familyThe × circle on hover, then pressed, on the recognised (blue) and sent (green) chips.
--global-neutral-grey-300 / 500#F1F1F1 / #E6E6E6The × circle on the neutral chip, which has no family tint.
--global-red-300 / 400#FFC9C9 / #FAA4A4The × circle on the invalid chip. It already rests on red-200, so the circle goes one step deeper to show.
--global-blue-400 / red-300 / green-300#A3E7FF / #FFC9C9 / #C1F4D4Recognised, invalid and sent chip borders.
--shadow-dropdown0 4px 12px rgba(0,0,0,.08), …The suggestion list.

Checkbox

Turns an option on or off, alone or as one of several independent choices.

Refined portVue source: WildCheckbox.vueStorybook story: Forms/CheckboxReact component: WCheckbox

Try it

Click the label or the box, or tab to it and press Space.

Send alertsEvery reading outside the threshold
Size
Indeterminate
Error
Disabled
<WildCheckbox
  v-model="checked"
  size="md"
  label="Send alerts"
  sub="Every reading outside the threshold" />

When to use

  • Use a checkbox for an option that takes effect when the form is saved.
  • Use one per option when several can be on at once.
  • Use indeterminate on a “select all” row when only some of the rows beneath it are checked.
Don’t use it
  • For a setting that applies straight away. Use a toggle.
  • To pick one option from a set. Use a radio group.

Anatomy

A box, a tick, and an optional label with a sub-line. The label and sub-line are part of the target.

Send alertsSend alertsEvery reading outside the thresholdInclude archived sites
<WildCheckbox v-model="checked" label="Send alerts" />
<WildCheckbox
  v-model="checked"
  label="Send alerts"
  sub="Every reading outside the threshold" />

Sizes

Two sizes, 16px and 14px. The default is md.

md · 16
sm · 14
Use sm in dense rows, such as the header of a data grid, where it sits beside 11px labels.

States

Off, on and indeterminate, against each interaction state.

rest
hover
focus
error
unchecked
checked
indeterminate
Keyboard focus is a ring outside the box, so it shows on every fill.
Send alerts
disabled
Send alerts
disabled · checked
Disabled dims the whole row to 50%, label included.

Accessibility

A screen reader hears the label, then checked, not checked or mixed. A checkbox with no visible label needs ariaLabel.

KeyWhat it does
TabMoves focus to the checkbox. A disabled one is skipped.
SpaceTicks or clears it. Indeterminate becomes checked.

Props

PropTypeDefaultDescription
checkedbooleanfalseWhether the box is ticked.
indeterminatebooleanfalseShows the dash, whatever checked says.
onChange(checked: boolean) => void—Makes it operable. Called with the next value.
labelReactNode—Text beside the box. Part of the target.
substring—A second line under the label.
size'md' | 'sm''md'16px or 14px.
disabledbooleanfalseDims the row and leaves the tab order.
state'rest' | 'hover' | 'active' | 'focus' | 'error'—Forces a frame. Use error for a failed field.
ariaLabelstring—Accessible name when there is no label.

Tokens

TokenValueWhere it lands
--radius-sm4pxBox corners.
--color-primary#00AEEFChecked and indeterminate fill.
--color-primary-active#1877F2Checked fill on hover, and the focus ring.
--global-blue-1000#1877F2Unchecked border on hover, 4.2:1. It stays darker than the resting grey.
--global-neutral-grey-900#7F7D83Unchecked border, 4.1:1. A disabled box keeps a lighter grey.
--color-error#E12121Error border and fill.
(no token)1px gap, 2px ringKeyboard focus, outside the box.

Radio

Picks one option from a small set where every option stays visible.

Refined portVue source: WildRadio.vueStorybook story: Forms/RadioReact component: WRadio

Try it

Click an option, or tab to one and press Space. For arrow keys between options, use Radio group.

Every readingEvery sample the logger sendsOnly outside the thresholdRoughly 40 a week for this siteNeverAlerts stay in the dashboard only
Size
Sub-line
Error
Disabled
<WildRadioGroup v-model="mode">
  <WildRadio
    value="all"
    size="sm"
    label="Every reading"
    sub="Every sample the logger sends" />
  <WildRadio
    value="threshold"
    size="sm"
    label="Only outside the threshold"
    sub="Roughly 40 a week for this site" />
  <WildRadio
    value="never"
    size="sm"
    label="Never"
    sub="Alerts stay in the dashboard only" />
</WildRadioGroup>

When to use

  • Use radios when only one option can be chosen and every option should stay visible.
  • Use Radio group for a labelled set. It adds one tab stop, arrow keys and an error message.
Don’t use it
  • For a choice too long to show in full. Use a select.
  • For a single on or off. Use a checkbox or a toggle.

Anatomy

A ring, a dot, and an optional label with a sub-line. The label and sub-line are part of the target.

Every readingOnly outside the thresholdRoughly 40 a week for this siteNeverAlerts stay in the dashboard only
<WildRadioGroup v-model="mode">
  <WildRadio value="all" label="Every reading" />
  <WildRadio value="threshold" label="Only outside the threshold"
    sub="Roughly 40 a week for this site" />
</WildRadioGroup>

Sizes

Two sizes: 18px with a 6px dot, and 24px with a 10px dot. The default is sm.

sm · 18 / dot 6
lg · 24 / dot 10

States

rest
hover
active
focus
error
unselected
selected
Keyboard focus is a solid ring outside the radio, so it reads differently from hover.
Every reading
disabled
Every reading
disabled · selected
Disabled dims the whole row to 40%, label included.

Content

  • Keep labels short and parallel: Every reading, Only outside the threshold, Never.
  • Put the consequence in the sub-line, such as “Roughly 40 a week for this site”.
  • Use sentence case, with no full stop.

Accessibility

A screen reader hears the label, then selected or not selected. A radio with no visible label needs ariaLabel.

KeyWhat it does
TabMoves focus to the next radio. Disabled ones are skipped.
SpaceSelects the focused radio.

Props

PropTypeDefaultDescription
checkedbooleanfalseWhether this option is the chosen one.
onSelect() => void—Makes it operable. Called on click or Space.
labelReactNode—Text beside the radio. Part of the target.
substring—A second line under the label.
size'sm' | 'lg''sm'18px or 24px.
disabledbooleanfalseDims the row and leaves the tab order.
state'rest' | 'hover' | 'active' | 'focus' | 'error'—Forces a frame. Use error for a failed field.
ariaLabelstring—Accessible name when there is no label.

Tokens

TokenValueWhere it lands
--radius-full9999pxRing and dot.
--color-primary#00AEEFSelected fill.
--color-primary-active#1877F2Hover and press when selected; focus ring.
--global-neutral-grey-900#7F7D83Resting border, 4.1:1. A disabled radio keeps a lighter grey.
--global-blue-1000#1877F2Unselected border on hover, 4.2:1. It stays darker than the resting grey.
--color-error#E12121Error border, fill and label.
(no token)rgba(0,174,239,.06 / .12)Halo on hover and press.
(no token)1px gap, 2px ringKeyboard focus, outside the radio.

Toggle

Switches a setting on or off, taking effect immediately.

Refined portVue source: WildToggle.vueStorybook story: Forms/ToggleReact component: WToggle

Try it

Click it, or tab to it and press Space or Enter.

Size
Label
Disabled
<WildToggle
  v-model="enabled"
  size="md"
  label="Send SMS alerts"
/>

When to use

  • Use a toggle for a setting that takes effect the moment it’s flipped.
  • Use it for one setting at a time, each with its own label.
Don’t use it
  • For an option that waits for Save. Use a checkbox, so nobody wonders whether their change took effect.
  • To pick one of several. Use a radio group.

Anatomy

A track and a knob, with an optional label to the right. The label is part of the target.

Send SMS alertsInclude archived sites
<WildToggle v-model="enabled" label="Send SMS alerts" />

Sizes

Two sizes, each with 2px between the knob and the track. The default is md.

md · 44×24 / knob 20
sm · 36×20 / knob 16

States

off
on
hover
pressed
focus
disabled · off
disabled · on
Off is grey, so switching an alert off reads as quiet rather than as a fault.

Accessibility

A screen reader hears the label, then on or off. A toggle with no visible label needs ariaLabel.

KeyWhat it does
TabMoves focus to the toggle. A disabled one is skipped.
Space / EnterFlips it.

Props

PropTypeDefaultDescription
onboolean—Whether the setting is on.
onChange(on: boolean) => void—Makes it operable. Called with the next value.
labelstring—Text to the right of the track.
size'md' | 'sm''md'44×24 or 36×20.
disabledbooleanfalseDims it to 50% and leaves the tab order.
state'rest' | 'hover' | 'active' | 'focus'—Forces a frame, for static examples.
ariaLabelstring—Accessible name when there is no label.

Tokens

TokenValueWhere it lands
--radius-full9999pxTrack and knob.
--color-primary#00AEEFTrack when on.
--color-primary-hover#0ABEFFTrack when on, on hover.
--color-primary-active#1877F2Pressed track when on, and the focus ring.
--global-neutral-grey-900#7F7D83Track when off, 4.1:1. A disabled switch keeps a lighter grey.
--global-neutral-grey-1000 / -1100#4F4D55 / #2D2B32Off track on hover and press.
--ease-in-outcubic-bezier(.4,0,.2,1)Knob travel, 150ms.
(no token)2px gap, 2px ringKeyboard focus, outside the track.

Search bar

Filters a list or page by text, with a clear button once there is a query.

Refined portVue source: WildSearchBar.vueStorybook story: Forms/Search Bar

Try it

Type into it, then clear it with the ×. Clearing puts focus back in the field.

Size
Disabled
<WildSearchBar
  v-model="query"
  size="md"
  placeholder="Search sites"
/>

When to use

  • Use it to filter a list or a page by text.
  • Pass onClear so people can empty the query in one press.
Don’t use it
  • For a value that’s saved with a form. Use a text input.

Anatomy

A magnifier, the text field and a clear button, in one frame.

<WildSearchBar v-model="query"
  placeholder="Search sites" />
The clear button appears only once there’s a query to clear.

Sizes

Three heights: 36, 40 and 44px. The default is md.

sm
md
lg

States

The frame is the text input’s, so a search bar and a text field focus the same way on one form.

rest
hover
focus
error
disabled

Accessibility

The field is named by ariaLabel, or by its placeholder when there’s none. The clear button is named “Clear search”.

KeyWhat it does
TabMoves to the field, then to the clear button.
Enter / SpaceOn the clear button, empties the query and returns focus to the field.

Props

PropTypeDefaultDescription
valuestring—The query.
onChange(value: string) => void—Called on every keystroke.
onClear() => void—Shows the clear button while there’s a query.
placeholderstring'Search'Shown while empty. Also the fallback name.
size'sm' | 'md' | 'lg''md'Height: 36, 40 or 44px.
errorbooleanfalseRed frame.
disabledbooleanfalseBlocks typing and dims the frame.
trailingReactNode—A slot at the end, such as a shortcut hint.
widthnumber | string'100%'Frame width.
ariaLabelstring—Accessible name for the field.
autoFocusbooleanfalseFocuses the field on mount.
state'rest' | 'hover' | 'focus'—Forces a frame, for static examples.

Tokens

TokenValueWhere it lands
--radius-field10pxThe frame.
--global-neutral-grey-800#ADACB0Frame on hover.
--global-blue-1000#1877F2Frame on focus.
--color-error#E12121Frame on error.
--global-neutral-grey-900#7F7D83Magnifier, and the clear button at rest.
--global-neutral-grey-400 / -1100#ECECED / #2D2B32Clear button ground and ink on hover.
--size-icon-sm16pxThe magnifier.

Slider

Sets a value on a continuous range, for thresholds that are judged rather than typed.

Refined portVue source: WildSlider.vueStorybook story: Forms/SliderReact component: WSlider

Try it

Drag the handle, or tab to it and use the arrow keys, Home or End.

35%
Size
Colour
Value badge
Ticks
Disabled
<WildSlider
  v-model="threshold"
  label="Refill threshold"
  :min="0"
  :max="100"
  :step="1"
  size="md"
  color="blue"
  show-value
  :format-value="(n) => `${n}%`"
/>

When to use

  • Use a slider for a value that’s judged rather than typed, such as a refill threshold set by where the line usually sits.
  • Turn on ticks only where the value is stepped.
  • Keep it blue. Green, amber and red already mean state in this product.
Don’t use it
  • For a figure read off a datasheet, such as a calibration offset. Use a numeric input.

Anatomy

A label and an optional value badge, the track and its fill, and the handle. With ticks on, the min and max sit underneath.

30%
30%
<WildSlider v-model="v" />
<WildSlider v-model="v" label="Refill threshold" show-value />
<WildSlider v-model="v" label="Refill threshold" show-value show-ticks :step="10" />

Sizes

Tracks of 4, 6 and 8px, with 16, 20 and 24px handles. The default is md.

25
50
75
<WildSlider v-model="v" size="sm" />
<WildSlider v-model="v" size="md" />
<WildSlider v-model="v" size="lg" />
Below 640px wide, md takes the lg track and handle, for touch.

Ticks

Up to 11 marks, each on a value the handle can stop on. Ticks bring the min and max labels with them.

50
7
<WildSlider v-model="v" :min="0" :max="100" :step="10" show-ticks />
<WildSlider v-model="v" :min="1" :max="10" :step="1" show-ticks />

Colour

Five fills. Each one colours the handle’s ring and its focus ring too.

62
62
62
62
62

States

Hover and focus lift the handle, and focus adds a ring. Disabled greys the fill but keeps the value readable.

30
70
<WildSlider v-model="v" label="Normal" show-value />
<WildSlider v-model="v" label="Locked setting" show-value disabled />

Accessibility

It’s a native range input, named by label or ariaLabel, and it announces the formatted value. The pointer target is 24px tall at every size.

KeyWhat it does
TabMoves focus to the handle.
← / ↓Steps down by one step.
→ / ↑Steps up by one step.
Home / EndJumps to the min or the max.
Page Up / Page DownMoves by a larger stride.

Props

PropTypeDefaultDescription
value / defaultValuenumber0The value, controlled or uncontrolled.
onChange(value: number) => void—Fires throughout a drag, not only on release.
min / maxnumber0 / 100The range.
stepnumber1Increment, and the arrow-key stride.
size'sm' | 'md' | 'lg''md'Track 4, 6 or 8px; handle 16, 20 or 24px.
color'blue' | 'green' | 'orange' | 'red' | 'purple''blue'Fill, handle ring and focus ring.
disabledbooleanfalseGreys it out; blocks pointer and keyboard.
showValuebooleanfalseThe value badge in the header.
showTicksbooleanfalseStep marks, plus the min and max labels.
labelstring—Header text. Also names the slider.
formatValue(value: number) => string—Formats the badge and the spoken value.
ariaLabelstring—Accessible name when there is no label.

Tokens

TokenValueWhere it lands
--global-blue-1000#1877F2Fill, handle ring and focus ring, blue. 3.59:1 on the track.
--global-green-900#018030Green.
--global-squash-900#C47E0AOrange.
--global-red-800#E12121Red.
--global-purple-900#7B238DPurple.
--global-neutral-grey-400#ECECEDThe unfilled track.
--global-neutral-grey-900#7F7D83Ticks on the track. White on the fill.
--global-neutral-grey-200 / -1000#F5F5F5 / #4F4D55Value badge ground and ink.
--global-neutral-grey-1100#2D2B32The label.
(no token)#4F4D55Min and max labels.
--global-neutral-grey-300 / -800#F1F1F1 / #ADACB0Disabled track; disabled fill and ring.
--global-neutral-grey-700#C9C9CCThe label when disabled.
(no token)0 2px 4px rgba(10,9,11,.1)Handle shadow. Hover deepens it.
(no token)2px gap, 2px ringKeyboard focus, around the handle.
(no token)0.15s easeHandle lift and shadow.

Date picker

Picks a single date from a field that opens a calendar.

Refined portVue source: WildDatePicker.vueStorybook story: Forms/Date PickerReact component: WDatePicker

Try it

Click the field, or focus it and press Enter, to open the calendar. Turn on Min / max to limit it to 30 days.

Reading date
Stored as 2026-04-20
Size
Min / max
Error
Disabled
<WildDatePicker
  v-model="date"
  size="sm"
/>

When to use

  • Use it to pick one day, such as the date of a reading.
  • Set minDate and maxDate when only some days are valid. Days outside stay in place and can’t be picked.
Don’t use it

Anatomy

A trigger and a panel. The trigger uses the text input’s frame, and the panel is the calendar.

empty
holding a date

Sizes

Two heights, 36 and 44px, the same as the text input. The default is sm.

sm · 36
lg · 44

States

rest
error
disabled
Put the error message in the form field under the picker. The picker only turns red.

Accessibility

Opening the calendar moves focus to the chosen day, or to today. Closing it returns focus to the field.

KeyWhat it does
Enter / Space / ↓On the field, opens the calendar.
Backspace / DeleteOn the field, clears the date.
Arrow keysMove a day or a week, into the next or previous month.
Home / EndMove to the start or end of the week.
Page Up / Page DownMove a month. With Shift, a year.
Enter / SpaceOn a day, picks it and closes the calendar.
EscapeCloses the calendar without picking.

Props

PropTypeDefaultDescription
value / defaultValuestring''The date as ISO 8601, controlled or uncontrolled.
onChange(value: string) => void—Fires with the ISO string, keeping any time part.
size'sm' | 'lg''sm'36 or 44px.
minDate / maxDateDate | string | nullnullThe first and last days that can be picked.
errorstring''Any non-empty text turns the frame red.
disabledbooleanfalseBlocks the field and the calendar.
placeholderstring'Select date'Shown while empty. Also the field’s name.
widthnumber | string220Field width.

Tokens

TokenValueWhere it lands
--radius-field10pxThe field.
--global-blue-1000#1877F2Field on focus and while open.
--color-error / -light#E12121 / #FFE3E3Error border and ground.
--global-neutral-grey-900#7F7D83Calendar icon; blue-1000 on focus.
--shadow-lg0 10px 15px -3px …The open calendar.
--z-index-dropdown100The open calendar.
(no token)4pxGap between field and calendar.

Calendar

The month grid for picking a day, used inside the date picker and on its own.

Refined portVue source: WildCalendar.vueStorybook story: Forms/Calendar

Try it

Click a day, or tab into the grid and use the arrow keys. The arrows carry on into the next month.

November 2026

Su
Mo
Tu
We
Th
Fr
Sa
Sunday, November 8, 2026
Min / max
<WildCalendar
  v-model="date"
/>

When to use

  • Use it on its own where the month should stay open, such as in a schedule editor.
  • Set bounds when only some days are valid. Days outside keep their place, so the columns still line up with the weekdays.
Don’t use it

Anatomy

Three bands on a 340px surface. Every month shows six weeks, so the calendar never changes height.

  • Header: previous year, previous month, the month label, next month, next year.
  • Weekday row, starting on Sunday.
  • Six weeks of days, including days from the months either side.

Day states

default
today
selected
selected + today
other month
out of bounds
keyboard focus
Today keeps its dot when it’s selected, so the grid always shows which day is today.

Accessibility

The grid is one tab stop, on the chosen day or today. The month label is announced each time it changes.

KeyWhat it does
TabMoves through the header buttons, then onto one day in the grid.
Arrow keysMove a day or a week, into the next or previous month.
Home / EndMove to the start or end of the week.
Page Up / Page DownMove a month. With Shift, a year.
Enter / SpacePicks the focused day.

Props

PropTypeDefaultDescription
value / defaultValueDate | nullnullThe chosen day, controlled or uncontrolled.
onChange(day: Date) => void—Fires with the picked day.
minDate / maxDateDate | string | nullnullThe first and last days that can be picked.
autoFocusbooleanfalseFocuses the grid on mount.
footerReactNode—Actions under the grid, inside the surface.

Tokens

TokenValueWhere it lands
--global-blue-1000#1877F2The selected day. White on it is 4.23:1.
--global-blue-1100#0C296ASelected day on hover (white 13.63:1), and today’s number, 12.75:1 on its tint.
--global-blue-1000#1877F2The focus ring.
--global-blue-100#EBFAFFToday’s ground.
--global-blue-1000#1877F2Today’s dot. Blue-1000 is only 3.96:1 on the tint, so the number takes blue-1100.
--global-neutral-grey-200 / -1200#F5F5F5 / #1D1C20A hovered day and its number.
--global-neutral-grey-1100#2D2B32An ordinary day.
--global-neutral-grey-700#C9C9CCDays from the months either side.
(no token)#4F4D55Weekday headers.
--font-size-sm12pxDay numbers.
--font-weight-medium475Day numbers.
--radius-lg / -md8px / 6pxDay cells; header buttons.
--shadow-lg0 10px 15px -3px …The surface.

Month day picker

Picks a month and day for a date that repeats every year.

Refined portVue source: WildMonthDayPicker.vueStorybook story: Forms/Month Day PickerReact component: WMonthDayPicker

Try it

Start on 31 January, jump to April, then back. Compare the two overflow rules.

Stored day 31 · shows January 31
Jump to
Overflow
Year
Size
Error
Disabled
<WMonthDayPicker
  value={repeatsOn}
  onChange={setRepeatsOn}
  year={2026}
  overflow="clamp"
  size="sm"
  ariaLabel="Repeats on"
/>

When to use

  • Use it for a date that repeats every year, where the stored value carries no year.
  • Use hold when the chosen day must survive a change of month, such as the 31st.
  • Use clamp when the date must always exist in the month shown.
Don’t use it

Overflow

When a month is shorter than the chosen day, clamp moves the day to the month’s last day. hold keeps the stored day, shows the last day, and notes the held day beside the field.

clamp
hold
<WMonthDayPicker value={repeatsOn} onChange={setRepeatsOn} overflow="clamp" />
<WMonthDayPicker value={repeatsOn} onChange={setRepeatsOn} overflow="hold" />
Choose April, then go back to January. Clamp ends on the 30th; hold returns to the 31st.
Day 29 is held. February shows 28.
2026
2028
February’s length comes from year. The 29th shows only in a leap year.

Sizes and states

Two heights, 36 and 44px, to match the other fields. Error and disabled always apply to both selects.

Repeats on
Every year, on this date.
lg · 44
error
disabled
<WFormRow label="Repeats on" help="Every year, on this date.">
  <WMonthDayPicker value={repeatsOn} onChange={setRepeatsOn} width="100%" ariaLabel="Repeats on" />
</WFormRow>

Accessibility

The selects are announced as Month and Day. Pass ariaLabel to name the pair, usually with the form row’s label. A held day is announced as it appears.

KeyWhat it does
TabMoves focus to the month, then the day, closing any open list.
↓ / ↑Opens the list, then moves through it.
Enter / SpaceOpens or closes the list, or chooses the highlighted option.
EscapeCloses the list and keeps focus on the select.

Props

PropTypeDefaultDescription
valueMonthDay—The stored { month, day }. Month runs 1 to 12.
defaultValueMonthDayJanuary 31Starting value when value isn’t set.
onChange(value) => void—Called with the new month and day.
yearnumbercurrent yearSets February’s length.
overflow'clamp' | 'hold''clamp'What a too-short month does to the day.
size'sm' | 'lg''sm'36 or 44px.
widthnumber | string268Total width. The day select stays 88px, 96px at lg.
disabledbooleanfalseDisables both selects.
errorbooleanfalseError border on both selects.
ariaLabelstring—Accessible name for the pair.

Tokens

TokenValueWhere it lands
--radius-lg8pxBoth frames, and the open list.
--global-neutral-grey-600#DCDCDEBorder at rest.
--global-neutral-grey-800#ADACB0Border on hover, disabled chevron.
--global-blue-1000#1877F2Focus edge, halo and keyboard row.
--global-red-800#E12121Error edge.
--global-neutral-grey-300#F1F1F1Hovered and chosen rows, disabled ground.
--global-neutral-grey-500#E6E6E6Open list border, disabled border.
--global-neutral-grey-1100#2D2B32Chosen month and day, hovered or open chevron.
--global-neutral-grey-900#7F7D83Chevron at rest, disabled value.
--global-neutral-grey-1200#1D1C20Option text.
--global-blue-1100#0C296AThe chosen option’s text.
--global-neutral-grey-1000#4F4D55Placeholders and the held-day note.
--shadow-dropdown0 4px 12px rgba(0,0,0,.08), …The open list.
--spacing-28pxGap between month and day.

File upload

A drop zone that is the whole button, and a file list that reports every file in words.

Refined portVue source: WildFileUpload.vueStorybook story: Forms/File UploadReact component: WFileDrop

Try it

Drop files on it, click it, or add the samples. The site survey fails once, so you can retry it.

EvidenceOptional
Drop files here or browsePDF, CSV, JPG and PNG, up to 10 MB, 10 files at most.Or focus here and paste a screenshot
Nothing to hand?
Variant
Error
Disabled
<WFileDrop
  items={files}
  label="Evidence"
  hint="Optional"
  accept="image/*,.pdf,.csv"
  formats={['PDF', 'CSV', 'JPG', 'PNG']}
  maxSize={10 * 1024 * 1024}
  onAdd={upload}
  onRetry={upload}
  onCancel={cancel}
  onRemove={remove}
  onClear={clearAll}
/>

When to use

  • Attach files to a form or record: reports, spreadsheets, photos.
  • Take several files at once, each with its own progress and outcome.
  • Use the compact variant when the upload is one field among several.
Don’t use it
  • For words someone could type, such as a note. Use Textarea.
  • For picking something already in the system. Use Search select.

Anatomy

The drop zone, then the file list: a summary header with Clear all, and one row per file.

Drop files here or browsePDF, CSV, JPG and PNG, up to 10 MB, 10 files at most.Or focus here and paste a screenshot
6 files · 8.1 MB1 uploaded · 2 need attention
  • north-pump-service-report.pdf1.2 MBUploaded
  • tank-levels-2026.csv640 KBUploading · 62% · 4 s left
  • logger-photo-emu-park.jpg2.4 MBChecking the file
  • reseller-agreement.pdf820 KBWaiting
  • site-survey-bore-12.pdf3.1 MBFailed: connection lost
  • firmware-backup.zip14 MBPDF, CSV, JPG and PNG only
Each row shows the action its state needs: Cancel while it moves, Retry when it fails, Remove once it lands or is turned away.

Drag states

Drop files here or browsePDF and CSV, up to 10 MB, 10 files at most.
rest
Release to add 3 filesPDF and CSV, up to 10 MB, 10 files at most.
valid drag
PDF and CSV onlyPDF and CSV, up to 10 MB, 10 files at most.
wrong kind of file
The zone judges the kind of file before the drop, in colour and words together. Size and count are checked once the files land.

Compact

One row in place of the full zone. It keeps the drag states, paste and the list.

Acknowledgement note
Replaced the pressure sensor at Bore 12. Readings back to normal.
Photos
Attach photos or browseJPG, PNG and HEIC, up to 10 MB, 10 files at most.
1 file · 2.1 MB1 uploaded
  • bore-12-new-sensor.jpg2.1 MBUploaded

Content

  • Make the title a short instruction: “Attach photos”, “Drop files here”.
  • List formats in formats as people know them. Rejections are built from it: “PDF, CSV and JPG only”.
  • Write the field error as what’s missing: “Attach at least one photo of the installed logger.”

Accessibility

The zone is a button named by its label and title, described by its formats and any error, and it announces each drag message. Row actions name their file, such as “Retry site-survey-bore-12.pdf”.

KeyWhat it does
TabMoves from the zone to Clear all, then each row’s actions.
Enter / SpaceOn the zone, opens the file picker. On a row action, runs it.
Ctrl / ⌘ + VOn the zone, attaches a copied image.

Props

PropTypeDefaultDescription
itemsDropItem[]—The files and their state, from queued to done or rejected.
acceptstring—MIME types and extensions, as for a file input.
formatsstring[]—Short names shown as tags beside the size limit.
maxSizenumber10 MBPer file. Larger files are rejected with the reason.
maxFilesnumber10Most files the list holds. Extras are rejected.
multiplebooleantrueAllow more than one file per pick or drop.
variant'default' | 'compact''default'A full zone, or one row for a form.
disabledbooleanfalseBlocks picking, dropping and pasting.
titlestring'Drop files here'The zone’s instruction, and part of its name.
label / hint / help / errorstring—Field label, hint, help line and error message.
pastebooleantrueAttach a copied image by pasting on the focused zone.
onAdd(accepted, rejected) => void—Files that passed validation, and those that didn’t, with reasons.
onRetry / onCancel / onRemove(id) => void—The row actions.
onClear() => void—Clear all, shown once nothing is moving.

Tokens

TokenValueWhere it lands
--global-neutral-grey-600#DCDCDEResting dashed border.
--global-neutral-grey-800#ADACB0Border on hover.
--global-blue-1000#1877F2Focus and valid-drag edge, badge, progress.
--global-blue-100#EBFAFFValid drag ground.
--color-error#E12121Error and wrong-kind edge, badge.
--global-red-100#FEEBEBWrong-kind ground, rejected row.
--radius-xl12pxZone and list card.
--radius-lg8pxThumbnails and type tiles.

Inline edit title

Lets a heading be renamed in place, without opening a form.

Live React portVue source: WildInlineEditTitle.vueStorybook story: Forms/InlineEditTitle

Try it

Click the name and type. Enter finishes, Escape puts the old name back.

Size
Hide icon on focus
Empty
<WildInlineEditTitle
  v-model="alarm.name"
  size="md"
  placeholder="Untitled alarm"
  @submit="save"
/>

When to use

  • Use it for the name of the thing a dialog or screen edits, such as an alarm’s name in its dialog header.
  • Save on @submit or with the screen’s own Save button. The bound name changes on every keystroke, so don’t save on each change.
Don’t use it
  • For a field among other fields. Use Text input.
  • For a name that needs a label, hint or error message. Use a text input inside a Form field.

Sizes

Match the heading it replaces. The default is md.

sm · 14px / 600
md · 16px / 600
lg · 18px / 700
<WildInlineEditTitle v-model="name" size="sm" />
<WildInlineEditTitle v-model="name" size="md" />
<WildInlineEditTitle v-model="name" size="lg" />

States

It reads as a heading until it’s touched. Hover lays a grey ground, and focus draws the field’s blue edge.

rest
hover
focus
empty
The frame is always there, clear at rest, so the name never moves when it takes focus.

Content

  • Make the placeholder name the thing being named: Untitled alarm.
  • Keep names to one line. A longer name ends in an ellipsis at rest and scrolls while it’s edited.
  • Set maxLength when the name has a limit, so typing stops there.

Accessibility

A title has no visible label, so give it aria-label. Without one, screen readers fall back to the placeholder.

KeyWhat it does
TabMoves focus into the name.
EnterLeaves the field and submits the name.
EscapeRestores the name from before editing and leaves the field. A dialog around it stays open.

Props

PropTypeDefaultDescription
modelValuestring—The name (v-model).
placeholderstring'Untitled'Shown when the name is empty.
size'sm' | 'md' | 'lg''md'Type size: 14, 16 or 18px.
iconstring'pencil-solid'Leading icon name.
hideIconOnFocusbooleantrueFades the icon out while editing.
maxLengthnumber—Maximum characters.
aria-labelstring—Accessible name. Falls back to the placeholder.
@submit() => void—Enter pressed. The field has already blurred.
@cancel() => void—Escape pressed. The name is already restored.
@blur(e: FocusEvent) => void—Focus left the field. Saves nothing.

Tokens

TokenValueWhere it lands
--global-neutral-grey-1100#2D2B32The name.
--global-neutral-grey-1000#4F4D55Placeholder, at weight 400.
--global-neutral-grey-900#7F7D83The pencil.
--global-neutral-grey-200#F5F5F5Hover ground.
--global-base-white#FFFFFFFocus ground.
--global-blue-1000#1877F2Focus edge, halo and caret.
--global-blue-300#BDEDFFSelected text.
--radius-lg8pxHover and focus frame corners.

Radio group

Several radios bound to one value, with one label, hint and error for the whole group.

Refined portVue source: WildRadioGroup.vueStorybook story: Forms/Radio GroupReact component: WRadioGroup

Try it

Tab to the group, then use the arrow keys to change the choice.

Report readingsApplies to 12 loggers
Every 5 minutes288 readings a day
Every 15 minutes96 readings a day
Hourly24 readings a day
DailyOne reading a day
Shorter intervals use more battery.
Layout
Style
Size
Sub-lines
Error
Disabled
<WRadioGroup
  value={reportEvery}
  onChange={setReportEvery}
  label="Report readings"
  hint="Applies to 12 loggers"
  help="Shorter intervals use more battery."
  size="sm"
  options={[
    { id: '5m', label: 'Every 5 minutes', sub: '288 readings a day' },
    { id: '15m', label: 'Every 15 minutes', sub: '96 readings a day' },
    { id: '1h', label: 'Hourly', sub: '24 readings a day' },
    { id: '1d', label: 'Daily', sub: 'One reading a day' },
  ]}
/>

When to use

  • Use a radio group when people pick exactly one of two to six options.
  • Use it when every option should stay visible, so they can be compared.
  • Use option cards when each choice needs a line of explanation.
Don’t use it
  • For more than six options. Use a Select.
  • For several choices at once. Use Checkbox.
  • For an on or off setting. Use a Toggle.
  • To switch views. Use Segmented.

Anatomy

A label above, an optional hint on the right, the options, then help or an error below.

Pressure unitShown on every chart
kPa
bar
psi
Changes how pressure readings are shown.

Layouts

Stack options that are sentences. Use a row only for three or four short words.

kPa
bar
psi
vertical
kPa
bar
psi
horizontal

Option cards

When each choice needs a line of explanation, make the option a card.

Pressure unit
kPaKilopascals
barBar, equal to 100 kPa
psiPounds per square inch
Selected adds a blue tint and ring. The edge stays 1px in every state, so choosing never shifts the layout.

States

kPa
bar
psi
rest
kPa
bar
psi
Choose a unit.
error
kPa
bar
psi
one option disabled
kPa
bar
psi
group disabled
Error turns every enabled ring red and leaves the labels dark. Write the message as what to do: “Choose a unit.”

Accessibility

The group is one tab stop, and a screen reader hears its label as focus enters. An error is announced when it appears. Give a group with no visible label an ariaLabel.

KeyWhat it does
TabMoves to the chosen option, or the first enabled one.
↓ / →Chooses the next option, skipping disabled ones and wrapping.
↑ / ←Chooses the previous option.
Home / EndChooses the first or last option.
Space / EnterChooses the focused option.

Props

PropTypeDefaultDescription
valuestring—The chosen option’s id.
defaultValuestring—Starting choice when value isn’t set.
onChange(id: string) => void—Called with the newly chosen id.
options{ id, label, sub?, disabled? }[]—The choices, in order.
labelstring—The group’s visible name.
hintstring—Short context, right of the label.
helpstring—Guidance under the options.
errorboolean | stringfalseA string replaces the help as the message.
orientation'vertical' | 'horizontal''vertical'Stack or row.
variant'plain' | 'card''plain'Card adds an outline, a selected tint and padding.
size'sm' | 'lg''sm'Radio diameter: 18 or 24px.
disabledbooleanfalseDisables every option.
namestring—Posts the value with a form.
ariaLabelstring—Accessible name when there’s no visible label.

Tokens

TokenValueWhere it lands
--global-blue-1000#1877F2Chosen radio, unchosen hover, focus ring.
--global-blue-1100#0C296AChosen hover, unchosen press.
--global-blue-1200#081B45Chosen press.
--global-neutral-grey-900#7F7D83Resting radio ring.
--global-neutral-grey-700#C9C9CCDisabled ring, card outline on hover.
--global-neutral-grey-500#E6E6E6Resting card outline.
--global-blue-300#BDEDFFRing around a chosen card.
--global-blue-100#EBFAFFGround of a chosen card.
--global-red-800#E12121Error ring and message.
--global-red-300#FFC9C9Card outline in the error state.
--global-neutral-grey-1300#0A090BOption labels.
--global-neutral-grey-1100#2D2B32Sub-lines.
--global-neutral-grey-1000#4F4D55Group label, hint and help.
--radius-lg8pxCard corners.

Text input

The single-line field every form is built from, with its own label, message and counter, in four content shapes.

Refined portVue source: WildTextInput.vueReact component: WTextInput

Try it

Type, tab and paste into it. Paste a full address into website and the https:// comes off.

Use a name your team will recognise.
Type
Size
Icon
Clear button
Counter
Error
Read-only
Disabled
<WildTextInput
  v-model="siteName"
  size="sm"
  label="Site name"
  help="Use a name your team will recognise."
  clearable
>
  <template #leading>
    <MapPinIcon />
  </template>
</WildTextInput>

When to use

  • Use it for one line of free text.
  • Use a type for websites, amounts and counts.
Don’t use it

Anatomy

A label, the frame and a message line. Inside the frame: an optional icon, the value and a clear button once there’s a value.

Keep it short
Use a name your team will recognise.
Up to 40 characters.

Types

Four content shapes, each with a plain model.

Use a name your team will recognise.
Paste a full address or just the domain.
Dollars, with or without cents.
Between 1 and 50.
Website drops a pasted protocol. Currency stores 1250 and shows 1,250.00 once you leave. Quantity is a spinbutton with steppers.

Icon, clear and counter

Three opt-in additions. The icon is for the basic type only.

Escape clears it too.
Up to 40 characters.
Up to 12 characters.
Typing past the limit is allowed. The count turns red and the field is invalid until it fits, so a paste is never cut short.

Sizes

Use sm, at 36px, by default. Use lg, at 44px, where a field needs a bigger touch target.

sm · 36
lg · 44

States

empty
filled
hover
focus
error
error, focused
read-only
disabled
An invalid field ignores hover. Focused, it keeps the red edge and adds the focus ring outside it. Read-only stays in the tab order.

In context

Press Add site with the name empty. The error shows and focus moves to the field.

Add a site
Only the name is required.
Up to 40 characters.
Optional
Add it now or later.

Inside a form field

Where a Form field already gives the label, help and error, use the bare box, WInput. It takes the same sizes, types and states.

Site name*
e.g. Bore 12
<WildFormField label="Site name" required>
  <WildTextInput placeholder="e.g. Bore 12" />
</WildFormField>

Content

  • Write labels as short nouns: Site name, Monthly budget.
  • Use the placeholder for an example value, never as the label.
  • Write the error as the fix: Enter a site name.

Accessibility

Help, error and limit are read with the input. The count is announced when typing pauses.

KeyWhat it does
TabInto the field, then to the clear button.
EscapeClears a clearable field.
↑ / ↓Quantity: one step.
Page Up / Page DownQuantity: ten steps.
Home / EndQuantity: minimum or maximum.

Props

PropTypeDefaultDescription
v-modelstring—The value, as a plain string.
type'basic' | 'website' | 'currency' | 'quantity''basic'See Types.
size'sm' | 'lg''sm'36px or 44px.
labelstring—The visible name.
hintstring—Short context, right of the label.
helpstring—Guidance under the field.
errorboolean | stringfalseA string is also the message.
requiredbooleanfalseAsterisk on the label, and aria-required.
placeholderstring—An example value.
aria-labelstring—Names a field with no visible label.
readonlybooleanfalseSelectable but not editable.
disabledbooleanfalseFlat grey, out of the tab order.
clearablebooleanfalseClear button once filled.
maxlengthnumber—A soft limit with a counter.
suffixstring—Trailing unit or currency code.
website-prefix / currency-symbolstring'https://' / '$'Website and currency prefixes.
min / max / stepnumber0 / — / 1Quantity bounds and step.
#leadingslot—Icon ahead of the text.
@clear() => void—After a clear.

Tokens

TokenValueWhere it lands
--global-neutral-grey-600#DCDCDEResting border, 1px.
--global-neutral-grey-800#ADACB0Hover border.
--global-blue-1000#1877F2Focus edge and halo, caret.
--color-error#E12121Error edge, message, over-limit count.
--global-neutral-grey-1100#2D2B32Value text.
--global-neutral-grey-900#7F7D83Icon, clear ×, disabled text.
--global-neutral-grey-1000#4F4D55Label, placeholder, helper, units, counter.
--global-neutral-grey-200#F5F5F5Disabled ground, lighter than hover.
--global-neutral-grey-500#E6E6E6Disabled and read-only border.
--radius-lg8pxThe frame.

Input autocomplete

A text field that suggests matches from a list as you type, and can also take free text.

Refined portVue source: WildInputAutoComplete.vueReact component: WInputAutocomplete

Try it

Type “pu” and press Enter, then try “ohau”, the arrow keys and Escape.

16 on this account
Choose where this alarm watches.
v-model""@select—
Size
Min characters
Row icons
Sub-lines
Free text
Error
Disabled
<WildInputAutoComplete
  v-model="query"
  :options="sites"
  label="Site or logger"
  hint="16 on this account"
  placeholder="Search sites and loggers"
  help="Choose where this alarm watches."
  size="sm"
  :max-results="6"
  clearable
  @select="watch"
>
  <template #leading>
    <MagnifyingGlassIcon />
  </template>
</WildInputAutoComplete>

When to use

  • Use it to pick one site, logger or person from a long list.
  • Turn on allow-custom when the answer may be outside the list.
  • Turn on clear-on-select to add several in a row.
Don’t use it

Anatomy

The text input’s frame, with a list 4px below. Rows hold a name, optional icon and sub-line, and a tick on the choice.

16 on this account
Pump Shed GatewayGateway · 14 loggers
Pukekohe PackhouseCool store · air temperature
North PumpEmu Park · pressure and flow
South PumpEmu Park · pressure
The typed part is bold, so each row shows why it matched.

Matching

Case, extra spaces and macrons are ignored. Start-of-name matches rank first, then start-of-word, then anywhere.

ranked by where it matches
Hokio Beach BoreHorowhenua · groundwater level
Tank 1 — HomesteadLevel · 22,000 L
Pukekohe PackhouseCool store · air temperature
macrons folded
Ōhau Channel IntakeConsent PW-2231 · flow
“ho” ranks Hokio Beach Bore first and Pukekohe Packhouse last.

Rows

Add an avatar and sub-line when names alone could be confused.

label only · 34px
Kiwifruit — Gold
Kiwifruit — Green
Kiwifruit — Red
Kiwiberry
avatar and sub-line · 44px
Ana Tupouana@emupark.co.nz
Ben HartleyReseller · Hartley Irrigation
Grace Liugrace@emupark.co.nz
Hemi ParataOn call this week · SMS
Showing 4 of 6. Keep typing to narrow.

Free text

With allow-custom, the typed text is the last row. Enter still takes the first match.

matches, then your text
Grace Liugrace@emupark.co.nz
Add “gr”
only your text
Add “sam@emupark.co.nz”

States

rest
hover
focus
error
error, focused
disabled
Focus adds a blue inner edge and halo. Error adds a red edge.
nothing matches
No matches for “bore 99”
reopened on a choice
Bore 12Emu Park · groundwater level
Bore 4River Flat · groundwater level
Bore 9Decommissioned in March
North PumpEmu Park · pressure and flow
South PumpEmu Park · pressure
Hokio Beach BoreHorowhenua · groundwater level
Pump Shed GatewayGateway · 14 loggers
Pukekohe PackhouseCool store · air temperature
Tank 1 — HomesteadLevel · 22,000 L
Tank 3 — Top PaddockLevel · 30,000 L
Soil Probe AKiwifruit Block 2 · soil moisture
Soil Probe BKiwifruit Block 3 · soil moisture
Weather Station 4Emu Park · rain, wind, temperature
Frost Sensor 2Avocado Block 1 · air temperature
Ōhau Channel IntakeConsent PW-2231 · flow
Effluent PondDairy shed · level

In context

Alarm recipients
Soil moisture below 18% · North Pump
People or email addresses
HHemi ParataAAna Tupouops@emupark.co.nz
Message
WildEye: soil moisture at North Pump is 16.2% (below 18%) as of 05:40.
3 recipients.
Each pick empties the field and keeps focus.

Content

  • Label it with what it holds: Site or logger, Recipients.
  • Write the placeholder as what to search: Search sites and loggers.
  • Make custom-label a verb: Use, Add.

Accessibility

Focus stays in the field. Screen readers hear the highlighted row and count.

KeyWhat it does
TypingFilters and highlights the first match.
↓ / ↑Opens the list, or moves the highlight and skips unavailable rows.
SpaceTypes a space. It never picks a row.
EnterTakes the highlighted row, or typed text with allow-custom.
EscapeCloses the list, then clears a clearable field.
TabLeaves without choosing.

Props

PropTypeDefaultDescription
v-modelstring—The query text.
options{ key, value, sub?, leading?, disabled? }[]—Suggestions; disabled ones show but are skipped.
@select(item: KeyValue | string)—The chosen suggestion, or typed text.
labelstring—Visible name.
aria-labelstring—Name when there is no visible label.
hintstring—Context beside the label.
helpstring—Guidance under the field.
errorboolean | stringfalseA string shows as the message.
placeholderstring'Search…'Empty-field text.
min-charsnumber1Characters before the list opens.
max-resultsnumber8Rows before the “keep typing” line.
allow-custombooleanfalseOffer the typed text as a choice.
custom-labelstring'Use'Verb on the free-text row.
clear-on-selectbooleanfalseEmpty the field after each pick.
auto-highlightbooleantrueHighlight the first match.
no-match-textstring'No matches'Said when nothing matches.
clearablebooleanfalseClear button once there is text.
size'sm' | 'lg''sm'36px or 44px.
requiredbooleanfalseMarks the label, sets aria-required.
disabledbooleanfalseFlat grey, takes no input.
v-model:openboolean—Controlled open state.
#leadingslot—Icon ahead of the text.

Tokens

TokenValueWhere it lands
--global-neutral-grey-600#DCDCDEResting border, 1px.
--global-blue-1000#1877F2Focus edge, halo, tick, add mark.
--color-error#E12121Error edge and message.
--global-neutral-grey-500#E6E6E6List edge, disabled border.
--global-neutral-grey-300#F1F1F1Highlighted row.
--global-neutral-grey-200#F5F5F5Disabled ground.
--global-neutral-grey-1000#4F4D55Placeholder and secondary lines.
--shadow-dropdown0 4px 12px rgba(0,0,0,.08), 0 2px 4px rgba(0,0,0,.04)The list surface.
--radius-lg8pxFrame and list corners.
--font-weight-bold600The matched part.

Select buttons

One button that opens a short menu of choices, and skips the menu when there is only one.

Refined portVue source: WildSelectButtons.vueReact component: WSelectButtons, WSelectButtonsPicture

Try it

Open the menu and choose an alarm type.

Nothing added to Bore 12 yet.
Variant
Size
Drop up
Align
Rows
One choice
Disabled
<WildSelectButtons
  label="Add alarm"
  :options="[
    { Id: 'high', Name: 'High level', icon: 'arrow-up',
      description: 'Reading rises above a threshold' },
    { Id: 'low', Name: 'Low level', icon: 'arrow-down',
      description: 'Reading falls below a threshold' },
    { Id: 'rate', Name: 'Rate of change', icon: 'arrow-trending-up',
      description: 'Level moves faster than a set rate' },
    { Id: 'nodata', Name: 'No data', icon: 'clock',
      description: 'Logger misses its reporting window' },
    { Id: 'battery', Name: 'Low battery', icon: 'battery-0',
      description: 'Supply drops under 3.4 V' },
  ]"
  @button-clicked="addAlarm"
/>

When to use

  • Use it for one action whose object comes in kinds: Add alarm, Export.
  • Use it where a row of buttons won’t fit.
Don’t use it
  • To pick a value that stays chosen. Use Select.
  • For different actions on one row. Use Menu.

Anatomy

A button, a chevron and a menu of choices.

Button
The shipped button, with an optional leading icon.
Chevron
Turns over while the menu is open. Gone with one choice.
Heading
Optional. It also names the menu for screen readers.
Choice
Icon, label and one line of description. The whole row is the target.

Variants

primary
secondary
borders
<WildSelectButtons buttonClass="btn-borders btn-md" … />
Use primary for the page’s main action, and borders in card headers and toolbars.

Sizes

xs · 32
sm · 36
md · 40
lg · 44
<WildSelectButtons buttonClass="btn-borders btn-sm" … />

Choices

Use labels when words are enough, icons for kinds of thing, and descriptions to tell labels apart.

labels
icons
descriptions
One description puts every row on two lines, so the icons line up.

Placement

The menu opens above by default, or below, at the button’s end or start.

drop up · end
drop up · start
drop down · end
drop down · start
<WildSelectButtons :dropUp="false" align="start" … />
The menu flips when it doesn’t fit and the other side has more room, and scrolls when neither side fits.

One choice

With one choice, the button does it straight away.

Opens a menu of types.
bore 12 · five alarm types
Adds it straight away.
rain gauge 4 · one alarm type
<!-- five types: a menu -->
<WildSelectButtons label="Add alarm" :options="boreTypes" @button-clicked="addAlarm" />

<!-- one type: no menu, no chevron -->
<WildSelectButtons label="Add rainfall alarm" :options="[rainfall]" @button-clicked="addAlarm" />
Name the choice on the button. The event is the same, so calling code doesn’t change.

States

While the menu is open, the button keeps its hover fill.

rest
hover
focus
disabled
pointer on a choice
keys on a disabled choice
A disabled choice stays on the key path, so its description can say why.

In context

Choose an alarm type and it joins the card’s list.

AlarmsBore 12 · Emu Park
  • High levelAbove 180 kPa · SMS to 2 peopleArmed
  • No dataNo reading for 2 hours · EmailArmed
  • Low batteryUnder 3.4 V · EmailPaused
Turn dropUp off in a card header, so the menu drops over the card’s list.

Content

  • Start with a verb and name the object: Add alarm.
  • With one choice, name it: Add rainfall alarm.
  • Keep descriptions to one line, or they’re cut off.

Accessibility

KeyWhat it does
Enter / SpaceOpens the menu, or does the one choice. In the menu, chooses.
↓ / ↑Opens the menu on the first or last choice. In the menu, moves, wrapping.
Home / EndMoves to the first or last choice.
A letterMoves to the next choice that starts with it.
EscapeCloses the menu and returns to the button.
TabCloses the menu and moves on.

Props

Rows from options[].description down are refined additions.

PropTypeDefaultDescription
labelstring'Add new alarm'Button text.
optionsSelectOption[]—Choices, in order, each { Id, Name }.
@button-clicked(option: SelectOption) => void—Fires with the choice.
align'start' | 'end''end'Button edge the menu aligns to.
dropUpbooleantrueOpens above. Flips when it doesn’t fit.
autoSelectSinglebooleantrueOne option: choose it, no menu.
buttonClassstring'btn-primary btn-medium'Button variant and size classes.
triggerIconstring | nullnullIcon name on the button.
triggerIconOpenstring | nullnullIcon while the menu is open.
triggerIconPosition'left' | 'right''right'Icon side of the label.
options[].descriptionstring—One line under the label.
options[].iconstring—Icon name before the label.
options[].disabledbooleanfalseDims the choice. Keys still reach it.
headingstring—Menu heading, and its accessible name.
chevronbooleantrueTrailing chevron that turns over while open.
disabledbooleanfalseDisables the button.

Tokens

TokenValueWhere it lands
--color-primary-hover#0ABEFFPrimary fill while the menu is open.
--global-blue-1000#1877F2Ring on a row the keys reach.
--global-neutral-grey-500#E6E6E6Menu outline and icon tile ring.
--global-neutral-grey-300#F1F1F1Highlighted row.
--global-neutral-grey-400#ECECEDPressed row.
--global-neutral-grey-1000#4F4D55Descriptions and the heading.
--shadow-dropdown0 4px 12px rgba(0,0,0,.08), …Menu elevation.

Date time picker

A date and a time chosen together, on the site’s clock, built for scheduling rather than for filtering.

Refined portVue source: WildDateTimePicker.vueReact component: WDateTimePicker

Try it

Pick a day, then type or pick a time.

Start irrigationNorth Pump
NZDT
In the site’s time. Runs for 2 hours.
Means 2026-09-28T05:30+13:00
Clock
Size
Min / max
Zone label
Error
Disabled
<WildDateTimePicker
  v-model:date="start.date"
  v-model:time="start.time"
  label="Start irrigation"
  hint="North Pump"
  help="In the site’s time. Runs for 2 hours."
  time-zone="Pacific/Auckland"
  size="sm"
  required
/>
The list steps every 15 minutes; any minute can be typed.

When to use

  • Use it to schedule a day and time, such as an irrigation run.
  • Use it when the time follows the site’s clock through daylight saving.
Don’t use it

Anatomy

Two fields, one label, one message.

Start irrigationNorth Pump
NZDT
In the site’s time.
closed
September 2026
Mo
Tu
We
Th
Fr
Sa
Su
date, open
00:00
00:15
00:30
00:45
01:00
01:15
01:30
01:45
02:00
02:15
02:30
02:45
03:00
03:15
03:30
03:45
04:00
04:15
04:30
04:45
05:00
05:15
05:30
05:45
06:00
06:15
06:30
06:45
07:00
07:15
07:30
07:45
08:00
08:15
08:30
08:45
09:00
09:15
09:30
09:45
10:00
10:15
10:30
10:45
11:00
11:15
11:30
11:45
12:00
12:15
12:30
12:45
13:00
13:15
13:30
13:45
14:00
14:15
14:30
14:45
15:00
15:15
15:30
15:45
16:00
16:15
16:30
16:45
17:00
17:15
17:30
17:45
18:00
18:15
18:30
18:45
19:00
19:15
19:30
19:45
20:00
20:15
20:30
20:45
21:00
21:15
21:30
21:45
22:00
22:15
22:30
22:45
23:00
23:15
23:30
23:45
time, open

Typed times

How typed times are read.

Typed24-hour12-hour
53005:305:30 am
5:3005:305:30 am
5.30pm17:305:30 pm
1717:005:00 pm
12.15 p.m.12:1512:15 pm
25:00Not a timeNot a time

Unreadable text stays in the field, with a prompt for a time like 05:30.

Time zones

The zone label is the site’s, for the day and time chosen.

Start irrigation
NZST
means 2026-09-26T05:30+12:00
Start irrigation
NZDT
means 2026-09-28T05:30+13:00
05:30 either side of the clocks going forward, 27 September 2026.
Start irrigation
NZDT
02:30 is skipped on Sun 27 Sep 2026: the clocks go from 02:00 straight to 03:00.
typed 02:30 on 27 Sep
00:00
00:15
00:30
00:45
01:00
01:15
01:30
01:45
02:00Skipped
02:15Skipped
02:30Skipped
02:45Skipped
03:00
03:15
03:30
03:45
04:00
04:15
04:30
04:45
05:00
05:15
05:30
05:45
06:00
06:15
06:30
06:45
07:00
07:15
07:30
07:45
08:00
08:15
08:30
08:45
09:00
09:15
09:30
09:45
10:00
10:15
10:30
10:45
11:00
11:15
11:30
11:45
12:00
12:15
12:30
12:45
13:00
13:15
13:30
13:45
14:00
14:15
14:30
14:45
15:00
15:15
15:30
15:45
16:00
16:15
16:30
16:45
17:00
17:15
17:30
17:45
18:00
18:15
18:30
18:45
19:00
19:15
19:30
19:45
20:00
20:15
20:30
20:45
21:00
21:15
21:30
21:45
22:00
22:15
22:30
22:45
23:00
23:15
23:30
23:45
the list that day
That Sunday the clocks skip 02:00 to 03:00, so those times can’t be chosen.

States

Shared field states and the two heights.

Start irrigation
NZDT
Choose when the run starts.
empty
Start irrigation
NZDT
In the site’s time.
filled
Start irrigation
NZST
Choose Mon 28 Sep 2026 or later.
day out of range
Start irrigation
NZDT
On Mon 28 Sep 2026 the earliest time is 05:00.
time out of range
Start irrigation
NZDT
North Pump is already running then.
error from outside
Start irrigation
NZDT
Set by the schedule.
disabled
An error reddens the half that’s wrong. One passed in reddens both.
Start irrigation
NZDT
sm · 36 · 24-hour
Start irrigation
NZDT
lg · 44 · 12-hour
The time half fits its longest value, so it never truncates.

In context

Scheduling a manual run, with its finish time.

Schedule a run
North Pump · Emu Park · zones 1 to 3
Start irrigation
NZDT
In the site’s time.
Run for
Runs Mon 28 Sep 2026, 05:30 to 07:30 NZDT.

Content

  • Name the event in the label: Start irrigation.
  • Say what clashes and what to do: North Pump is already running then. Choose another start.

Accessibility

A screen reader hears “Start irrigation date” and “Start irrigation time”, then the message.

KeyWhat it does
Enter / Space / ↓ / ↑Date field: opens the calendar.
Backspace / DeleteDate field: clears, unless required.
← → ↑ ↓Calendar: moves a day or a week.
Home / EndCalendar: start or end of the week.
Page Up / Page DownCalendar: a month, or a year with Shift. Time list: an hour.
Enter / SpaceCalendar: chooses the day.
↓ / ↑Time list: moves, skipping times that can’t be chosen.
Enter / TabTime field: keeps what the field says.
EscCloses the panel. Twice restores the saved time.

Props

PropTypeDefaultDescription
v-model:datestring''The day, YYYY-MM-DD.
v-model:timestring''The time, HH:mm, 24-hour.
labelstring—Shared by both halves.
hintstring—Context, right of the label.
helpstring—Guidance under the fields.
errorboolean | stringfalseReddens both halves; a string also shows.
requiredbooleanfalseMarks the label; removes Clear.
disabledbooleanfalseBoth halves.
size'sm' | 'lg''sm'36px or 44px tall.
hour-cycle12 | 2424How times show.
stepnumber15Minutes between listed times.
min / maxstring—A date, or a date and time.
time-zonestring'Pacific/Auckland'The site’s IANA zone.
zone-labelstring | falseworked outOverrides the label; false hides it.
week-starts-on0 | 111 is Monday, 0 Sunday.
todaystringsite’s todayPins today, YYYY-MM-DD.
widthnumber | string—Control width.
namestring—Posts YYYY-MM-DDTHH:mm, no offset.
aria-labelstring—Names the pair when there’s no label.

Tokens

TokenValueWhere it lands
--radius-lg8pxBoth fields.
--global-blue-1000#1877F2Focus edge, open field, chosen day.
--global-blue-100#EBFAFFToday’s tint.
--global-blue-1100#0C296AToday’s number; chosen day on hover.
--color-text-secondary#4F4D55Weekdays, hint, help, zone.
--color-error#E12121Error edge and message.
--shadow-dropdown0 4px 12px rgba(0,0,0,.08), …Both panels.

Date range selector

Two bounded dates chosen together, with the presets that cover most of what anyone actually asks for.

Refined portVue source: WildDateRangeSelector.vueReact component: WDateRangeSelector

Try it

Pick a preset or click two days, then Apply.

Reporting periodBore 12 · Emu Park
Includes today’s readings so far.
{ from: '2026-08-30', to: '2026-09-28', preset: 'last_30days' }
Size
Months
Presets
Max 31 days
Future days
Error
Disabled
<WDateRangeSelector
  value={period}
  onChange={setPeriod}
  label="Reporting period"
  hint="Bore 12 · Emu Park"
  size="sm"
  max={today}
/>

When to use

  • Use it to set a reporting or export period.
  • Keep the presets on for common periods.
Don’t use it

Anatomy

A field showing the period, and a panel of presets, months and footer.

a preset
a custom range
A preset shows its name; a custom range, its day count.
Last 7 days
Last 30 days
Last 60 days
Last 90 days
This month
Last month
Last 2 months
Last 3 months
Custom
August 2026
Mo
Tu
We
Th
Fr
Sa
Su
September 2026
Mo
Tu
We
Th
Fr
Sa
Su
30 Aug – 28 Sep 2026 · 30 days

Selection

Click a start, then an end; the band follows the pointer.

start chosen, end previewed
range set
one day
today
today in a range
hover
keyboard focus
unavailable
4 to 9 September
The band rounds off at each week and month end.

Presets

A preset keeps its id, so a saved report keeps rolling forward.

PresetCovers
Last 7, 30, 60 or 90 daysUp to and including today.
This monthThe 1st to today.
Last month, 2 or 3 monthsWhole months before this one.

Limits

maxDays caps the range; here, 31 days. Days and presets past it grey out.

Last 7 days
Last 30 days
Last 60 days
Last 90 days
This month
Last month
Last 2 months
Last 3 months
Custom
August 2026
Mo
Tu
We
Th
Fr
Sa
Su
September 2026
Mo
Tu
We
Th
Fr
Sa
Su
From 10 Sep 2026 · choose an end date, up to 31 days

Sizes and states

sm · preset
lg · custom range
empty
across a year
Locked while readings load.
disabled
Choose 31 days or fewer.
error

In context

View readings
Logger
Bore 12 · Emu Park
Period

Content

  • State a limit in the hint: “Up to 31 days”.
  • Say what to change in the error: “Choose 31 days or fewer.”

Accessibility

Days read their full date and place in the range.

KeyWhat it does
Enter / Space / ↑ / ↓On the field, opens the panel.
↑ / ↓Moves through presets; ← / → in a row.
EnterOn a preset, applies it; on Custom, moves to the days.
Enter / SpaceOn a day, sets the start, then the end.
Arrow keysOn the days, a day or a week.
Home / EndFirst or last preset; week start or end.
Page Up / DownA month; with Shift, a year.
TabStays in the panel.
EscapeCloses without changes.

Props

PropTypeDefaultDescription
value / defaultValue{ from, to, preset } | null{ preset: 'last_30days' }YYYY-MM-DD dates; a preset id wins.
onChange(range) => void—Fires when a range is applied.
presets{ id, label, range }[] | falsethe eightfalse hides the list.
todaystringtodayWhat presets count from.
min / maxstring—First and last usable days.
maxDaysnumber—Longest range, both ends counted.
months1 | 22Drops to one without room.
weekStartsOn0 | 111 is Monday, 0 is Sunday.
label / hint / helpstring—Name, side note, guidance below.
errorboolean | stringfalseA string shows below.
size'sm' | 'lg''sm'36 or 44px high.
align'start' | 'end''start'Field edge the panel aligns to.
placeholderstring'Choose a period'Shown when empty.
disabledbooleanfalseKeeps the field shut.
ariaLabelstring—Names the field without a label.
widthnumber | string300Field width.

Tokens

TokenValueWhere it lands
--global-blue-1000#1877F2Range ends and focus ring.
--global-blue-1100#0C296ABand ink, today, hovered ends.
--global-blue-100 / -200 / -300#EBFAFF / #D6F4FF / #BDEDFFPreviewed band; band and chosen preset; hover.
--global-neutral-grey-1100 / -1300#2D2B32 / #0A090BField value, days, presets; months, hover.
--global-neutral-grey-1000#4F4D55Placeholder, weekdays, counts, hint.
--global-neutral-grey-600#DCDCDEField border.
--global-red-800#E12121Field border on error.
--global-neutral-grey-700 / -900#C9C9CC / #7F7D83Unavailable days; inside the band.
--global-neutral-grey-300#F1F1F1Hover fills.
--global-neutral-grey-400 / -500#ECECED / #E6E6E6Dividers; the panel border.
--radius-lg / -xl8px / 12pxDay cells; the panel.
--shadow-dropdown0 4px 12px …The panel.