Tidewatch design system · icons v1.0

Eighteen icons for a coastal station

Organic, mechanical and interface forms drawn on one 24 px grid, at one stroke weight, with one set of terminals — so a swell, an anemometer and an export arrow can sit in the same 24 px row without one of them looking borrowed.

18 icons 20 symbols 24×24 grid stroke 1.5 sprite —

The row test

All eighteen at 24 px, one line

The only test that matters. Same optical weight across organic, mechanical and abstract; same terminals; nothing heavier or lighter than its neighbour; every icon centred on the same rule. The same sprite is doing all three rows — one drawing, two polarities, two sizes.

24 px · surface
24 px · inverted, no second copy
16 px · minimum size

Construction

The rules the eighteen were drawn to

Stated, not implied. Anything that looks wrong when it is mathematically right gets corrected optically, and the correction is written down.

Artboard
24 × 24 viewBox. No transforms on geometry; coordinates to two decimal places.
Live area
20 × 20, inset 2. Every stroke centreline sits inside it, so round caps still leave ≥ 1.25 clear of the edge and nothing clips.
Keylines
square 18; circle ø19 (+5.6 % so it reads the same size as the square); portrait 14 × 20; landscape 20 × 14; triangle base 18.5 (triangles lose area at the tips).
Stroke
1.5 at 24 px — 1:16 of the artboard. Inherited from CSS, never set in the sprite, so a host can run 1.25 or 2 (try the slider above).
Terminals
Round cap, round join, everywhere. No mitres, no butts, no exceptions — star tips and funnel corners take the join radius and nothing else.
Corners
r2 on large forms, r1 on details.
Density
2–4 marks per icon; at most three stacked lines; ≥ 2 units of clear space between parallel strokes (3.5 centre to centre).
Shared parts
One wave primitive #tw-wave (5 commands) instanced by four icons on a single y offset. One chevron head — 2.5 legs at 45° — in current, export, compare and history. Scale dots are zero-length round-cap subpaths, so they track stroke-width instead of freezing as filled circles.
Node budget
24 path commands per icon. Largest here: —.
Colour
currentColor only. Fill is off; the one filled state re-uses the same symbol via .i--fill.
Circle keyline ø19. Terminator is one elliptical arc, rx 6.
Square keyline 18. Funnel mouth sits on it exactly.
Portrait keyline 14 × 20. Mast on centre, cups on a 5.5 radius.

The set

Three families, one hand

Water and sky are drawn with curves off the shared wave and one circle radius; instruments are drawn with a mast, a plumb line and graduation ticks; interface marks are drawn with the same chevron head and the same corner radii. Nothing in the third group is allowed a weight the first group cannot match.

Scale

16 px to 96 px, honestly

One drawing per icon at every size — the stroke scales as a ratio of the artboard, so 96 px is a 6 px stroke and 16 px is a 1 px stroke. Two icons outrun 16 px and ship a second form; the rest do not need one.

The anemometer’s hub-to-cup gaps (1.4 units) and the rainfall’s three-drop rhythm (3.5 units) are the only places the set closes up below 20 px. The 16 px forms drop the hub and one drop, widen what is left, and keep the same skeleton so the swap is invisible in a row.

In context

A station panel, using seventeen of the eighteen

Icons earn their keep at 18–24 px next to type, in buttons, in a warning band and in a metadata strip — where optical weight against 13 px text matters more than how the icon looks alone on a plate.

Whitstable Street
Buoy 4207 · 51.37° N 1.03° E · 14:06 BST
1.8 m
Swell Hs
24 kt
Wind · gust 31
1012.4
hPa · falling
14.2 °C
Sea temperature
Tide 3.1 m, rising
High
14:42 · 4.8 m

Swell above threshold. 2.5 m exceeded 13:00–16:00 — mooring inspection advised.

Waxing gibbous 05:12 · 20:44 SW 220° Flood 1.4 kt 0.2 mm/h 12 berths free

Fifteen decorative instances, two icon-only controls named on the <button>, one named graphic. The favourite toggle is the same symbol filled by CSS — click it.

Accessibility, as shipped

  • Decorative — aria-hidden="true" on the host <svg>. The label is already in the adjacent text, and the cloned <title> is not announced twice.
  • Meaningful — role="img" plus aria-label, which overrides the symbol title. Used for the warning mark, which carries severity that the sentence does not repeat.
  • Icon-only controls — the accessible name lives on the button, the graphic is hidden, and the hit area is 34 px. State is aria-pressed, not colour alone: the star also fills.
  • Every symbol has a <title>, so the sprite is self-documenting when opened on its own.
  • Contrast follows text — icons are currentColor, so they inherit whatever ratio the surrounding type already passes, on either polarity.

Usage

<!-- 1 · paste the sprite once, first thing in <body> -->
<svg id="sprite" aria-hidden="true">
  <defs><path id="tw-wave" d="M3 0c1.5-1.67 3-1.67 4.5 0s3 1.67…"/></defs>
  <symbol id="tw-swell" viewBox="0 0 24 24">
    <title>Swell / sea state</title>
    <use href="#tw-wave" y="6.5"/><use href="#tw-wave" y="12"/>
    <use href="#tw-wave" y="17.5"/>
  </symbol>
</svg>

/* 2 · one rule styles the whole set */
.i   { width:var(--s,24px); height:var(--s,24px); fill:none;
       stroke:currentColor; stroke-width:var(--sw,1.5);
       stroke-linecap:round; stroke-linejoin:round }
.i--fill { fill:currentColor }          /* filled state, same symbol */
.tools .i { --s:18px; --sw:1.5 }        /* size + weight from CSS   */

<!-- 3 · decorative: text already carries the label -->
<svg class="i" aria-hidden="true"><use href="#tw-swell"/></svg> 1.8 m

<!-- 4 · meaningful: name the graphic -->
<svg class="i" role="img" aria-label="Warning"><use href="#tw-alert"/></svg>

<!-- 5 · icon-only control: name the button, hide the graphic -->
<button aria-label="Favourite this station" aria-pressed="true">
  <svg class="i i--fill" aria-hidden="true"><use href="#tw-favourite"/></svg>
</button>