Virtual Lab Design System

The visual layer behind every Galileo investigation. A creamy lab-bench surface, hard-offset shadows, Space Grotesk display type for instructions, IBM Plex Sans for body, IBM Plex Mono for labels and numeric readouts. Every value on this page is read live from the kit — tokens from galileo_viewer_kit/css/tokens.css, atoms from css/atoms/*.css, handlers from js/handlers/**/*.css, and widgets directly from the <lab-widget-*> custom elements.

40 device widgets · 10 UI components · 3 type families · 4 reviewer rounds shipped

Where to start

Six entry points into the lab's UI vocabulary. The first three are what you'll touch every day; the last three back them with the typography, spacing, and palette they're built from.

Three rules that explain most of the system

Convention What it does Where you see it
Two type families Plex Sans for chrome, Space Grotesk for narrative voice, Plex Mono for numeric readouts. Three voices kept strictly apart so each role reads at a glance. Buttons · step instructions · value readouts
Hard offset shadows No blur. Every elevated surface uses a 2–6 px solid offset (2px 2px 0, 5px 5px 0, 6px 6px 0) that reads as ink-print, not glass. Step bar · widget bar · completion screen
Tight rectangles --radius-full: 2px is the default for buttons and bars; true circles use --radius-pill: 9999px. Chosen to look stamped, not glassy. Buttons · widget bar · step-bar phase chip

Typography

Three families. IBM Plex Sans carries the chrome — buttons, counters, captions, body copy. Space Grotesk carries narrative voice — step instructions, modal titles, the value displays inside widget bars. IBM Plex Mono carries data — numeric readouts, unit labels, the phase chip.

IBM Plex Sans — --font-family

UI default. All buttons, counters, labels, body copy. 400 / 500 / 600 / 700 / 800 weights are loaded.

The quick brown fox jumps over the lazy dog
0123456789 · ABCDEFGHIJKLM · abcdefghijklm

Space Grotesk — --font-display

Display + readout. Step-bar instructions (24 / 700), value displays (38 / 600 with tabular numbers), pause-stamp pills (18 / 700). Optical-size axis tuned via opsz.

Drag the beaker onto the hot plate
142.5 g

Six steps from caption to display

Used directly as var(--font-size-*). Below each row shows the resolved px value plus a sample at that size. Both --font-size-* values resolve from tokens.css at load time.

--font-size-xs
Caption / counter / chip
--font-size-sm
Body small / button label
--font-size-md
Default body
--font-size-lg
Card heading
--font-size-xl
Step instruction (Space Grotesk 600)
--font-size-2xl
Section title
--font-size-3xl
Display
Aa Regular
--font-weight-regular · 400
Aa Medium
--font-weight-medium · 500
Aa Semibold
--font-weight-semibold · 600
Aa Bold
--font-weight-bold · 700

Spacing

The lab's rhythm. Six steps from 8 px to 32 px built on 0.5 rem increments. Use the tokens — never literal pixel values — so screen-density and accessibility tweaks ripple through the whole UI from one edit. Each row reads its width from var(--space-*) directly, so the bars stay accurate if the scale ever shifts.

Six tokens, one rhythm

The widget bar's internal gap is --space-md. Card padding is --space-xl. Step-bar gap between progress and header is --space-xs. Don't reach for literals — extend the scale instead.

--space-xs
--space-sm
--space-md
--space-lg
--space-xl
--space-2xl

Where each step lands in the wild

Token Typical use Where you'll see it
--space-xsInline, tightStep-bar header gap, completed-pill badge gap
--space-smCompact groupToggle-group internal gap, range-input thumb gap
--space-mdDefaultWidget-bar inter-control gap, time-control gap
--space-lgLoose groupScale-bar value-pill gap, completion-screen icon→title
--space-xlCard gutterModal padding, card margins, section breathing room
--space-2xlPage gutterCatalog page padding, landing-screen vertical rhythm

Hand-sized chrome heights

A few non-token sizes are baked into the system because they're physical-feeling constants — students press them with fingers (eventually on tablet) and they need to feel uniform across handlers. These come from the actual atom + handler CSS, not from --space-*.

Widget bar 56 px
Widget button 28 px
Widget button (small) 26 px
Time-bar inner control 32 × 44 px

Color

A creamy lab bench, a sticky-note yellow chrome, near-black ink, and a single semantic green for "go." Every fill below is background: var(--token) — the displayed hex is read live from getComputedStyle at page load.

Lab bench & ink

--color-bg
--color-surface
--color-surface-hover
--color-text
--color-text-secondary
--color-text-tertiary

Yellow stick & "go" green

--color-yellow-sticky
--color-yellow-hover
--color-success
--color-success-hover
--color-success-border
--widget-bar-color

Status & alert

--color-warning
--color-error
--color-highlight
--color-highlight-light

Greys

--color-gray-100
--color-gray-600
--color-gray-700

Radii & shadows

Tight rectangles by default, true circles only when intended. Hard-offset shadows — never blurred — so every elevated surface reads as ink-stamped paper.

Five steps

--radius-full: 2 px is the system default. K12 uses "full" for a tight rectangle (rubber-stamp aesthetic) — true circles use the wrapper-only --radius-pill.

--radius-sm
4 px
--radius-md
6 px
--radius-lg
8 px
--radius-full
2 px (default)
--radius-pill
9999 px

Hard-offset library

No blur. Every shadow uses solid offsets in pixels — that's the look. The widget bar and step bar have the most pronounced ones (5–6 px) so they hover above the canvas.

--shadow-button
--shadow-panel
--shadow-panel-hover
--shadow-hard
--widget-bar-shadow

Two transition tokens

--transition-fast

150ms ease-out

Hover, press, toggle. Buttons + indicator dots + step-bar segment fills.

--transition-normal

250ms ease-out

Larger surface entrances (modal in, completion fade), settle animations.

Atoms

Every button, pill, indicator, toggle, and range primitive applied here uses the same class names handlers and widgets reach for. Sources: galileo_viewer_kit/css/atoms/button.css + pill.css for the global stamps; js/widgets/_base/widget-base-styles.js and js/handlers/widget/widget-bar.css for the widget-button family that lives with its surface.

.widget-btn — green stamped chip, the workhorse

The primary affordance inside every widget bar (Tare, Start, Ignite, Launch…). Green-success fill with a 2 px hard-offset shadow, scaled down on press. Four state modifiers cover the patterns: .active is an explicit alias of the default green for toggles that flip an engaged state; .inactive is the cream-ghost "not currently selected" option in a radio group; .off is the dark-ink "currently off" state of a binary on/off master toggle (pairs with .active as the on state — label text carries the literal "OFF" / "ON"); :disabled mutes to a faded outline. Sample below renders the live .wb-btn (light-DOM mirror); shadow-DOM .widget-btn is visually identical and used inside <lab-widget-*> custom elements.

.wb-btn / .widget-btn — 28 px
default (green stamp)
.active (alias of default)
.off (dark stamp — toggle off)
.inactive (cream ghost)
:disabled
.wb-btn.small / .widget-btn.small — 26 px
small + green
small + .active
small + .off
small + .inactive
.toggle-group (cluster — chip treatment suppressed)
two-option toggle
radio-style cluster

.btn-primary-dark — Space Grotesk black stamp

Heavy primary CTA used for destructive confirms and the prominent "Unpause" action. Space Grotesk 700, 18 px, hard 3 px shadow.

default

.btn-step-continue — yellow Continue stamp

Surfaced by the step bar when a ui: button step needs an explicit Continue. The .unpause variant flips to a black stamp during settle pauses.

default
.unpause

.btn-completion-* — completion screen

Primary green + white outlined secondary. Stacked horizontally inside the success card.

primary
secondary

.zoom-in-btn — floating yellow stamp

Auto-appearing zoom-in trigger. Visible while a camera_preset step is loaded. .active flips to a black stamp once the student has zoomed in.

.visible
.visible.active

.modal-btn-* — modal actions

Primary green for affirmative confirms, .danger black stamp for destructive ones, white outline for cancel.

primary (green)
.danger (black)
secondary

Static label primitives

.pill-paused — Space Grotesk 700, rotate(1°)
Time paused
.pill-selection — Space Grotesk 700, ink border
Thermometer
Beaker 250 mL
.widget-label — selection label, animates opacity in the lab (from widget.css)
Thermometer
Hot plate

Always-on labels for liquid containers

A container with a declared properties.contents renders a screen-space chip above it, displaying the authored identity (Cold water, HCl 1M, Mixture, …) prefixed by any hazard badges from a closed taxonomy. Identity only — temperature, pH and concentration stay on instruments. Empty containers carry no chip. Owned by ContentsChipHandler; design captured in journal/007/2026-05-13_16_contents_label_chips.md.

Identity, no hazard

Plain chip when the liquid is benign. Example: a freshly poured beaker of water.

Water
contents only
Mixture
post-pour, cross-species

Physical-state hazards — hot & cold

An orange disc on liquids above ~40 °C; a blue disc on those below ~10 °C. The qualifier in the identity string (Warm, Cold) is the author's initial condition only — it drops once the simulation perturbs the contents (heating, mixing), at which point the chip reverts to the base identity (Water).

Warm water
hot
Cold water
cold

Chemical hazards — corrosive

A red-bordered diamond with a black centre dot — categorical hazard mark, not strict GHS — for both acids and bases. Authoring vocabulary keeps corrosive_acid vs corrosive_base separate so sub-type lessons can elaborate later, but the chip collapses both to one mark; an acid + base mixture therefore shows a single diamond, not two. After cross-species pour the destination becomes Mixture and inherits the union of source hazards.

HCl 1M
corrosive_acid
NaOH 1M
corrosive_base
Mixture
acid + base mixture (deduped)

Focus dimming

A step's focus_set (default = union of object: / objects:) names the containers the student should attend to. Out-of-focus chips recede at chip level — the frame fades to a hairline, the drop-shadow drops, and the text greys. Hazard badges keep full strength so safety information is never lost. An explicit empty focus_set: [] turns dimming off everywhere.

Warm water
in focus
HCl 1M
.contents-chip--dimmed
Cold water
.contents-chip--dimmed (badge stays bright)

UI components

The DOM surfaces handlers render over the Unity canvas. Each render below uses the same DOM structure and class names the handler emits at runtime — only positioning is unwrapped (handlers anchor with position: fixed; previews use position: relative) so multiple instances can stack inline.

Step bar

StepBarHandler · #step-bar · handlers/stepbar/step-bar.css

Top-center card. Phase progress, phase chip, step counter, Space Grotesk instruction (v1 chrome — 22 px / 1.3, instruction-dominant), optional Continue / Unpause / Restart action. Always visible during an investigation.

Observation Step 3 of 8
Watch the iron filings align with the field lines.
Default — observation, no action
Observation Step 5 of 8
When you are done observing, continue to the next step.
With Continue CTA — ui: button
Observation Step 4 of 6
Take a moment to record your readings before resuming.
Unpause variant — paused_reading
Action Step 3 of 3 Completed!
Great work — moving to the next phase.
Step-advance flash — green "Completed!" overwrites the header

Modal

ModalHandler · #modal-backdrop · handlers/modal/modal.css

Centered card with blurred backdrop. Top accent ribbon: green for Information / Destructive, yellow for Confirm — variant signal moves to the stamp + label below. Stamp icon (cyan i / yellow ? / ink !), mono-uppercase stamp label (INFORMATION / CONFIRM / CONFIRM), Space Grotesk 22 title, body, and a full-bottom action row (single button = full-width; paired buttons = equal 50/50 split) so the layout anchors horizontally instead of trailing into an empty bottom-right corner. Three callable shapes: info (single Done), confirm (cancel + green save), destructive confirm (cancel + black-stamp danger). Dismissal goes through the explicit action button — there's no separate close X.

Info — green ribbon, single Done
Confirm — yellow ribbon, amber label
Destructive — green ribbon, ink stamp (.danger)

Completion screen

CompletionHandler · #completion-backdrop · handlers/completion/completion.css

Full-viewport success card. Top accent ribbon (green = regular / table, yellow = all-complete), ink-stamped badge with Space Grotesk glyph, mono stamp label (COMPLETED / ALL DONE — tertiary ink for regular, amber on all-complete), title, subtitle, full-width action column. Three variants: regular, table-complete, all-investigations.

Regular — green ribbon
All complete — yellow ribbon, amber label

Inspection modal

<lab-inspection-modal> · js/components/inspection-modal/

Post-measurement review modal. Green 4 px top stripe, ink-stamped i + REVIEW stamp label + Space Grotesk title + description + Continue (primary, full-width) and Restart Procedure (secondary, full-width). Focus-trapped; Escape resolves as Continue. Buttons share an identical width so the row never reflows when labels change.

Classroom nav

<lab-classroom-nav> · js/components/classroom-nav/

"Back to classroom" nav button for station-rotation investigations.

.visible

Pause overlay

PauseOverlayHandler · 6 px green inset · handlers/overlay/pause-overlay.css

Non-interactive (pointer-events: none). Active whenever simulated time is paused.

Time paused
Viewport thumbnail

Restart investigation pill

RestartLinkHandler · handlers/restart/restart-link.css

Persistent bottom-left pill, always visible. Click opens a confirmation modal; on confirm dispatches restart-investigation. The wrapper routes that to a parent-coordinated re-feed (embedded mode, via HostBridgeHandler's RestartRequested postMessage) or a page reload (standalone).

Default

Orbit-trace readout

OrbitTraceHandler · handlers/orbit/orbit-trace.css

Yellow stamp pinned above the bottom of the canvas. Auto-updates per frame with current semi-major axis and eccentricity.

a = 8.0 cm    e = 0.00Circular
a = 14.2 cm    e = 0.62Stretched

Loading screen

LoadingHandler · handlers/loading/loading-screen.css

Full-screen black overlay shown from first paint until Unity hands over control on the first real step. 500 ms fade-out.

Loading investigation…

Setting up your lab workspace

Loading

Loader curtain

LoaderCurtainHandler · SimulationBusyHandler · handlers/loader-curtain/

Mid-investigation "please wait" overlay shown when a /simulate request stays in flight longer than 2 s, kept on screen for at least 1.5 s once shown so a fast response doesn't cause a flash. Two siblings at separate z-indices: a faint 15 % cream wash sits below the lab chrome (z-index 55, dims the canvas only — step bar, widget bar, time control, and restart pill stay bright and clickable), and a small ink-bordered card with spinner + label sits above the chrome (z-index 95, the focal point). Reads as "calculating, hold on" rather than "loading, welcome". LoaderCurtainHandler is the visual primitive (show({label, sub}) / hide()); SimulationBusyHandler owns the threshold and min-visible timers and binds them to the simulate-request / simulate-response / simulate-fail DOM events bridged off the sealed engine in GalileoWrapper/js/main.js.

Calculating simulation

Default — label only

Calculating simulation

This usually takes a few seconds.

With sub line

Lab screens

Two states of the lab — Frozen (action step, awaiting input) and Simulating (clip running). Composed live from the same chrome the kit ships: step-bar, restart-link, camera-controls, time-control, and a single device widget at bottom-centre. In Simulating the time-control bar takes the bottom-centre slot and the device widget stacks above it. Production CSS edits land here too.

Frozen · five beakers, step-focus on three

Mockup of the always-on contents chip overlay on a five-beaker scene. Step prose names warm_beaker, cold_beaker and hcl_beaker, so those three chips render at full strength; naoh_beaker is outside focus_set and recedes at chip level (light frame, no shadow, grey label) while keeping its hazard badge bright. The empty leftmost beaker carries no chip (absence means empty). Beakers are CSS silhouettes — this is a layout sketch, not a 3D render. The chips themselves are the production .contents-chip components from Atoms. Blocking work to make this live: journal/007/2026-05-13_17_contents_chips_dev_team_request.md.

Action Step 2 of 5
Add the warm and cold water to the HCl 1M sample.
no chip · empty
Warm water
in focus
Cold water
in focus
HCl 1M
in focus
NaOH 1M
dimmed

Frozen

Action step · time bar absent · device widget sits at bottom-centre

Observation Step 3 of 8
Place the thermometer in the beaker.

Simulating

Clip running · step bar carries the SIM phase chip · time-control bar at bottom-centre with the device widget stacked above it

Observation Step 3 of 8
SIM
Watch the temperature climb as the heater runs.
00:04 / 00:10

Simulating · speed popover open

Same as Simulating (device widget above the time-control bar); the speed dial is engaged (green) and the speed popover floats above the dial. Three forward speeds — 3x at the top, 1x at the bottom (active).

Observation Step 3 of 8
SIM
Watch the temperature climb as the heater runs.
00:04 / 00:10

Widgets

Per-device controls mounted into the cream-paper widget bar. Each render below is the actual <lab-widget-*> custom element from galileo_viewer_kit/js/widgets/ — refactor a widget and this page updates automatically. Time Control sits at the top as the canonical example of the widget-bar pattern, followed by every device widget grouped by control archetype. Use the index below to jump straight to one.

Time control

TimeControlHandler · #time-control · handlers/time/time-control.css

Field Notebook v2 standard, current source of truth Figma node 110:421. Cream pill (530 × 48, 1.5 px ink border, 12 px corner, 3 px hard shadow). Layout left → right: speed dial · pause · scrubber · readout · joined "Previous-step / Skip-to-end" pair. Two visible states — collapsed default and engaged (green dial + a compact vertical speed popover anchored above the dial). Speed labels use the lowercase letter x (1x, 2x, 3x), not the multiplication sign. Tapping any popover button assigns the speed and dismisses the popover; tapping outside the popover also dismisses. The left button of the joined pair dispatches a step-previous event (it re-uses the skip-back glyph but navigates investigation steps, not the clip cursor); the right button still skips the cursor to the clip's end. Mounts whenever the active step has a duration: and auto-hides when the step ends.

Controls

Speed dial (current speed + chevron, opens popover) Pause / play (green when running) Scrubber (drag the handle) Readout MM:SS / MM:SS (Space Grotesk 13, tabular-nums) Previous-step  ·  Skip-to-end (joined pair — left dispatches step-previous, right scrubs to clip end) Speeds: 1x · 2x · 3x (forward only — reverse playback retired May 2026)

States

00:04 / 00:10
Default · running at 1x
00:09 / 00:15
Paused at 2x
00:04 / 00:10
Engaged · popover open

Electronic scale

<lab-widget-scale> · js/widgets/scale/ScaleWidget.js

The minimum viable widget: a label, a value with unit, and one CTA. Establishes the cadence used by every readout-style widget — Space Grotesk 22 px value, mono Plex 11 px unit hugging the descender.

Empty (0 g)
142.5 g

Spring scale

<lab-widget-spring-scale> · js/widgets/spring-scale/SpringScaleWidget.js

Adds a horizontal pill-shaped track with green fill and per-newton ticks alongside the readout. The tick rule (filled ticks turn semi-transparent white as the fill passes them) is shared with several other gauges.

0 N · empty
2.5 N · mid
Maxed

Hot plate

<lab-widget-hot-plate> · js/widgets/hot-plate/HotPlateWidget.js

Combines a binary on/off button (left) with a segmented intensity selector (Low / Med / High) and a power-state indicator dot. The active state is conveyed two ways — black-stamp button + glowing red dot. Canonical pattern for "device with intensity levels."

Off
On · Low
On · Medium
On · High

Colored flashlight

<lab-widget-colored-flashlight> · js/widgets/colored-flashlight/ColoredFlashlightWidget.js

Three independently-toggleable channels (R, G, B) plus a master ON/OFF and live status dots. Demonstrates non-mutually-exclusive toggle buttons inside the widget bar. The neighbouring color-dot indicators glow when the corresponding channel is lit.

Off · all dim
Red + Blue (magenta)
All three (white)

Gauge

<lab-widget-gauge> · js/widgets/gauge/GaugeWidget.js

Continuous value with a draggable thumb on a 5 px track. A green "sweet spot" region marks the target band — the student aims to land the thumb inside it. Pointer events are captured directly by the widget; no separate confirm.

At min · 0.0 °C
In sweet spot
At max · 100 °C

Slider (generic)

<lab-widget-slider> · js/widgets/slider/SliderWidget.js

Native <input type="range"> styled with the system thumb (green disk + 3 px hard shadow), plus a Space Grotesk readout and an OK button to commit. Used for any continuous parameter where the student should preview before submitting.

Volume 65 %
Freq 1000 Hz

Microscope

<lab-widget-microscope> · js/widgets/microscope/MicroscopeWidget.js

The most control-dense widget. Three independent axes: objective lens (mutually exclusive 4x / 10x / 40x), focus pair (Coarse + Fine, both toggleable), and Diaphragm (single toggle). Demonstrates how to fit several toggle groups + standalone toggles into one bar without crowding.

Initial · 4x · all toggles inactive
10x · coarse + diaphragm set
40x · fine + diaphragm

Cart launcher

<lab-widget-cart-launcher> · js/widgets/cart-launcher/CartLauncherWidget.js

Three-state machine (placed → armed → fired) with a state readout in the bar. Buttons disable when their action is invalid for the current state. Same pattern other state-machine widgets follow (microwave, stopwatch, generators).

Placed
Armed
Fired

Pour

<lab-widget-pour> · js/widgets/pour/PourWidget.js

The system's exception: a free-floating, vertical column rather than a yellow widget bar. Pinned in screen-space above the active liquid container, it shows a fill-track (color-coded by liquid temperature: blue / red / cyan / done-green), volume in mL above, and a status string below. Sets the visual vocabulary for the next round of "ambient" widgets.

Idle
Pouring · room-temp
Pouring · hot
Pouring · cold
Full · done

Thermometer

<lab-widget-thermometer> · js/widgets/thermometer/ThermometerWidget.js

ON/OFF probe with a Space Grotesk °C readout. The pilot of the readout-style family; every "value + unit" widget below shares its display rules.

Off
Room · 22 °C
Hot · 85.3 °C

IR thermometer

<lab-widget-ir-thermometer> · js/widgets/ir-thermometer/IrThermometerWidget.js

Adds a Trigger button + green laser-dot indicator to the readout pattern: ON arms it; pulling the trigger captures the spot temperature.

Off
Armed
Reading · 37.2 °C

Ammeter

<lab-widget-ammeter> · js/widgets/ammeter/AmmeterWidget.js

ON/OFF + segmented mA / A unit selector. Establishes the "readout with a unit toggle" pattern shared by graduated cylinder, ruler, and others.

Off
42.5 mA
1.235 A

Light meter

<lab-widget-light-meter> · js/widgets/light-meter/LightMeterWidget.js

Lux readout with a cyclic range button (auto / 2K / 20K / 200K). Same chrome as the ammeter but with a step-through control instead of a binary toggle.

Off
450 lux · auto
15K lux · 20K range

Humidity sensor

<lab-widget-humidity-sensor> · js/widgets/humidity-sensor/HumiditySensorWidget.js

Two stacked readouts in one bar: %RH primary, °C secondary. Pattern for any device that reports two correlated values.

Off
45 % @ 22 °C
78.3 % @ 19.5 °C

CO₂ sensor

<lab-widget-co2-sensor> · js/widgets/co2-sensor/Co2SensorWidget.js

ppm readout with a stabilizing-state visual: the value blinks while warming up, settles when stable. First widget to model a transient sub-state inside an "on" mode.

Off
Stabilizing
Stable · 1250 ppm

Graduated cylinder

<lab-widget-graduated-cylinder> · js/widgets/graduated-cylinder/GraduatedCylinderWidget.js

Volume in mL or L, toggleable. Always-on (no power button) — the cylinder is a passive measurement device.

25.5 mL
1250 mL → L

Ruler

<lab-widget-ruler> · js/widgets/ruler/RulerWidget.js

Distance with cm / mm toggle. Shares unit-cycle chrome with graduated cylinder — same family, different domain.

12.5 cm
4.8 cm → mm

Force sensor

<lab-widget-force-sensor> · js/widgets/force-sensor/ForceSensorWidget.js

The control-rich corner of the readout family: live N value, peak captured below it, plus Zero / Start-Stop recording buttons. Pattern for any tool that records over time.

Idle
Recording · 12.4 N (peak 18.7)

Flashlight

<lab-widget-flashlight> · js/widgets/flashlight/FlashlightWidget.js

Single-channel light: on/off + a wide / narrow beam selector. The "binary toggle plus modal sub-control" template every directional emitter follows.

Off
Wide beam
Narrow beam

Laser pointer

<lab-widget-laser-pointer> · js/widgets/laser-pointer/LaserPointerWidget.js

The minimal toggle widget: one button, two states. Reference for every device whose only choice is on or off.

Off
On

Kitchen lighter

<lab-widget-kitchen-lighter> · js/widgets/kitchen-lighter/KitchenLighterWidget.js

Strike → lit → extinguish. Looks like a toggle but the third state (extinguished) is reachable only from "lit", giving a small directional state graph in a single button group.

Idle
Lit
Extinguished

Fan

<lab-widget-fan> · js/widgets/fan/FanWidget.js

Same shape as hot plate: power button + a Low / High segmented selector. Different domain, identical chrome.

Off
Low
High

Hair dryer

<lab-widget-hair-dryer> · js/widgets/hair-dryer/HairDryerWidget.js

Variant of the fan: same controls, different label. Confirms that copies of an archetype don't need a new visual primitive.

Off
Low
High

Power strip

<lab-widget-power-strip> · js/widgets/power-strip/PowerStripWidget.js

Master switch for everything plugged in. Same chrome as the laser pointer; different mental model — students associate this with "all devices live".

Off
On

Rocker switch

<lab-widget-rocker-switch> · js/widgets/rocker-switch/RockerSwitchWidget.js

Domain-specific binary toggle for circuit work — labelled "open" / "closed" instead of "on" / "off". Preserves the pattern but speaks the right vocabulary.

Open
Closed

Tumble buggy

<lab-widget-tumble-buggy> · js/widgets/tumble-buggy/TumbleBuggyWidget.js

Toggle plus a forward / reverse selector — the "device with a directional axis" variant of the fan/hot-plate shape.

Off
Forward
Reverse

Speaker

<lab-widget-speaker> · js/widgets/speaker/SpeakerWidget.js

Binary toggle decorated with animated wave bars while playing. Sets the visual rule for "audio currently emitting" used by the rest of the sound family.

Silent
Playing

Tuning fork

<lab-widget-tuning-fork> · js/widgets/tuning-fork/TuningForkWidget.js

Strike action + a "vibrating" indicator that decays over time. Same toggle shape as the speaker but with momentary semantics.

Still
Vibrating

Drum

<lab-widget-drum> · js/widgets/drum/DrumWidget.js

Same momentary-toggle shape as the tuning fork, with a "struck" indicator instead of "vibrating". Both widgets share their attack/decay rules.

Still
Struck

pH strip

<lab-widget-ph-strip> · js/widgets/ph-strip/PhStripWidget.js

No yellow chrome — just a coloured strip plus a numeric pH. The undipped state shows the strip blank; once dipped, the colour and number lock in.

Un-dipped
Acidic · pH 3
Neutral · pH 7
Basic · pH 11

BTB indicator

<lab-widget-btb-indicator> · js/widgets/btb-indicator/BtbIndicatorWidget.js

Coloured chip + categorical label (Acidic / Neutral / Basic). Demonstrates a colour-driven readout where the value is qualitative, not numeric.

Acidic
Neutral
Basic

LED (bi-color)

<lab-widget-led> · js/widgets/led/LedWidget.js

Three-state indicator (off / green / red). No chrome around it — drops directly onto the bench as a status light.

Off
Green
Red

Microwave

<lab-widget-microwave> · js/widgets/microwave/MicrowaveWidget.js

Four-state machine (door open → closed → running → stopped) with a digit timer. Adds a countdown readout to the cart-launcher pattern.

Door open
Door closed
Running
Stopped

Stopwatch

<lab-widget-stopwatch> · js/widgets/stopwatch/StopwatchWidget.js

Owns its own clock — increments live in the second tile. Same Space Grotesk tabular display rule as the time control bar, but mounted as a per-step measurement tool.

Idle
Running (live)

Hand-crank generator

<lab-widget-generator> · js/widgets/generator/GeneratorWidget.js

Stop / slow / fast / intermittent / reverse with a paired V + mA readout. The fullest example of how a state-machine widget adds quantitative outputs alongside its mode buttons.

Idle
Slow · 1.2 V · 0.15 A
Fast · 3.8 V · 0.45 A
Reverse · −3.8 V · −0.45 A

Homemade generator

<lab-widget-homemade-generator> · js/widgets/homemade-generator/HomemadeGeneratorWidget.js

Sister widget to the hand-crank generator: same state machine, but the readout is replaced by two indicator LEDs for a more qualitative student experience.

Idle
Slow (dim)
Fast (bright)
Reverse

Snapping track

<lab-widget-snapping-track> · js/widgets/snapping-track/SnappingTrackWidget.js

Continuous-input chrome that snaps to a fixed list of positions. Same shape as the gauge but the thumb can only land on authored angles.

45°
90°

Smart-cart track

<lab-widget-smart-cart-track> · js/widgets/smart-cart-track/SmartCartTrackWidget.js

Pairs a numeric angle readout with a brake-release toggle: the cart only rolls when the brake is released. Demonstrates the "input + safety interlock" combination.

Flat · brake
15° · brake
25° · released

Preset

<lab-widget-preset> · js/widgets/preset/PresetWidget.js

Generic multi-choice selector for any ui: preset step. Wraps the segmented-toggle atom with the standard yellow chrome and emits a preset-select event with the picked value.

Three options
Five options