Foundation

Colour

The colour tokens: one brand cyan, a neutral ramp for text and surfaces, and three status families.

Live React portSource file: src/assets/css/tokens.cssStorybook story: Foundation/Colors

When to use

  • Use cyan, --color-primary, for fills and large marks.
  • Use blue-1000, --color-primary-active, for blue text, icons, thin lines and the focus ring. On the blue-100 tint, use blue-1100.
  • For meaning, use a SEMANTIC token, never a ramp step. Put a tone’s text on its own surface.
Don’t use it
  • Cyan for text, icons or thin lines on white. Use blue-1000.
  • Indigo or --color-info for blue status. Use the info tone.

Brand

The primary cyan and its four states, all on the blue ramp. Pressed is blue-1000, #1877F2, one step deeper on the same ramp.

#00AEEF
2.53 : 1
--color-primary
#0ABEFF
2.14 : 1
--color-primary
-hover
#1877F2
4.23 : 1
--color-primary
-active
#EBFAFF
1.07 : 1
--color-primary
-light
#0C296A
13.63 : 1
--color-primary
-dark

Neutrals

Thirteen steps. 50–200 are surfaces, 400–600 are borders, and 900 and darker are text.

--color-grey-50--color-grey-1200
--color-grey-900#4F4D55hsl(255 5% 32%)8.32 : 1AAAon white2.39 : 1Failon ink

Hover or select a step to read its value and contrast.

Status tones

Six tones, in order of severity, each with seven roles. A tone’s text and icon pass on its own surface. onFill is for text or an icon on the solid fill.

Success
Done, healthy, in range
fill
onFill
surface
border
icon
text
textStrong
Warning
Marginal, worth watching
fill
onFill
surface
border
icon
text
textStrong
Attention
Someone needs to act
fill
onFill
surface
border
icon
text
textStrong
Error
Failed, unsafe, out of range
fill
onFill
surface
border
icon
text
textStrong
Info
Running now, or news
fill
onFill
surface
border
icon
text
textStrong
Neutral
Idle, off, no status
fill
onFill
surface
border
icon
text
textStrong
The Vue library’s --color-success, --color-warning and --color-error are the fill role here.

Roles

Every other colour role, named by job. Hover a swatch for its value.

Text
primary
secondary
tertiary
disabled
placeholder
inverse
onPrimary
link
linkHover
linkInverse
Icon
primary
secondary
disabled
inverse
interactive
Surface
page
card
raised
overlay
inset
hover
pressed
selected
disabled
skeleton
inverse
Border
divider
field
strong
fieldHover
control
disabled
focus
error
Interactive
primary
primaryHover
primaryPressed
secondary
secondaryHover
secondaryPressed
ghostHover
ghostPressed
danger
dangerHover
dangerPressed
selected
selectedText
disabled
Chart
series1
series2
series3
series4
series5
grid
axis
axisLabel
thresholdWarning
thresholdCritical
Brand
primary
surface
Plus surface.scrim, grey-1300 at 50%, behind dialogs and drawers.

Where they're used

  • Dashboard widgets: the status panel, badges and freshness read SEMANTIC.status, under their widget names (ok, note, act, bad, live, idle).
  • Mobile: the same tokens. Screens import them, so a phone and the dashboard can’t drift apart.
  • Controls: alerts, field errors and toasts take a tone’s surface, border and text.

Accessibility

KeyWhat it does
TabMoves focus to the selected grey step.
Right / DownSelects the next, darker step.
Left / UpSelects the previous, lighter step.
Home / EndSelects the lightest or darkest step.

Palettes

The twenty-three global ramps that every semantic colour token is cut from.

Live React portSource file: src/assets/css/global.css

Solid families

Eighteen families at thirteen steps each: fourteen hues, then four greys. Every colour a component paints is a step on one of these ramps. Indigo ships here, but no component uses it: blue roles use the blue ramp.

red
orange
squash
forest
green
pool-blue
teal
blue
indigo
ultramarine
violet
purple
magenta
maroon
neutral-grey
flat-grey
cool-grey
bright-grey
10013 steps1300
--global-red-900#BC1C1Chsl(0 74% 42%)6.32 : 1AAon white3.14 : 1AA Largeon ink

Hover or tab to a cell to read its token, value and contrast.

Semantic mapping

The primary tokens and the global steps they come from.

Semantic tokenGlobal stepValue
--color-primary--global-blue-900#00AEEF
--color-primary-hover--global-blue-800#0ABEFF
--color-primary-active--global-blue-1000#1877F2
--color-primary-dark--global-blue-1100#0C296A

The blue ramp holds one hue from 100 to 1300. Pressed, 1000, is a deeper cyan-blue rather than a different blue, so a pressed primary darkens without changing hue. It’s also the first step to pass 4.5:1 on white.

Alpha families

Five ramps in rgba(), for scrims, hover washes and anything that sits over a map, chart or photo.

neutral-grey-alpha
flat-grey-alpha
cool-grey-alpha
bright-grey-alpha
alpha-white
10013 steps1300
--global-neutral-grey-alpha-900rgba(6, 0, 11, 0.48)alpha — no fixed ratio

Usage

  • Reach for a semantic token on Colour first. Use a global step only when none fits.
  • Use an alpha family over a map, chart or photo, where a solid fill would hide what’s underneath.
  • Check an alpha colour’s contrast in place, because it depends on what’s behind it.

Typography

Inter at twelve sizes and five weights, tuned for dense tables of readings.

Live React portSource file: src/assets/css/tokens.css

Scale

Twelve sizes, from 9 to 36px, all in Inter (--font-family-base). Half of them sit under 14px.

--font-size-5xl · 36pxSoil moisture 300 mm
--font-size-4xl · 30pxSoil moisture 300 mm
--font-size-3xl · 24pxSoil moisture 300 mm
--font-size-2xl · 20pxSoil moisture 300 mm
--font-size-xl · 18pxSoil moisture 300 mm
--font-size-lg · 16pxSoil moisture 300 mm
--font-size-base · 14pxSoil moisture 300 mm
--font-size-md · 13pxSoil moisture 300 mm
--font-size-sm · 12pxSoil moisture 300 mm
--font-size-xs · 11pxSoil moisture 300 mm
--font-size-2xs · 10pxSoil moisture 300 mm
--font-size-3xs · 9pxSoil moisture 300 mm

Weights

Five weights. The names run one step light: semibold is 500 and bold is 600.

Ag
400 · normal
Bore 12 · 4.2 m
Ag
475 · medium
Bore 12 · 4.2 m
Ag
500 · semibold
Bore 12 · 4.2 m
Ag
600 · bold
Bore 12 · 4.2 m
Ag
700 · extrabold
Bore 12 · 4.2 m
475 is the house medium. It stays open at 11px, where 500 fills in, and holds a row apart better than 400.

Roles

The size and weight each part of the interface uses.

RoleSizeWeight
Metrics tile figure5xl · 36700
Gauge reading3xl–4xl · 24–30600
Inline edit title2xl · 20500
Dialog and empty-state titlelg · 16600
Body text, checkbox and radio labelsbase · 14400
Button labelbase · 14475
Field valuebase · 14500; placeholder 400
Table cellmd · 13400; numbers 475
Table header, field label, badgesm · 12475
Helper and error textxs · 11400
Tooltip2xs · 10500
Menu group label2xs · 10600, capitals

Usage

  • Use 14px for body text, and 13px inside tables.
  • Set numbers in a table at 475 with tabular figures, so the columns line up.
  • Use 475 (--font-weight-medium) for button labels, field labels, badges and table headers.

Spacing

A 4px scale for padding, gaps and layout, in twenty-two steps.

Live React portSource file: src/assets/css/tokens.cssStorybook story: Foundation/Spacing

Scale

Twenty-two steps on a 4px base. They run in fine steps up to 48, then widen to 56, 64, 80, 96, 112 and 128.

--spacing-0-5
2
--spacing-1
4
--spacing-1-5
6
--spacing-2
8
--spacing-2-5
10
--spacing-3
12
--spacing-3-5
14
--spacing-4
16
--spacing-5
20
--spacing-6
24
--spacing-7
28
--spacing-8
32
--spacing-9
36
--spacing-10
40
--spacing-11
44
--spacing-12
48
--spacing-14
56
--spacing-16
64
--spacing-20
80
--spacing-24
96
--spacing-28
112
--spacing-32
128
The grid behind each bar is 4px.

Usage

  • Use --spacing-2 (8px) between adjacent controls in a toolbar.
  • Use --spacing-3 (12px) between rows inside a card.
  • Use --spacing-4 (16px) for card padding.
  • Use --spacing-6 (24px) for the page gutter and between sections.
  • Use the half-steps (2, 6, 10 and 14px) for padding and gaps inside small controls such as buttons and badges.
  • Keep steps up to 48 for components, and the wider steps for page layout.
  • Don’t use values between steps. For 52px, pick 48 or 56.

Radius

Corner radii for each kind of surface, from 6px buttons to fully round pills.

Live React portSource file: src/assets/css/tokens.css

Scale

Eight values, from square to pill.

none · 0
sm · 4px
md · 6px
lg · 8px
xl · 10px
2xl · 12px
3xl · 16px
full · 9999px

In components

Each kind of object has its own radius. The bigger the box, the bigger the corner.

button · 6
Online
badge · 6
card · 8
Bore 12
field · 10
dialog · 16
Bore 12
chip · 24

Usage

  • Use --radius-sm (4px) for checkboxes and skeleton text.
  • Use --radius-md (6px) for buttons, badges, icon buttons and menu rows.
  • Use --radius-lg (8px) for cards, selects, chip inputs, dropdowns and alerts.
  • Use --radius-xl (10px) for the text input.
  • Use --radius-3xl (16px) for dialogs.
  • Use --radius-full for radios, toggles, avatars and presence dots.
  • Use 24px for chips. It has no token.

Elevation

Shadows that separate cards, menus and dialogs from the page beneath them.

Live React portSource file: src/assets/css/tokens.cssStorybook story: Foundation/Shadows

Scale

Seven steps, each tinted with the system’s black, #0A090B, so shadows stay warm against the greys.

xs
sm
md
lg
xl
2xl
inner

Component shadows

Four shadows, each named for the component it sits under.

input
card
dropdown
tooltip
--shadow-card has three layers, and the third is 0 0 0 1px. It draws the card’s edge without a border, so the edge adds nothing to the layout.
--shadow-input
0 1px 2px 0 rgba(10, 9, 11, 0.05)
--shadow-card
0 2px 12px -1px rgba(10, 9, 11, 0.10)
0 2px 2px -1px rgba(10, 9, 11, 0.04)
0 0 0 1px rgba(10, 9, 11, 0.05) ← the hairline
--shadow-dropdown
0 4px 12px rgba(0, 0, 0, 0.08)
0 2px 4px rgba(0, 0, 0, 0.04)
--shadow-tooltip
0 2px 8px rgba(0, 0, 0, 0.15)

Focus rings

button
Bore 12
chip
Bore 12
field
Every focus ring is a box-shadow, so focus never moves the layout.
TokenValueWhere it lands
(no token)2px gap, 2px ringButtons.
--shadow-button-focus2px white + 2px #1877F2Chips, and small buttons inside fields. Was 0 0 0 3px rgba(0,174,239,.3).
(no token)2px #1877F2 border, 3px halo at 6%Text fields. The border carries focus; the halo is decoration.

Usage

  • Don’t add a border to a surface that has --shadow-card. Its third layer already draws the edge.
  • Use --shadow-dropdown for menus and dropdowns.

Iconography

Bespoke glyphs for agricultural measurements, with Heroicons for general interface actions.

Live React portSource file: icons/heroicons.tsStorybook story: Foundation/IconReact component: Glyph, WGlyph

Try it

Switch sets, change the size, and turn off Animate to see each glyph’s resting frame.

windspeed
wind-direction
temperature
pressure
humidity
dew-point
solar-radiation
fire-index
rainfall
water-flow
irrigation-spray
flow-total
tank-level
water-temperature
soil-moisture
soil-temperature
leaf-wetness
evapotranspiration
conductivity
battery
signal
schedule
Set
Size
Animate
<script src="/widgets/icons-core.js"></script>
<link rel="stylesheet" href="/widgets/motion.css">

<Glyph name="evapotranspiration" size={24}
/>
Each glyph is one SVG on a 96×96 canvas, so the same file works at every size.

Measurement glyphs

Forty-seven glyphs, one for each thing a sensor measures. Each one moves, and the movement shows the reading.

Air & weather
windspeed
wind-direction
temperature
pressure
humidity
dew-point
solar-radiation
fire-index
frost
Water
rainfall
water-flow
irrigation-spray
flow-total
water-pressure
water-temperature
water-quality
effluent
weir
Level & storage
tank-level
reservoir
river
silo
trough
fuel
fertilizer
Soil & plant
soil-moisture
soil-temperature
leaf-wetness
evapotranspiration
conductivity
dendrometer
Power, control & movement
battery
power
signal
schedule
engine
pump
valve
tilt
movement
piezometer
Stations & assets
weather-station
soil-probe
rain-gauge
fridge
gps
generic
rainfallfalls
windspeedsways
temperaturerises
pressureswings
signalfills
batteryglows
water-flowspins
irrigation-spraydrips
evapotranspirationlifts
fire-indexflickers
Each glyph carries its own motion, so it animates wherever you place it.

Interface icons

Everything that isn’t a measurement uses Heroicons from the solid set, at 24×24: navigation, settings and alerts.

Graphs
Sites
Groups
Users
Map View
Clients
Operations
Settings
Log out
bell
edit
alertTriangle
shield
key

Sizes

Five sizes, shared by both sets.

xs · 14
sm · 16
md · 20
lg · 24
xl · 32

Usage

  • Use a measurement glyph to show what a sensor measures.
  • Use a solid Heroicon for navigation, settings and alerts.
  • Size both sets with the five icon tokens. Don’t use sizes between them.

Accessibility

A glyph has no text of its own, so show the measurement’s name beside it. With reduced motion on, every glyph holds a resting frame that still shows the reading.

Soil moisture
reading, labelled

Tokens

TokenValueWhere it lands
--size-icon-xs14pxInline with 11px table text.
--size-icon-sm16pxInside a button or field. The default.
--size-icon-md20pxCard headers.
--size-icon-lg24pxSidebar and dialog headers.
--size-icon-xl32pxEmpty states.
--size-icon-button24pxIcon button hit area.

Motion

Durations and easings for every transition, so the whole product moves at one pace.

Live React port

Try it

Pick a duration, a curve and a property. Changing any control replays the move.

150ms · --ease-out--duration-normal
transform
Duration
Ease
Property
Delay
.marker {
  transition: transform var(--duration-normal) var(--ease-out);
}
Delay waits --duration-slower before the move starts.

Durations

Seven steps, from 50 to 500ms. Most of the product moves at 100 or 150ms.

--duration-instant
50ms
--duration-fast
100ms
--duration-normal
150ms
--duration-moderate
200ms
--duration-slow
300ms
--duration-slower
400ms
--duration-slowest
500ms
All seven run on --ease-in-out, so only the duration differs.
1150ms · easeOutCubicThe gauge the product ships.
500ms · --ease-outThe ceiling, on the same arc.
Gauges sweep over 1150ms, past the top of the scale, because a reading is arriving rather than a control responding.

Easings

Six curves. Press Replay to run a dot along each one.

Duration
Slowed down so each curve’s shape shows.
linear
linear
cubic-bezier(0.4, 0, 1, 1)
in
cubic-bezier(0, 0, 0.2, 1)
out
cubic-bezier(0.4, 0, 0.2, 1)
in-out
cubic-bezier(0.34, 1.56, 0.64, 1)
spring
cubic-bezier(0.68, -0.55, 0.265, 1.55)
bounce
Spring and bounce overshoot the end. Bounce also dips below the start.

Composed transitions

Four tokens are whole transition values, each on --duration-normal.

Loops every 2s, slowed to 450ms.
--transition-transformtransform only
Save
Duplicate
Delete
--transition-allevery property, layout included
Save
Duplicate
Delete
/* Same duration. Only one of them moves the rest of the row. */
.save        { transition: var(--transition-transform); }
.save--lazy  { transition: var(--transition-all); }

/* --transition-all: all var(--duration-normal) var(--ease-in-out)
   and `all` includes width, padding and radius — every property
   that forces the elements beside it to be laid out again. */
--transition-all animates width too, so the buttons beside it move. --transition-transform leaves them still.

Usage

  • Use --duration-normal (150ms) for most transitions, and --duration-fast (100ms) for hover colour changes.
  • Keep anything a person acts on at 200ms or less. Use 400ms and 500ms only for skeletons and page transitions.
  • Use --ease-out for things entering, and --ease-in-out for everything else.
  • Use a composed token instead of listing properties, so every component animates the same ones.
  • Keep --ease-bounce for a button’s success and error pops.
  • Don’t use --transition-all. It animates layout, so the things around it move.
  • With reduced motion on, skip the move and show the end state.

Tokens

TokenValueWhere it lands
--duration-instant50ms—
--duration-fast100msHover colour and background changes.
--duration-normal150msThe default. Most component transitions.
--duration-moderate200msDisclosures and dropdowns opening.
--duration-slow300ms—
--duration-slower400msSkeletons and page transitions.
--duration-slowest500msSkeletons and page transitions.
--ease-linearlinear—
--ease-incubic-bezier(.4,0,1,1)—
--ease-outcubic-bezier(0,0,.2,1)Anything entering.
--ease-in-outcubic-bezier(.4,0,.2,1)The default curve.
--ease-springcubic-bezier(.34,1.56,.64,1)Not in use.
--ease-bouncecubic-bezier(.68,-.55,.265,1.55)Button success and error pops.
--transition-colorscolor, background, border · 150msText, fill and border colour.
--transition-opacityopacity · 150msOpacity only.
--transition-transformtransform · 150ms · ease-outTransform only. The one on ease-out.
--transition-allall · 150msEvery property, layout included. Avoid.
(no token)1150ms · easeOutCubicGauge sweep.
(no token)1800msCompass needle settling onto its bearing.