Feedback

Alert

Tells someone about something that needs their attention, in one of five tones.

Live React portVue source: WildAlert.vueStorybook story: Feedback/AlertReact component: WAlert

Try it

Switch the variant, then press the cross or tab to it.

Battery below 20%Logger 4412 will stop reporting in about six days.
Variant
Size
Title
Dismissible
<WildAlert
  variant="warning"
  size="md"
  title="Battery below 20%"
  dismissible
  @dismiss="hide"
>
  Logger 4412 will stop reporting in about six days.
</WildAlert>

When to use

  • Use an alert for a condition that affects the page or panel it sits in.
  • Use one per region, at the top of what it describes.
  • Let people dismiss it only once the message has done its job.
Don’t use it
  • To confirm an action that just finished. Use a toast.
  • For one field’s problem. Use an error message.
  • When there’s nothing to show yet. Use an empty state.

Variants

Five tones. Pick by meaning: warning and error ask for action; info, success and neutral only inform.

New schedule for logger 4412Readings now arrive every fifteen minutes.
Logger 4412 is reportingIts readings will appear here within fifteen minutes.
Battery below 20%Logger 4412 will stop reporting in about six days.
Sync failedThree sites didn’t report in the last window. Try again in a few minutes.
Maintenance on SundayReadings may arrive late between 2am and 4am.
<WildAlert variant="warning" title="Battery below 20%">
  Logger 4412 will stop reporting in about six days.
</WildAlert>
Readings arrive every fifteen minutes.
<WildAlert variant="info">Readings arrive every fifteen minutes.</WildAlert>
Drop the title in dense screens, where one line of consequence is enough.

Sizes

Three, with 13, 14 and 15px text. The icon and the cross scale with the padding.

Size smReadings arrive every fifteen minutes.
Size mdReadings arrive every fifteen minutes.
Size lgReadings arrive every fifteen minutes.
Use sm inside forms and panels, md for page banners and lg for a page with little else on it.

Content

  • Put the fact in the title: “Battery below 20%”.
  • Put the consequence or the next step in the body: “Logger 4412 will stop reporting in about six days.”
  • Keep the title to six words or fewer, in sentence case, with no full stop.
  • Name the logger or site, so nobody has to guess which one.

Accessibility

Error and warning are announced as they appear; the other tones wait their turn. The cross is named “Dismiss” plus the title, so a stack of alerts stays distinguishable.

KeyWhat it does
TabMoves focus to the cross, when the alert is dismissible.
Enter / SpaceDismisses the alert and moves focus to the next control.

Props

PropTypeDefaultDescription
variant'info' | 'success' | 'warning' | 'error' | 'neutral''info'The tone. See Variants.
size'sm' | 'md' | 'lg''md'Padding and text size: 13, 14 or 15px.
titlestring—Bold first line. Leave out for a single line.
iconstring | booleantrueReplaces the status icon by name; false hides it.
dismissiblebooleanfalseShows the cross.
@dismiss() => void—Fires when the cross is pressed. Hide the alert here.

Tokens

TokenValueWhere it lands
--radius-lg8pxCorners.
--global-blue-200#D6F4FFInfo ground.
--global-green-200#E1FAEASuccess ground.
--global-squash-200#FFF1DAWarning ground.
--global-red-200#FFE3E3Error ground.
--global-neutral-grey-200#F5F5F5Neutral ground.
--global-*-300grey-500 for neutralBorder.
--global-*-400 / 500grey-600 / 700 for neutralThe cross’s hover and pressed circle.
--global-*-1000 / 1100blue, squash: 1100; others: 1000Body text and the cross.
--global-*-1100 / 1200blue, squash: 1200; others: 1100Title, and the cross on hover.
--global-*-800 / 900 / 1000green, red: 800; grey: 900; blue, squash: 1000Status icon.
--global-blue-1000#1877F2Focus ring on the cross.

Toast

Confirms that something just happened, then gets out of the way.

Live React portVue source: WildToast.vueStorybook story: Feedback/ToastReact component: WToast

Try it

Close it with the cross, or turn on Auto-close and wait five seconds.

Threshold saved
Variant
Message
Closable
Auto-close
<WildToast
  variant="success"
  @close="hide"
>
  Threshold saved
</WildToast>

When to use

  • Use a toast to confirm something that just happened: a save, an export, a sent invite.
  • Use auto-close for messages nobody has to act on.
Don’t use it
  • For a condition that lasts, or an error someone must fix. Use Alert.
  • For a decision before an action runs. Use Confirm dialog.

Variants

Five tones. Pick the one that matches what happened. Use accent for information worth noticing, and neutral for routine confirmations.

Threshold saved
Could not save the threshold
Export finished with 2 sites skipped
Readings resume at the next check-in
Export queued
<WildToast variant="success">Threshold saved</WildToast>
<WildToast variant="error">Could not save the threshold</WildToast>

Closing

Toasts show a close cross by default. Turn it off only when the toast closes itself.

Threshold saved
Could not save the threshold for logger 4412 at Waikare Station
<WildToast variant="success" @close="hide">Threshold saved</WildToast>
<WildToast :closable="false" :auto-close="3000">Export queued</WildToast>
A long message wraps onto the next line, so none of it is cut off. The last toast closes itself after three seconds.

Content

  • Say what happened in a few words: “Threshold saved”.
  • Name the logger or site when it matters which one.
  • Use sentence case, with no full stop.
  • Keep it short enough to read at a glance.
Don’t use it
  • Auto-closing an error. People need time to read it.
  • Stacking more than three at once.

Accessibility

Error and warning toasts use role="alert", which interrupts a screen reader, and the others use role="status". Set role to override this. Auto-close pauses while the pointer is over the toast or focus is inside it.

KeyWhat it does
TabMoves focus to the cross, announced as “Close”.
Enter / SpaceCloses the toast and moves focus to the next control.

Props

PropTypeDefaultDescription
variant'success' | 'error' | 'warning' | 'accent' | 'neutral''neutral'The tone. See Variants.
messagestring'Notification'The text. The default slot replaces it.
closablebooleantrueShows the close cross.
autoClosenumber0Closes after this many milliseconds. 0 never closes.
truncatebooleanfalseKeeps the message on one line, with an ellipsis.
role'alert' | 'status' | 'none'—Live region. Defaults to alert for error and warning.
@close() => void—Fires after the toast hides, on the cross or auto-close.

Tokens

TokenValueWhere it lands
--radius-field10pxCorners.
(no token)0 2px 8px rgba(0,0,0,.08)Lift off the page.
--global-*-100white for neutralGround.
--global-*-300grey-500 for neutralBorder, 1.5px.
--global-*-800 / 900 / 1000green, red: 800; squash: 900; blue, grey: 1000Status icon.
--global-*-1000 / 1100 / 1300green: 1000; red, squash, blue: 1100; grey: 1300Message text.
--global-*-1000 / 1100rest / hoverThe cross.
--global-*-400 / 500grey-600 / 700 for neutralThe cross’s hover and pressed circle.
--global-blue-1000#1877F2Focus ring on the cross.

Tooltip

Shows a short label or an exact value next to the thing it describes.

Live React portVue source: WildTooltip.vueStorybook story: Feedback/TooltipReact component: WTooltip

Try it

Change the position to turn the arrow, and the tone to suit the surface underneath.

14 minutes ago
Position
Text
Tone
<WildTooltip
  text="14 minutes ago"
  position="top"
/>

When to use

  • Use a tooltip to name an icon button, or to give the exact value behind a rounded one.
  • Use it for the timestamp or unit a dense table had to drop.
Don’t use it
  • For anything people need to act. Touch screens can’t hover, so put it on the page.
  • For more than one line, or for a link. Use a popover.

Tones

Two, with the same size and arrow, so switching tone never moves the bubble.

Last reading 14 minutes ago
dark
Last reading 14 minutes ago
light
<WildTooltip text="14 minutes ago" />
<WildTooltip text="14 minutes ago" tone="light" />
  • Use dark by default. It stands out on white cards, grey rows and map tiles alike.
  • Use light on a dark or busy ground, such as a chart plot or a satellite map, where a dark bubble would sink into it.
  • Keep one tone per screen, so every tooltip on it reads as the same thing.

Positions

Four. The position is the side of the trigger the bubble sits on, and the arrow points back at it.

Last reading 14 minutes ago
top
Last reading 14 minutes ago
bottom
Last reading 14 minutes ago
left
Last reading 14 minutes ago
right
<WildTooltip text="Last reading 14 minutes ago" position="top">
  <WildIconButton icon="info" />
</WildTooltip>

Content

One line. A tooltip that wraps should have been a popover.

14 minutes ago
Soil moisture at 300 mm · 42.1%
Lead with the value, then its unit or time. Leave out the full stop.

Accessibility

Show it on keyboard focus as well as hover, and point the trigger’s aria-describedby at it. A screen reader then reads it after the trigger’s name.

KeyWhat it does
TabFocusing the trigger shows the tooltip.
EscapeHides it, and focus stays on the trigger.

Props

PropTypeDefaultDescription
textstring—The one line it shows.
position'top' | 'bottom' | 'left' | 'right''top'Which side of the trigger it sits.
tone'dark' | 'light''dark'See Tones.

Tokens

TokenValueWhere it lands
--global-neutral-grey-1300#0A090BDark ground.
--color-white#FFFFFFDark text; light ground.
--global-neutral-grey-1100#2D2B32Light text.
--global-neutral-grey-600#DCDCDELight border and arrow edges.
--shadow-dropdown0 4px 12px rgba(0,0,0,.08), …Both tones.
--radius-xs3pxCorner.
(no token)10px / 14px, 500Text size, line height and weight.

Skeleton

Holds the shape of content while it loads, so the page doesn't jump when it arrives.

Live React portVue source: WildSkeleton.vueStorybook story: Feedback/SkeletonReact component: WSkeleton

Try it

Pick a shape, then turn on Loaded to see the content it holds the place of.

Variant
Width
Height
Loaded
<WildSkeleton
  variant="text"
/>

When to use

  • Use a skeleton while a list, card or reading loads and its layout is already known.
  • Build it to the layout that replaces it, so nothing moves when the data lands.
Don’t use it
  • For an action the person just started. Use the loading state on Button.
  • For a job with a known end. Use Progress bar.
  • When the result is empty. Use Empty state.

Variants

One shape for each thing that loads: a line of text, an avatar, a square-cornered block and a card.

text
circular
rectangular
rounded
rounded · radius 16
<WildSkeleton variant="text" width="70%" />
<WildSkeleton variant="text" width="45%" />
<WildSkeleton variant="circular" />
<WildSkeleton variant="rectangular" height={72} />
<WildSkeleton variant="rounded" height={72} />
<WildSkeleton variant="rounded" height={72} radius={16} />
Rounded defaults to 8px corners. Set radius to 16 when it stands in for a dashboard widget card.

Sizes

28px
40px
56px
<WildSkeleton variant="circular" width={28} />
<WildSkeleton variant="circular" />
<WildSkeleton variant="circular" width={56} />
A circle stays a circle. Give it one dimension and the other follows.

Composition

Give each part you’re replacing a skeleton of its own shape. Use count for a run of lines.

<WildSkeleton variant="circular" width={32} />
<WildSkeleton variant="text" width="58%" height={12} />
<WildSkeleton variant="text" width="36%" height={10} />
<WildSkeleton variant="rounded" width={64} height={24} radius={6} />
A row that loads into an avatar, two lines and a badge gets a skeleton for each.
<WildSkeleton variant="text" count={3} />
Three lines hold exactly three lines of body text, so the paragraph lands without a shift.

Accessibility

A skeleton is hidden from screen readers and takes no focus. Set aria-busy on the region that’s loading, and clear it when the content lands. The pulse stops under reduced motion.

Props

PropTypeDefaultDescription
variant'text' | 'circular' | 'rectangular' | 'rounded''text'A line, a circle, a square block or a rounded block.
widthnumber | string—Defaults to 100%, or 40px for circular.
heightnumber | string—Defaults to 16, 40 or 100px by variant.
radiusnumber | string—Overrides the variant corner.
countnumber1Repeats the skeleton in a column.
animation'pulse' | 'none''pulse'Pulse, or a still block.
styleCSSProperties—Extra inline styles, applied last.

Tokens

TokenValueWhere it lands
--global-neutral-grey-300#F1F1F1The fill.
--radius-sm4pxText corners.
--radius-lg8pxRounded corners, by default.
--radius-none0Rectangular corners.
--radius-full9999pxCircular corners.
(no token)1500msOne pulse cycle.

Empty state

What an empty container says, decided by why it is empty: nothing yet, no matches, or all clear.

Refined portVue source: WildEmptyState.vueStorybook story: Feedback/Empty StateReact component: WEmptyStateRefined

Try it

Change the reason, and the picture, words and action change with it. Remove a filter, or clear them all.

No sites match these filters

42 sites are hidden. Remove a filter to widen the list.
Reason
Size
Description
Actions
<WildEmptyState
  reason="no-matches"
  title="No sites match these filters"
  description="42 sites are hidden. Remove a filter to widen the list."
  :filters="activeFilters"
  @remove-filter="removeFilter"
  primary-label="Add a site"
  @primary="create"
  secondary-label="Clear filters"
  @secondary="recover"
/>
Only nothing-yet gets a filled button, because creating the first record is its way out. The other two step down to grey.

Three reasons

Pick the reason by why the list is empty. Each has its own picture, words and weight of action.

No sites yet

Add your first logger and its readings appear here within a few minutes.
nothing yet

No sites match these filters

42 sites are hidden. Remove a filter to widen the list.
no matches

No alarms this week

Every site reported inside its thresholds.Checked 2 minutes ago
all clear
The picture is a ghost of what belongs there: skeleton cards, a list under a lens, or a tick when empty is good news.

Sizes

A whole page, the body of a card, or a row in a table.

Connect your first logger

Plug in a WildEye logger, press its button, and it appears here with its first reading.
Use page size when the whole screen has nothing yet. It has a larger picture, title and buttons.
AlarmsThis week

No alarms this week

Every site reported inside its thresholds.Checked 2 minutes ago
Panel size fills a card’s body. The card keeps its header, so people still know what would be here.
Loggers0 of 42
NameLast readingBattery

No loggers match “bore 14”

Check the spelling, or search by serial number.
Inline size sits where the rows would be, so the table keeps its header and shape.

Content

Three parts, each with one job.

PartIts jobExample
TitleSay what is empty, in the reader’s words.No sites match these filters
DescriptionSay why, or what to do next. Never repeat the title.42 sites are hidden. Remove a filter to widen the list.
ActionOne verb and a noun. The way out, not a tour.Clear filters

Not an error

A list that loaded and came back empty is an empty state. A list that failed to load is an alert.

Couldn’t load sitesThe readings service did not respond. Try again, or check the status page.
the load failed — alert

No sites yet

Add your first logger to start seeing readings.
the load worked and returned nothing — empty state
Empty says there’s nothing here. An error says the product couldn’t tell you.

Filters

On no-matches, show the filters that emptied the list as filter chips. Each × removes one, and its circle fills on hover.

  • Name how many results the filters hide, so people know removing one is worth it.
  • Offer Clear filters as the secondary action.

Props

PropTypeDefaultDescription
reason'nothing-yet' | 'no-matches' | 'all-clear''nothing-yet'Why it’s empty. Sets the picture and the action’s weight.
size'page' | 'panel' | 'inline''panel'A whole screen, a card body, or a table row.
titlestring—What is empty. Required.
descriptionstring—Why, or what to do next.
primary-labelstring—The way out. Filled only on nothing-yet.
secondary-labelstring—A second, quieter action.
filters{ id, label }[]—no-matches: the filters that emptied the list.
checked-textstring—all-clear: a live line such as “Checked 2 minutes ago”.

Tokens

TokenValueWhere it lands
--color-grey-1300#0A090BTitle.
--color-grey-1000#4F4D55Description.
--color-grey-400 / 500#ECECED / #E6E6E6Skeleton bars in the picture.
--shadow-group0 2px 12px -1px …The ghost cards.
--color-primary#00AEEFThe “+” tile on nothing-yet.
TONE.ok#E1FAEA · #C1F4D4 · #019939Rings and tick on all-clear.
--radius-field10pxGhost cards and the “+” tile.

Dialog

Asks for focused input or a decision in a panel over the page.

Live React portVue source: WildDialog.vueStorybook story: Feedback/DialogReact component: WDialog

Try it

Open it, change its width, then close it with Escape, the scrim or a button. Focus goes back to Edit site.

md · 448px
Size
Delete slot
Grey canvas
<WildDialog
  title="Edit site"
  size="md"
  footerLeft={<WildButton variant="destructive">Delete site</WildButton>}
  footer={
    <>
      <WildButton variant="secondary">Cancel</WildButton>
      <WildButton>Save changes</WildButton>
    </>
  }
  onClose={close}
>
  <WildFormField label="Site name">…</WildFormField>
</WildDialog>

When to use

  • Use a dialog for a short task that has to be finished or cancelled first, such as editing a site.
  • Use one dialog at a time. Never open a second on top of the first.
Don’t use it
  • To confirm a delete. Use a Confirm dialog.
  • For a message that needs no answer. Use a Toast.
  • For a long form people fill in beside the page. Use a Drawer.

Anatomy

A title with a close button, the body, and a footer. The footer’s left slot holds the destructive action, away from the primary.

Edit site
Site name*
Bore 12 — north paddock
Group
North paddock
<WildDialog title="Edit site" size="md" @close="close">
  <WildFormField label="Site name"> … </WildFormField>
  <template #footer>
    <WildButton variant="secondary">Cancel</WildButton>
    <WildButton>Save changes</WildButton>
  </template>
</WildDialog>
Point at the ×: it fills a grey circle on hover and a darker one when pressed. Keep it even when the footer has Cancel.

Widths

Four, each tied to a kind of task. Pick by the task, not by how much text there is.

sm · 384 · Confirm and destroy
Edit site
Readings arrive every fifteen minutes from this logger.
md · 448 · A short form
Edit site
Readings arrive every fifteen minutes from this logger.
lg · 672 · A form with two columns
Edit site
Readings arrive every fifteen minutes from this logger.
xl · 896 · Alarm configuration
Edit site
Readings arrive every fifteen minutes from this logger.

Content

  • Title it with the task, in sentence case: Edit site, Invite people.
  • Label the primary button with the verb from the title: Save changes, Send invites.
  • Keep the body to what the task needs. A form that scrolls belongs on a page.

Accessibility

Move focus into the dialog when it opens, and back to the button that opened it when it closes. The × is a button named “Close dialog”.

KeyWhat it does
TabMoves between the dialog’s controls, and stays inside it.
EscapeCloses it, the same as Cancel.

Props

PropTypeDefaultDescription
titlestring—The task, shown in the header.
size'sm' | 'md' | 'lg' | 'xl''md'Width: 384, 448, 672 or 896px.
footerslot—The actions, right-aligned.
footerLeftslot—The destructive action, or a footer control.
canvasbooleanfalseGrey body, for white cards inside.
@close() => void—Draws the × and handles it.

Tokens

TokenValueWhere it lands
--radius-3xl16pxThe dialog corner.
--dialog-overlayrgba(10, 9, 11, 0.5)The scrim.
--shadow-xl0 20px 25px -5px …, 0 10px 10px -5px …Elevation.
--z-index-modal500Above dropdowns; below popovers and toasts.
--color-bg-elevated#FFFFFFThe surface.
--global-neutral-grey-400#ECECEDHeader and footer rules; × pressed.
--global-neutral-grey-300#F1F1F1The × circle on hover.
--global-neutral-grey-900 / 1100#7F7D83 / #2D2B32The × at rest, then on hover.

Progress

One reading judged against a threshold, such as soil moisture against its refill point.

Refined portVue source: WildProgress.vueStorybook story: Feedback/ProgressReact component: MeasureBar, BarPill

Try it

Walk the value across the threshold, then move the readout to the head of the fill.

42%
Colour
Value
Readout
Threshold
Scale
<MeasureBar
  label="Soil moisture"
  pct={42}
  value="42"
  unit="%"
  threshold={50}
  fill={RAMP.green[800]}
  fillBelow={RAMP.squash[1000]}
  scale={['0', '50', '100']}
/>
The figure and colour change at once and the fill follows, so the reading is never late.

When to use

Progress is a 32px instrument for one reading against a threshold. For a job with an end, use the slim Progress bar.

  • Use it for a reading judged against a threshold, such as soil moisture against its refill point.
  • Stack one bar per reading, so the figures line up in one column.
Don’t use it
  • For a job with an end, such as an upload or a firmware update. Use Progress bar.
  • For a bar inside a table row or a list. Use Progress bar, inline.
  • For a whole probe profile in one gauge. Use the soil rail.
  • While content loads. Use Skeleton.

Threshold

A black line marks the point the reading is judged against. Pass fillBelow so the bar changes colour under the line. Keep the line clear of an end readout, which sits over it.

34%
71%
<MeasureBar label="Soil moisture" pct={34} threshold={50} fill={RAMP.green[800]} fillBelow={RAMP.squash[1000]} value="34" unit="%" scale={['0', '50', '100']} />
<MeasureBar label="Soil moisture" pct={71} threshold={50} fill={RAMP.green[800]} fillBelow={RAMP.squash[1000]} value="71" unit="%" scale={['0', '50', '100']} />
The line keeps a white edge, so it reads on the track and on any fill.

Readout

The value sits in a white pill, so it reads over the fill and the track alike.

18%
42%
68%
18%
<MeasureBar label="Soil moisture" pct={18} value="18" unit="%" />
<MeasureBar label="Soil moisture" pct={42} value="42" unit="%" />
<MeasureBar label="Soil moisture" pct={68} value="68" unit="%" />
<MeasureBar label="Soil moisture" pct={18} value="18" unit="%" readoutAt="head" />
Keep the readout at the end in a stack, so the figures form a column. Use head on a lone bar.

Compact readout

42.1%
value
17.4°C
with unit
3,184
bare
<BarPill value="42.1" unit="%" />
Where only the figure matters, show the readout alone in the same pill.

Content

  • Name the reading in label, such as “Soil moisture”.
  • Put the figure in value and its unit in unit, so the unit sets lighter.
  • Give every bar in a stack the same unit and decimal places.

Accessibility

The bar is a meter: a screen reader hears its label and the readout, such as “34%”. Under reduced motion the fill doesn’t sweep or ease.

KeyWhat it does
TabPasses over the bar. It never takes focus.

Props

PropTypeDefaultDescription
labelstring—Required accessible name, such as the reading.
pctnumber—Fill, from 0 to 100.
valuestring—The readout. Leave out for a bare bar.
unitstring—Shown after the value, in lighter ink.
readoutAt'end' | 'head''end'Readout at the track end, or at the fill's end.
thresholdnumber—Where the black line sits, from 0 to 100.
fillstringblue-1000Fill colour; with fillBelow, only at or over the threshold.
fillBelowstring—Fill colour under the threshold.
trackstringgrey-300Track colour.
scalestring[]—Marks under the bar, at even positions.

BarPill The readout on its own.

PropTypeDefaultDescription
valuestring—The figure, already formatted.
unitstring—Shown after the value, in lighter ink.

Tokens

TokenValueWhere it lands
--global-blue-1000#1877F2Default fill.
--global-green-800#019939Fill at or over the threshold.
--global-squash-1000#9D6508Fill under the threshold.
--global-neutral-grey-300#F1F1F1The track.
--global-neutral-grey-1300#0A090BThreshold line and value.
--global-neutral-grey-1000#4F4D55Scale marks and unit.
--global-base-white#FFFFFFReadout pill and threshold edge.
--radius-md6pxTrack and pill corners.
--font-weight-extrabold700Readout value.
(no token)32pxTrack height.
(no token)800msFill sweep and change.

Drawer

A panel from the edge of the screen for editing one item while the page stays in view.

Live React portVue source: WildDrawer.vueStorybook story: Feedback/DrawerReact component: WildDrawer

Try it

Open the drawer, change a setting, then close it with Escape, the scrim or a button.

Sites
Bore 12North paddock
Bore 14North paddock
Tank 3Home block
Pump stationRiver flats
Tank 7River flats
Position
Size
Close button
Close on scrim
<WildDrawer
  :open="open"
  title="Bore 12"
  position="right"
  size="md"
  @close="open = false"
>
  …
  <template #footer>…</template>
</WildDrawer>

When to use

  • Use a drawer to edit or inspect one item while the list behind it stays in view.
  • Use it for settings and short forms that belong to the current page.
Don’t use it
  • To confirm a deletion or other decision that should stop the work. Use a confirm dialog.
  • For a short task that doesn’t need the page behind it. Use a dialog.
  • For the app’s main navigation. Use Sidebar nav.
  • Inside another drawer. Replace the drawer’s content, or move the task to its own page.

Anatomy

A header with the title and close button, a body that scrolls, and a footer for actions. The panel fills the height of the screen.

Sites
Bore 12North paddock
Bore 14North paddock
Tank 3Home block
Pump stationRiver flats
Tank 7River flats
Bore 12
Site name
Bore 12
Group
North paddock
SMS alertsImmediate
<WildDrawer :open="open" title="Bore 12" @close="open = false">
  …
  <template #footer>
    <WildButton variant="secondary" size="sm">Cancel</WildButton>
    <WildButton size="sm">Save changes</WildButton>
  </template>
</WildDrawer>

Position

Right is the default, for detail views and forms. Use left for secondary content.

Sites
Bore 12North paddock
Bore 14North paddock
Tank 3Home block
Pump stationRiver flats
Tank 7River flats
Bore 12
Site name
Bore 12
Group
North paddock
SMS alertsImmediate
<WildDrawer :open="open" position="left" title="Bore 12" />

Content

  • Title the drawer with the name of the thing it shows: the site, logger or alarm.
  • Put the primary action last in the footer, and start it with a verb.
  • Keep the body to one task. A second task needs its own drawer or page.

Accessibility

The drawer is announced as a dialog named by its title. Focus moves into it on open, stays inside it, and returns to the control that opened it.

KeyWhat it does
Tab / Shift+TabMoves through the drawer’s controls, wrapping at either end.
EscapeCloses the drawer. An open list inside it closes first.
Enter / SpaceOn the close button, closes the drawer.

Props

PropTypeDefaultDescription
openboolean—Shows the drawer. Required.
titlestring—Heading in the header, and the accessible name.
size'sm' | 'md' | 'lg' | 'xl' | 'full''md'320, 400, 560, 720px or full; full at 640px and under.
position'left' | 'right''right'The edge it slides in from.
showClosebooleantrueShows the close button in the header.
closeOnOverlaybooleantrueCloses when the scrim is clicked.
closeOnEscbooleantrueCloses when Escape is pressed.
ariaLabelstring—Accessible name when there is no title.
@closeevent—Fired when the drawer asks to close.
#headerslot—Replaces the title. The close button stays.
#footerslot—Actions, aligned right.

Tokens

TokenValueWhere it lands
--dialog-overlayrgba(10, 9, 11, 0.5)Scrim, shared with the dialog, 2px blur.
(no token)0 20px 25px -5px …The dialog’s panel shadow.
--global-neutral-grey-500#E6E6E6Leading edge, header and footer rules.
--global-neutral-grey-1200#1D1C20Title, 18px at 600.
--duration-slow300msSlide in, with --ease-out.
--duration-moderate200msSlide out, with --ease-in.
(no token)1000Z-index, above toolbars.

Confirm dialog

Asks one question before an action that can't be undone, such as deleting an API key.

Live React portVue source: WildConfirmDialog.vueReact component: WConfirmDialog

Try it

Set the label to OK to see how little the button says. Press Delete key to reopen it.

Delete this API key?
It is removed permanently.
Confirm label
Tone
Consequence
<WConfirmDialog
  open={open}
  title="Delete this API key?"
  confirmLabel="Delete key"
  onConfirm={deleteKey}
  onCancel={() => setOpen(false)}
>
  It is removed permanently.
</WConfirmDialog>

When to use

  • Use it before an action that deletes data or can’t be undone.
  • Use it when an action affects more than the thing in front of the person, such as anything that uses an API key.
  • Use it, in the primary tone, before a reversible action with wide effects, such as pausing an automation.
Don’t use it
  • For routine actions that are quick to undo. Run them, and confirm with a toast.
  • To collect input or show a form. Use Dialog.

Variants

Destructive is the default. Use primary when nothing is lost but the effect is wide, such as pausing an automation. Add a consequence panel when the action reaches beyond the thing itself.

<WConfirmDialog
  open={open}
  title="Cancel this invite?"
  confirmLabel="Cancel invite"
  cancelLabel="Keep invite"
  onConfirm={cancelInvite}
  onCancel={() => setOpen(false)}
>
  It is removed permanently.
</WConfirmDialog>
The confirm sits on the right, where the primary always sits. Cancel can name what it keeps.
<WConfirmDialog
  open={open}
  title="Delete this API key?"
  confirmLabel="Delete key"
  consequence="Anything using this key can no longer reach the API."
  onConfirm={deleteKey}
  onCancel={() => setOpen(false)}
>
  It is removed permanently.
</WConfirmDialog>
<WConfirmDialog
  open={open}
  title="Pause this automation?"
  confirmLabel="Pause automation"
  tone="primary"
  onConfirm={pauseAutomation}
  onCancel={() => setOpen(false)}
>
  It stops running until you turn it back on.
</WConfirmDialog>

Content

  • Ask the question in the title, naming the thing: “Delete this API key?”
  • Say in the body what is lost and what isn’t.
  • Name the consequence on the button: “Delete key”, “Cancel invite”. People who skim the body still read the button.
  • Name what else breaks in the consequence panel, with a number where there is one.
  • Name what Cancel keeps when “Cancel” would be ambiguous: “Keep invite”.
Don’t use it
  • OK, Yes or Confirm on the button. They don’t say what happens.

Accessibility

It opens as an alert dialog, named by its title and described by its body and consequence. Focus lands on Cancel, so a double Enter can’t delete anything, and returns to the trigger on close.

KeyWhat it does
Tab / Shift+TabMoves between the buttons, staying inside the dialog.
Enter / SpacePresses the focused button.
EscCancels and closes.

Props

PropTypeDefaultDescription
openboolean—Shows the dialog.
titleReactNode—The question, naming the thing affected.
childrenReactNode—What is lost, in a sentence or two.
confirmLabelstring—Verb and object: Delete key. Required.
cancelLabelstring'Cancel'The safe action.
tone'destructive' | 'primary''destructive'Primary when nothing is lost, such as a pause.
consequenceReactNode—Warning panel for knock-on effects.
loadingbooleanfalseSpinner on the confirm while the action runs.
onConfirm() => void—Runs the action.
onCancel() => void—Cancel, Esc, the close button or the scrim.
closeOnOverlaybooleantrueWhether a scrim click cancels.
containedbooleanfalseFills the nearest positioned parent instead of the viewport.
autoFocusbooleantrueMoves focus to Cancel on open.

Tokens

TokenValueWhere it lands
max-w-sm384pxPanel width.
--radius-3xl16pxPanel corners.
--dialog-overlayrgba(10, 9, 11, 0.5)The scrim, with a 2px blur.
--color-error#E12121Destructive confirm fill.

Error message

The one-line failure text under a field, which holds its space so a message never shifts the layout.

Refined portVue source: WildErrorMessage.vueReact component: WErrorMessage

Try it

Switch the message on and off, and watch the field below it stay put.

Use a value between 0 and 250 kPa.
Message
<WildTextInput v-model="high" aria-describedby="high-error" />
<WildErrorMessage
  id="high-error"
  :messages="errors"
  :current-message-id="1"
/>
The line is always there. It holds 18px when empty, so a one-line message never moves the next field.

When to use

  • Use it under one field, to say how to fix that field’s value.
  • Show it on blur or on submit, and clear it once the value is valid.
Don’t use it
  • For a problem with the whole form or a failed save. Use an alert.
  • Inside a form field. Set its error prop instead.

Anatomy

An exclamation mark and a sentence, left-aligned under the field it belongs to.

Use a value between 0 and 250 kPa.

Length

One line is the target. A longer message wraps under its own text, keeps the mark on the first line, and pushes the next field down.

Enter a site name.
one line
Use a value between 0 and 250 kPa. Use numbers only, e.g. 120 or 120.5, with no unit after them.
wrapping
Inline, nothing below
Required.
no reserved line

Content

Say what to do, not what went wrong. “Use a value between 0 and 250 kPa” fixes the field; “Invalid value” only tells you it’s broken.

Use a value between 0 and 250 kPa.
do
Invalid value.
don't

Accessibility

Give the line an id and point the field’s aria-describedby at it, so a showing message is read with the field. In React the line also stays mounted as a polite live region, so a new message is announced without moving focus. The shipped Vue line is not a live region and unmounts when empty.

Enter a site name.
<WildTextInput v-model="name" aria-describedby="name-error" />
<WildErrorMessage id="name-error" :messages="errors" :current-message-id="0" />

Props

Rows from id down are refined additions.

PropTypeDefaultDescription
messages{ id: number, message: string }[]—Every message this field can show.
current-message-idnumber—Required. Index into messages; -1 shows none.
idstring—Target for the field’s aria-describedby.
reservebooleantrueHold the 18px line when empty.

Tokens

TokenValueWhere it lands
--color-error#E12121Message text and mark.
--font-size-sm12pxMessage text, 18px line.
--size-icon-xs14pxThe exclamation mark.

Success screen

The end of a flow: what happened, what it happened to, and the next thing to do.

Refined portVue source: WildSuccessScreen.vueStorybook story: Feedback/SuccessScreenReact component: WSuccessScreen

Try it

Change the tone and the copy changes with it. Press Replay to watch it arrive.

3 invites sent

We’ve emailed each of them a link to join Emu Park Orchard.

  • aroha.walker@emupark.co.nzOperator
  • sam.te-ao@emupark.co.nzViewer
  • priya.nair@baycitrus.co.nzViewer
Tone
Size
Items
Actions
Animate
<WildSuccessScreen
  headline="3 invites sent"
  subtitle="We’ve emailed each of them a link to join Emu Park Orchard."
  :items="invites"
  items-label="Invited people"
  :primary="{ label: 'Done', onClick: close }"
  :secondary="{ label: 'Invite more', onClick: next }"
/>

When to use

  • Use it to end a flow that changed something.
  • Use it in place of the form it confirms.
Don’t use it
  • When the person carries on where they were. Use a toast.
  • When the work failed. Use an alert.

Anatomy

Five parts, top to bottom. Only the mark and the headline always show.

3 invites sent

We’ve emailed each of them a link to join Emu Park Orchard.

  • aroha.walker@emupark.co.nzOperator
  • sam.te-ao@emupark.co.nzViewer
  • priya.nair@baycitrus.co.nzViewer
Mark
The tone disc in two rings. Decorative; the words carry the meaning.
Headline
What happened, with its count. A heading, 18px at 600.
Subtitle
What happens next, or what to check.
Items
What it happened to. Optional, and capped with “Show N more”.
Actions
The next step and a way on. Optional where a footer owns them.
The list stays left-aligned, so it scans like a table.

Tones

It all worked, some of it worked, or it was accepted and will finish later.

Alarm saved

Bore 12 alerts Operators below 18 kPa.

success

2 of 3 invites sent

One address bounced. Fix it to resend.

warning

Export queued

We’ll email you a link when it’s ready.

info

Sizes

Medium for a dialog. Small for a drawer, a side panel or a card.

Alarm saved

It’s watching Bore 12 now. The first check runs at the next reading.

md · dialog

Alarm saved

It’s watching Bore 12 now.

sm · drawer

Items

Three rows show, and the rest open behind Show N more.

11 of 12 invites sent

One address bounced. Fix it and we’ll send that invite again.

  • Failed: sam.te-ao@emupak.co.nzAddress not found
  • aroha.walker@emupark.co.nzOperator
  • priya.nair@baycitrus.co.nzViewer
Failed rows sort first, say why and always show. One hidden row just shows.

Entrance

Disc, rings, glyph, then words, in about half a second. Reduced motion shows the settled screen.

Alarm saved

It’s watching Bore 12 now.

Disc0–300
Inner ring60–440
Outer ring120–500
Glyph180–520
Headline100–300
Subtitle140–340
List180–380
Actions260–460
0200400600ms

The disc and the glyph dots are springs that settle in about 300ms. The strokes draw; the rings and words ease out.

In context

Invite people
Email addresses
aroha.walker@emupark.co.nzsam.te-ao@emupark.co.nzpriya.nair@baycitrus.co.nz
They’ll join Emu Park Orchard as viewers.
In a dialog, Done takes Send’s place in the footer, and focus moves to it.
New alarmStep 3 of 3

Alarm saved

It’s watching Bore 12 now. The first check runs at the next reading.

Site
Bore 12 · Emu Park
When
Soil moisture below 18 kPa for 30 min
Notify
Operators, by SMS and email
Without a footer, the screen brings its own actions and focuses Done on arrival.

Content

  • Put the count in a past-tense headline: 3 invites sent.
  • Say what happens next, or what to check, in the subtitle.
  • Make the primary Done, or the fix on a partial result.
  • Make the secondary the likeliest next task: Invite more.

Accessibility

A polite live region announces the headline and subtitle. With auto-focus, focus lands on the primary, so Enter finishes the flow.

KeyWhat it does
TabMoves to the list if it scrolls, then “Show N more”, then the actions.
Enter / SpacePresses the focused action, or opens and closes the list.

Props

PropTypeDefaultDescription
headlinestring—What happened, with its count.
subtitlestring—What happens next.
variant'success' | 'warning' | 'info''success'Sets the disc, rings and glyph.
size'md' | 'sm''md'Dialog, or drawer and panel.
items(string | { id?, label, meta?, failed? })[][]What it happened to.
item-iconstring'mail'mail, user, site, logger, file, alarm or none.
items-labelstring—Names the list for screen readers.
max-visible-itemsnumber3Rows before “Show N more”.
primarystring | { label, onClick?, icon? }—The next step, usually “Done”.
secondarystring | { label, onClick?, icon? }—A bordered second action.
animatebooleantruePlay the entrance.
auto-focusbooleanfalseFocus the primary, or the headline, on arrival.
announcebooleantrueAnnounce the outcome politely.
title-as'h2' | 'h3' | 'h4' | 'p''h2'Headline element.
#iconslot—Replaces the glyph in the disc.
#defaultslot—Anything between the list and the actions.

Tokens

TokenValueWhere it lands
--global-green-200 · 300#E1FAEA · #C1F4D4Success rings.
--global-squash-200 · 300#FFF1DA · #FFE9BAWarning rings.
--global-blue-200 · 300#D6F4FF · #BDEDFFInfo rings.
--color-success#019939Success disc.
--global-squash-900#C47E0AWarning disc.
--global-blue-1000#1877F2Info disc and toggle label.
--global-blue-1100#0C296AToggle label on hover.
--global-neutral-grey-1300#0A090BHeadline.
--global-neutral-grey-1100#2D2B32Row label.
--global-neutral-grey-1000#4F4D55Subtitle and row meta.
--global-neutral-grey-900#7F7D83Item icon.
--global-neutral-grey-400#ECECEDList frame.
--global-neutral-grey-300#F1F1F1Row dividers, and the pressed toggle.
--global-neutral-grey-200#F5F5F5Toggle ground on hover.
--global-red-100#FEEBEBFailed row ground.
--global-red-800#E12121Failed row icon.
--global-red-1000#961616Failed row meta.
--color-primary-active#1877F2Focus ring on the toggle and scroller.

Code block

Shows read-only code and data, highlighted and numbered, with one click to copy.

Refined portVue source: WildCodeBlock.vueStorybook story: Feedback/Code BlockReact component: WCodeBlock

Try it

Copy, wrap and open each example, then try empty.

Responsejson200 OK
{  "serial": "WE-10442",  "name": "Bore 12",  "site": {    "id": "site_2291",    "name": "Emu Park"  },  "firmware": "4.2.1",  "online": true,  "battery_v": 3.62,  "signal_dbm": -87,  "interval_min": 15,  "last_seen": "2026-09-28T05:30:02+13:00",  "channels": [    {      "id": "soil_moisture_10cm",      "unit": "%",      "value": 34.2    },    {      "id": "soil_moisture_30cm",      "unit": "%",      "value": 31.4    },    {      "id": "soil_moisture_60cm",      "unit": "%",      "value": 28.9    },    {      "id": "line_pressure",      "unit": "kPa",      "value": 212.5    }  ],  "alarms": {    "active": 0,    "acknowledged": 1  },  "reseller": null}
Content
Line numbers
Collapsible
Title
<WildCodeBlock
  :code="response"
  language="json"
  title="Response"
  show-line-numbers
  collapsible
  :collapse-threshold="12"
  @copy="onCopied"
>
  <template #meta>
    <WildBadge color="success" size="2xs">200 OK</WildBadge>
  </template>
</WildCodeBlock>
Values carry the colour; keys stay ink. Copy leaves out line numbers.

When to use

  • Show a payload, export or log someone will read or copy.
  • Use the single line for an endpoint, key or secret.
  • Mask secrets on shared screens.
Don’t use it
  • For a value people edit. Use Text input.
  • For readings people compare. Use Table.

Anatomy

A header, a grey well, and a footer when there’s more to show.

Webhook payloadjsonalarm.triggered
{  "event": "alarm.triggered",  "id": "evt_01J8Z6Q4M2WQ",  "occurred_at": "2026-09-28T05:42:17+13:00",  "alarm": {    "id": "alm_7731",    "name": "Tank low",    "severity": "high",    "threshold": 20,    "value": 17.8,    "unit": "%"  },  "site": {    "id": "site_3107",    "name": "North Pump"  },  "logger": {    "serial": "WE-10877",    "name": "Tank 3"  },  "acknowledged": false,  "acknowledged_by": null}
Title, language and meta sit left; wrap and Copy sit right, at 32px.

Highlighting

Five JSON roles share the grey well. XML reuses them.

"battery_v"Keys--global-neutral-grey-1300 ·#0A090B
"Emu Park"Strings--global-blue-1100 ·#0C296A
3.62 -87Numbers--global-violet-800 ·#6E23C9
true nulltrue, false, null--global-purple-900 ·#7B238D
{ } [ ] : ,Punctuation--global-neutral-grey-1000 ·#4F4D55
12Line numbers--global-neutral-grey-1000 ·#4F4D55
No green, amber or red in the syntax, because those mean state.

Languages

JSON is re-indented. XML and plain text appear as they arrive.

Weekly exportxml
<?xml version="1.0" encoding="UTF-8"?><export site="Emu Park" from="2026-09-01" to="2026-09-07">  <!-- Daily mean soil moisture, 30 cm probe -->  <logger serial="WE-10442" name="Bore 12">    <reading date="2026-09-01" unit="%">32.1</reading>    <reading date="2026-09-02" unit="%">31.8</reading>    <reading date="2026-09-03" unit="%">30.9</reading>    <reading date="2026-09-04" unit="%">34.6</reading>    <reading date="2026-09-05" unit="%">33.9</reading>    <reading date="2026-09-06" unit="%">33.2</reading>    <reading date="2026-09-07" unit="%">32.4</reading>  </logger></export>
Sync logplain text
05:30:02  Bore 12     Connected over LTE-M, signal -87 dBm05:30:04  Bore 12     Uploaded 96 readings at a 15-minute interval05:30:05  Bore 12     Battery 3.62 V, charging from solar05:31:10  Tank 3      No response, retry 1 of 3 in 60 s05:32:10  Tank 3      Connected over LTE-M, signal -101 dBm05:32:13  Tank 3      Uploaded 12 readings05:32:14  North Pump  Alarm “Tank low” raised: 17.8 % is under the 20 % threshold05:32:15  North Pump  Webhook POST https://hooks.example.com/wildeye/alarms returned 422 Unprocessable Entity after 1.2 s
Plain text is never highlighted, so no log line outranks the next.

Single line

For one value someone will paste elsewhere.

Endpoint
https://hooks.example.com/wildeye/alarms
API key
hidden, ending 7c2e
Masked shows any prefix and the last four characters. Copy takes the whole value.

Long content

Responsejson
{  "serial": "WE-10442",  "name": "Bore 12",  "site": {    "id": "site_2291",    "name": "Emu Park"  },  "firmware": "4.2.1",  "online": true,  "battery_v": 3.62,  "signal_dbm": -87,  "interval_min": 15,  "last_seen": "2026-09-28T05:30:02+13:00",  "channels": [    {      "id": "soil_moisture_10cm",      "unit": "%",      "value": 34.2    },    {      "id": "soil_moisture_30cm",      "unit": "%",      "value": 31.4    },    {      "id": "soil_moisture_60cm",      "unit": "%",      "value": 28.9    },    {      "id": "line_pressure",      "unit": "kPa",      "value": 212.5    }  ],  "alarms": {    "active": 0,    "acknowledged": 1  },  "reseller": null}
“Show all 41 lines” says how long the block is.
Sync logplain text
05:30:02  Bore 12     Connected over LTE-M, signal -87 dBm05:30:04  Bore 12     Uploaded 96 readings at a 15-minute interval05:30:05  Bore 12     Battery 3.62 V, charging from solar05:31:10  Tank 3      No response, retry 1 of 3 in 60 s05:32:10  Tank 3      Connected over LTE-M, signal -101 dBm05:32:13  Tank 3      Uploaded 12 readings05:32:14  North Pump  Alarm “Tank low” raised: 17.8 % is under the 20 % threshold05:32:15  North Pump  Webhook POST https://hooks.example.com/wildeye/alarms returned 422 Unprocessable Entity after 1.2 s
wrapped
Sync logplain text
05:30:02  Bore 12     Connected over LTE-M, signal -87 dBm05:30:04  Bore 12     Uploaded 96 readings at a 15-minute interval05:30:05  Bore 12     Battery 3.62 V, charging from solar05:31:10  Tank 3      No response, retry 1 of 3 in 60 s05:32:10  Tank 3      Connected over LTE-M, signal -101 dBm05:32:13  Tank 3      Uploaded 12 readings05:32:14  North Pump  Alarm “Tank low” raised: 17.8 % is under the 20 % threshold05:32:15  North Pump  Webhook POST https://hooks.example.com/wildeye/alarms returned 422 Unprocessable Entity after 1.2 s
scrolling
Wrapped lines continue two characters in. Unwrapped, line numbers stay pinned.

States

Responsejson
No response yetThe body appears here after the first delivery.
empty
Request bodyjson
{  "id": "alm_7731",  "name": "Tank low",  "severity": "high",  "threshold": 20,  "value": 17.8,  "unit": "%"}
error line
Empty keeps its header and disables Copy. An error line takes a red tint and bar.

In context

Delivery failed422
Tank low · North Pump · 28 Sep 2026, 05:42 · attempt 2
Request bodyjson
{  "event": "alarm.triggered",  "id": "evt_01J8Z6Q4M2WQ",  "occurred_at": "2026-09-28T05:42:17+13:00",  "alarm": {    "id": "alm_7731",    "name": "Tank low",    "severity": "high",    "threshold": 20,    "value": 17.8,    "unit": "%"  },  "site": {    "id": "site_3107",    "name": "North Pump"  },  "logger": {    "serial": "WE-10877",    "name": "Tank 3"  },  "acknowledged": false,  "acknowledged_by": null}
Responsejson422
{  "error": "invalid_payload",  "field": "alarm.value",  "message": "Expected an integer, got 17.8",  "request_id": "req_5KX2P9"}
The 422 names alarm.value and the body marks that line. Retry swaps the response to 200.
API access
Credentials for your integrations.
API key
hidden, ending 7c2e
Webhook password
hidden, ending yH1d

Accessibility

Copy announces “Copied to clipboard”. A masked key reads as “hidden, ending 7c2e”.

KeyWhat it does
TabWrap, Copy, the code if it scrolls, then Show all.
Enter / SpaceRuns the focused button. Copy then reads Copied.
Arrow keysScroll the focused code.
Ctrl / ⌘ + ASelects only the code.
Ctrl / ⌘ + CCopies the block when nothing is selected.
EscapeHides a tooltip.

Props

PropTypeDefaultDescription
codestring | object | array | null—Objects and arrays print as JSON.
language'json' | 'xml' | 'text''text'Picks the highlighter.
titlestring—Names the block.
aria-labelstring—Names the block when there is no title.
#metaslot—A badge beside the title.
show-line-numbersbooleanfalsePinned gutter, never copied.
highlight-linesnumber[]—Lines to mark, from 1.
highlight-tone'accent' | 'error''accent'Blue or red mark.
max-heightnumber | null400Scrolls past this height.
collapsiblebooleanfalseCollapses above the threshold.
initially-collapsedbooleantrueIgnored if a marked line would hide.
collapse-thresholdnumber10Lines shown while collapsed.
v-model:wrapbooleanfalseWraps long lines, keeping indent.
wrap-togglebooleantrueShows the wrap button.
copyablebooleantrueShows Copy.
variant'block' | 'inline''block'Inline is one line.
maskedbooleanfalseInline only. Shows any prefix and last four.
reveal-labelstring'Show value'Inline only. Names the reveal toggle.
empty-textstring'Nothing to show yet'Empty state title.
empty-descriptionstring—Empty state detail.
@copy(text) => void—Fires with the copied text.

Tokens

TokenValueWhere it lands
--global-neutral-grey-200#F5F5F5The well.
--global-neutral-grey-500#E6E6E6Outline.
--global-neutral-grey-400#ECECEDHeader and footer rules.
--global-neutral-grey-1300#0A090BKeys, plain text.
--global-blue-1100#0C296AStrings, hovered footer label.
--global-violet-800#6E23C9Numbers, XML attributes.
--global-purple-900#7B238DLiterals, XML tags.
--global-neutral-grey-1000#4F4D55Punctuation, comments, line numbers.
--global-blue-1000#1877F2Focus, footer label, highlight bar.
--global-blue-100#EBFAFFHighlighted line, pressed toggle, footer hover.
--global-blue-200#D6F4FFText selection.
--global-red-100#FEEBEBError line tint.
--global-red-800#E12121Error line bar.
--radius-lg8pxCorners.
--font-family-monoJetBrains MonoCode, 13/20.

Progress bar

How far a job with a known end has got, or how full a reading is, with the figure always there as text.

Refined portVue source: WildProgressBar.vueStorybook story: Feedback/Progress BarReact component: WProgressBar

Try it

Move the level: the figure changes at once and the fill catches up.

Tank 2 · North Pump
Pump running · full in about 42 min.
Level
Size
Tone
Value
Layout
Striped
Indeterminate
<WildProgressBar
  label="Tank 2 · North Pump"
  :value="12400"
  :max="20000"
  unit="L"
  show-value="percent"
  size="md"
  help="Pump running · full in about 42 min."
/>
On auto, the tank turns to warning under 40% and error under 20%.

When to use

  • Use it for a job with a known end, such as an upload.
  • Use it inline, on auto tone, for a reading in a list.
Don’t use it

Anatomy

Label, value, track and fill, and a help line. Only the track is required.

Backfilling readings · Bore 12
Catching up after 10 days offline.
Soil 30 cm
Soil 60 cm
Soil 90 cm
Inline gives the value at least 52px, so a column of percentages ends on one edge.

Sizes

Four heights. Label, value and help stay 12px at every size.

xs · 4px
File rows
sm · 6px
Table cells
md · 8px
With a label
lg · 12px
A card’s one reading

Tones

Tone says what the work is doing.

Syncing readings · Emu Park
Backfill complete · Bore 12
Uploading logs · North Pump
Signal is weak. It will finish, just more slowly.
Firmware upload · Bore 12
Connection lost at 71%. Try again when Bore 12 is back online.
Paused · Tank sensor 2
Resumes when the logger next checks in.
Give warning and neutral a help line saying what’s wrong.

Auto tone

For readings. Two thresholds, as percentages of the range, set the colour.

Bore 12
North Pump
Weather stn 4
battery · warning 40, error 20
Readings
Logs
Exports
storage · warning 70, error 90
Error below warning means higher is better. Error above warning means higher is worse.

Value

Percent, fraction or your own words. A screen reader hears the same words.

Firmware
percent
Firmware
fraction
Firmware
value-text
Import
0.4 of 100
Import
99.6 of 100
Import
100 of 100
It shows 99% until the end, and at least 1% once started.

States

Queued, moving, waiting and done.

tank-sensor-2.csv
Starts after bore-12.csv.
queued
Importing 1,240 readings
About a minute left.
striped
Waiting for Bore 12
Starts when Bore 12 next checks in.
indeterminate
Import complete
1,240 readings added to Bore 12.
complete
Stripes drift while a job runs. Under reduced motion, indeterminate holds still as a hatch.

In context

Firmware 4.2.1
Bore 12 · Emu Park
Update available
Ready to send
Bore 12 is on 4.1.8.
The badge is a polite live region, so a screen reader hears only the phase changes.
Battery
4 loggers · sorted by site
Bore 12
Emu Park · 4 min ago
North Pump
Emu Park · 11 min ago
Weather station 4
Kinross · 2 h ago
Tank sensor 2
Kinross · 6 min ago
Storage
Storage used
Warning from 70%, error from 90%.

Content

  • Name the work and its subject: Sending to Bore 12.
  • Use help for what happens next, such as time left.
  • Give an error its cause and the fix.

Accessibility

A screen reader hears the label and the value in words, even when the value is hidden. Indeterminate has no value. An error is announced when it appears.

KeyWhat it does
TabPasses over the bar. It never takes focus.

Props

PropTypeDefaultDescription
valuenumber—Progress, in your own units.
minnumber0Start of the range.
maxnumber100End of the range.
labelstring—Visible and accessible name.
show-value'percent' | 'fraction' | falsefalseWhat the header shows.
value-textstring—Your own words. Wins over show-value.
unitstring—Appended to the fraction.
size'xs' | 'sm' | 'md' | 'lg''md'4, 6, 8 or 12px tall.
tone'brand' | 'success' | 'warning' | 'error' | 'neutral' | 'auto''brand'Auto colours by thresholds.
thresholds{ warning, error }{ warning: 80, error: 40 }Percentages of the range.
layout'stacked' | 'inline''stacked'Header above, or all on one line.
helpstring—The line under the bar.
errorstring—Turns it red; replaces help.
stripedbooleanfalseMoving stripes. md and lg only.
animatedbooleantrueFill-in, eased changes and moving stripes.
indeterminatebooleanfalseA wait with no figure.
aria-labelstring—Name when there’s no label.

Tokens

TokenValueWhere it lands
--global-blue-1000#1877F2Brand fill.
--global-neutral-grey-300#F1F1F1The track.
--global-green-800#019939Success fill.
--global-green-900#018030Complete value and tick.
--global-squash-1000#9D6508Warning fill, value and icon.
--global-red-800#E12121Error fill, value and message.
--global-neutral-grey-900#7F7D83Neutral fill.
--global-neutral-grey-1000#4F4D55Label and help line.
--global-neutral-grey-1300#0A090BValue.
--duration-slowest500msFill-in on mount.
--duration-moderate200msEvery change after that.

Progress tracker

Shows where people are in a multi-step flow, with finished steps ticked and the current one marked.

Refined portVue source: WildProgressTracker.vueReact component: WProgressTracker, ProgressStep, ProgressTrackerSize

Try it

Step through with Next and Back. Turn on Clickable, then click a finished step to go back to it.

  1. Site, completedName and location
  2. LoggerSerial number
  3. Inputs, not startedWhat it measures
  4. Review, not startedCheck and save
Orientation
Size
Descriptions
Numbers
Clickable
<WildProgressTracker
  :steps="steps"
  :current-step="1"
  aria-label="New site"
/>

When to use

  • Use it above a flow of three to six steps, such as setting up a site.
  • Use it when people need to know how much is left before they commit.
  • Make finished steps clickable when going back to edit is safe.
Don’t use it

Orientation

  1. Site, completed
  2. Logger, completed
  3. Inputs
  4. Review, not started
horizontal
  1. Site, completedName and location
  2. Logger, completedSerial number
  3. InputsWhat it measures
  4. Review, not startedCheck and save
vertical
Use vertical in a side panel, or when descriptions run past a few words.

Sizes

Circles of 24, 32 and 40px. The default is md.

  1. Site, completed
  2. Logger
  3. Inputs, not started
  4. Review, not started
sm
  1. Site, completed
  2. Logger
  3. Inputs, not started
  4. Review, not started
md
  1. Site, completed
  2. Logger
  3. Inputs, not started
  4. Review, not started
lg

States

  1. Site
  2. Logger, not started
  3. Inputs, not started
  4. Review, not started
first step
  1. Site, completed
  2. Logger, completed
  3. Inputs
  4. Review, not started
in progress
  1. Site, completed
  2. Logger, completed
  3. Inputs, completed
  4. Review, completed
all done
  1. Site, completed
  2. Logger
  3. Inputs, not started
  4. Review, not started
numbers off
Set currentStep to the step count once it’s saved, so every step shows a tick. With numbers off, the current circle holds a dot.

Content

  • Name each step with a noun of one or two words: Site, Logger, Review.
  • Keep descriptions to a few words saying what the step asks for.
  • End on the step where people check and save.

Accessibility

Give it an aria-label that names the flow. Screen readers hear each step as current, completed or not started.

KeyWhat it does
TabMoves to the next clickable finished step.
Enter / SpaceGoes back to that step, and focus moves with it.

Props

PropTypeDefaultDescription
steps(string | { label, description? })[]—The steps, in order.
currentStepnumber00-based. Equal to the step count when all are done.
showNumbersbooleantrueNumbers in the circles of unfinished steps.
size'sm' | 'md' | 'lg''md'Circle of 24, 32 or 40px.
orientation'horizontal' | 'vertical''horizontal'Row across the top, or a column.
clickablebooleanfalseFinished steps become buttons.
@step-click(index: number) => void—Emitted when a finished step is chosen.
aria-labelstring'Progress'Names the flow for screen readers.

Tokens

TokenValueWhere it lands
--global-blue-900#00AEEFFinished and current circles, lines, current halo.
--global-neutral-grey-300#F1F1F1Upcoming circle fill.
--global-neutral-grey-600#DCDCDEUpcoming lines.
--global-neutral-grey-1100#2D2B32Finished step label, upcoming number.
--global-neutral-grey-1000#4F4D55Upcoming labels, descriptions.
--global-blue-1000#1877F2Current step label, focus ring.
--global-base-white#FFFFFFNumber, tick and dot on cyan; gap inside the focus ring.