WGD.ai

SEVEN OBJECTS

Not a framework. Not a component library. Seven things you can own. Custom elements, no dependencies, no build step. Copy a tag and a script and it works.

Install
<!-- all seven, ~20KB, no dependencies -->
<script src="https://wgd.ai/1/instruments.js"></script>

<!-- then anywhere on the page -->
<wgd-switch label="POWER"></wgd-switch>
001SwitchControl · boolean
Click · drag past centre · space · arrows

A switch should feel like a decision. The detent breaks over mid-drag, so once you're past centre you can't take it back — the same reason a real toggle has no undo.

Usage
<wgd-switch label="POWER" on></wgd-switch>

// state
power.on = true;        // silent, reflects to attribute
power.toggle();         // clicks + fires change

// events
power.addEventListener("change", e =>
  console.log(e.detail.value));

// forms — submits like a real control
<form><wgd-switch name="power" value="on"></wgd-switch></form>
API
Attributes
onboolPresence = on. on="false" is deleted on parse so CSS and JS can't disagree.
labelstringRendered beside it, and used as the accessible name
name / valuestringForm field name; value submitted when on (default "on")
disabledboolUntabbable, unclickable, silent
muteboolSilences this instance only
electricboolArc while on
Properties · Events
.onboolGet/set. Setting does not fire change
.toggle()→ boolFlips and fires change
changeeventdetail.value — boolean
CSS
--switch-sizelengthWidth. Everything else scales off it
--switch-accentcolorFalls back to --wgd-accent
on false
002DialControl · continuous
Drag vertically · shift = fine · wheel · arrows · home/end

Vertical drag, not circular chasing — your finger never has to orbit the knob. Detents tick only when you actually cross one. The arc fires at either endstop, so you can feel the range end without looking.

Usage
<wgd-dial label="GAIN" min="0" max="11"
          step="0.5" value="4" detent></wgd-dial>

// input while turning, change on release
gain.addEventListener("input",  e => live(e.detail.value));
gain.addEventListener("change", e => save(e.detail.value));

gain.value = 7.5;      // silent, quantized to step
API
Attributes
min / maxnumberDefault 0 / 100
stepnumberQuantization. 0 for continuous. Sets readout decimals
valuenumberReflected — the attribute always shows the live value
unitsstringAppended to the readout
detentboolAudible tick per step crossed (needs step)
Properties · Events
.valuenumberGet/set. Setting is silent
inputeventEvery change while dragging/wheeling
changeeventOn release, or immediately on keypress
CSS
--dial-sizelengthDiameter
--dial-accentcolorIndicator, lit ticks, readout
value 4drives meter 003 36%
003MeterReadout · linear

The hairline is spring-damped, so it arrives before it stops. Over redline the arc fires: on a readout electric means alarm, not decoration. That's the whole reason the arc exists in a set of controls.

Usage
<wgd-meter label="LOAD" min="0" max="100"
           redline="85" units="%" electric></wgd-meter>

// readouts take a value and emit nothing but alarm
load.value = 92;

load.addEventListener("alarm", e => {
  // fires once on entering AND leaving tolerance
  page.classList.toggle("critical", e.detail.alarm);
});
API
Attributes
valuenumberCurrent reading
min / maxnumberScale ends. Default 0 / 100
redlinenumberZone start. Reaching it raises alarm
unitsstringAppended to the readout
Properties · Events
.valuenumberGet/set
.alarmboolRead-only current state
alarmeventdetail.alarm — on both edges
CSS
--meter-sizelengthWidth; bar height derives from it
--meter-redlinecolorZone tint
value 36alarm false
004RadioControl · exclusive
Click · arrows · home/end

Latching keys, the way car radio presets worked: pressing one pops the others out, and you hear both events. The arc surges on latch and dies — current spikes when a contact closes, it doesn't sit there glowing.

Usage
<wgd-radio label="BAND" options="AM,FM,SW,AIR"
           value="FM"></wgd-radio>

band.addEventListener("change", e => {
  e.detail.value;   // "SW"
  e.detail.index;   // 2
});

band.value = "AIR";   // or band.selectedIndex = 3
API
Attributes
optionscsvComma-separated labels. Rebuilds the keys when changed
valuestringSelected label. Reflected
namestringForm field name
Properties · Events
.valuestringGet/set by label
.selectedIndexnumberGet/set by position
.optionsstring[]Read-only, parsed
changeeventdetail.value, detail.index
CSS
--radio-sizelengthDrives key size and type scale
--radio-accentcolorPressed key
value FMindex 1
005CompassReadout · angular

The card turns, the lubber line doesn't. That's how you read a real compass — you look at the fixed mark, not a spinning needle. It's deliberately underdamped, so it swings past and settles, and it always takes the short way across zero.

Usage
<wgd-compass heading="41" bearing="90"
             tolerance="12" electric></wgd-compass>

hdg.heading = 352;   // wraps, takes the short path

// optional: live device heading + tilt.
// must be called from a gesture — never auto-prompts.
const stop = await WGD.attachOrientation(hdg, lvl);
API
Attributes
headingdeg0–360, normalized
bearingdegOptional target mark. Omit for no alarm
tolerancedegAlarm past this from bearing. Default 10
Properties · Events
.headingdegGet/set
alarmeventOff-bearing edges
CSS
--compass-sizelengthDiameter
--compass-accentcolorLubber line, N, readout
heading 041°off bearing yes
006LevelReadout · tilt

The bubble is overdamped on purpose — fluid is viscous, so unlike every needle in the set it must never overshoot. Offset tracks the sine of the angle, not the angle, which is why it slows as it nears the end of the vial.

Usage
<wgd-level label="TRIM" angle="1.8"
           range="6" tolerance="0.5" electric></wgd-level>

trim.angle = -3.4;    // degrees, signed

trim.addEventListener("alarm", e =>
  e.detail.alarm && warn("out of trim"));
API
Attributes
angledegSigned. Positive tilts right
rangedegAngle at the end of the vial. Default 6
tolerancedegAlarm past this from level. Default 0.5
Properties · Events
.angledegGet/set
alarmeventOut-of-tolerance edges
CSS
--level-sizelengthVial width
--level-accentcolorBubble
angle 1.8°
007VUReadout · ballistic

Real ballistics: 300ms to 99% with about 1% overshoot. That one spec is the entire difference between a VU meter and a bar graph — it never quite catches the signal, which is correct. The peak dot holds, then falls back slowly.

Usage
<wgd-vu label="MIX L" value="0.35" electric></wgd-vu>

// value is AMPLITUDE, not dB. 1.0 = 0 VU.
vu.value = 0.9;

// live from the mic. gesture only. mic writes an internal
// level, not the attribute — 60 attribute writes a second
// would run every aria update 60 times a second.
await vu.enableMic();
vu.disableMic();
API
Attributes
valueamp0–1.6 amplitude. 1.0 is 0 VU, 1.42 is +3
Properties · Events
.valueampGet/set. Reads the live level while mic is on
.enableMic()→ PromiseRequires a user gesture. Never auto-prompts
.disableMic()voidStops tracks and resets
alarmeventCrossing 0 VU
CSS
--vu-sizelengthWidth; 200:118 aspect
--vu-redlinecolorAbove 0 VU
amplitude 0.35dB −9

One grammar the whole point

Shared across all seven
// CONTROLS — switch, dial, radio. Emit while moving, commit on release.
el.addEventListener("input",  e => e.detail.value);   // continuous only
el.addEventListener("change", e => e.detail.value);   // all three
// setting .value or .on programmatically is always SILENT

// READOUTS — meter, vu, compass, level. Take a value, emit alarm.
el.value = 92;
el.addEventListener("alarm", e => e.detail.alarm);  // both edges

// THEME — one declaration restyles every instrument on the page
:root { --wgd-accent: #ffc328; --wgd-size: 80px; --wgd-font: 'Inter'; }
// per-instrument overrides win: --switch-accent, --dial-size, ...

// SHADOW PARTS — for anything the vars don't reach
wgd-switch::part(cap) { border-radius: 2px; }

// PAGE LEVEL
WGD.electrify(true);   // arcs on, every instrument
WGD.mute = true;      // silence everything; or mute="" per element
WGD.all();             // → every instrument in the document
WGD.version;          // "1.0.0"

Shared runtime why one file

One ticker
Every needle, bubble and arc runs off a single requestAnimationFrame that stops itself when nothing is moving
One audio bus
One AudioContext, four voices — click, tick, clack, release. Nothing plays until you touch something, because a context can't start without a gesture anyway
One stylesheet per class
Constructed once and adopted by every instance, not re-parsed per element
One physics
The same damped spring drives the meter hairline, the compass card, the bubble and the VU needle. Only frequency and damping differ, and those numbers are the design
Offscreen
An arc scrolled out of view holds its state and computes nothing
Reduced motion
Travel, swing and arc animation all drop to a still frame. Sound is untouched — that's a vestibular setting, not an audio one
Weight
About 20KB for all seven. No dependencies, no build step, no login

Event stream live

All instruments0 events
// touch something