Skip to content

Switch

Tier A. Follows the anatomy template and the conventions.

Turns a setting on or off, taking effect at once. Renders a native <input type="checkbox" role="switch"> with a <label>, following the WAI-ARIA Switch pattern. The element is native; role="switch" is the one ARIA attribute, because HTML has no switch element. A choice that is submitted later with a form is a Checkbox.

A track (a well) with a spherical knob that slides. The track’s base is the input’s border box.

Part Shape Height at 50% Moves
Track Pill (radius.pill) sunk by elevation.well (2) −elevation.well No; turns emissive when on
Knob Sphere of diameter track height − 2 · object.taper (14 at the default size) elevation.raised (3) Slides the track width minus the track height, over motion.normal

The input’s border box is size.switchWidth by size.indicator: 30 by 16, 36 by 20 (default) or 44 by 24 px for data-strata-size small, medium or large (section 13), and the label row is at least target.min (24) high and covers the whole row, so the row is the target (WCAG 2.5.8). The track fills the box. The knob’s whole travel stays inside it, inset by object.taper at both ends, so the native control covers every position of the knob (section 6, moving parts). The knob casts a shadow onto the track.

Face Colour role Text role Material role
Track, off track — field (matte)
Track, on accent — trackOn (emissive)
Knob, off knob, with a 1.5 px edge ring — knob (satin)
Knob, on knob (the same colour), with a 1.5 px on-accent ring — knob (satin)
Track outline edge, 1 px — None
Label None (sits on the panel) text None

The track’s edge outline keeps its shape at 3:1 against the panel. The knob keeps at least 3:1 against the track under it in both states (section 8). The state is also shown by the knob’s position, never by colour alone. data-strata-material on the input overrides the track material.

State Relief Objects CSS
Rest, off Track at −elevation.well; knob at the start end Knob at the start Inset shadow, track fill
Hover Knob height × state.hoverHeight; track interior +state.hoverLift Knob rises a little Larger knob shadow, brightness(1.04)
Pressed (:active) Knob height × state.pressHeight; interior −state.pressSink Knob sinks Smaller shadow, brightness(0.96)
Focus-visible Accent edge, focus.width, outside the track Emissive glow on the track outline: focus.width solid accent
On (:checked) Track emissive; knob slides to the end over motion.normal Track lights; knob slides accent fill; knob translateX
Disabled Track and knob at height 0, no emissive glow, content at 38% Flattened to the face Flat, opacity: 0.38
Invalid (aria-invalid="true") danger edge on the track Same outline in danger

Busy does not apply; for work in progress the application disables the switch and shows a Spinner. Under prefers-reduced-motion, the knob jumps to the end with no slide.

Two: the <input> is a well (radius.pill); the knob is a knob, drawn from a child <span data-strata-face="knob">. Its position follows :checked; the pose lives in the GPU state and in CSS transform, never in the input’s box.

Token Prop
colour.track, colour.accent, colour.knob, colour.edge, colour.text, colour.danger —
radius.pill — (theme or scope)
elevation.well, elevation.raised, object.taper, state.*, interior.max —
material.roles.field, material.roles.trackOn, material.roles.knob material
target.min (track height), density (label row height), space.* —
font.size.body —
focus.width, focus.offset, motion.normal, motion.easing.standard —
level level
<label class="ug-switch">
<input class="ug-switch__input" data-strata-face="well" type="checkbox" role="switch" name="wifi" />
<span class="ug-switch__knob" data-strata-face="knob" aria-hidden="true"></span>
<span class="ug-switch__label">Wi-Fi</span>
</label>
  • The <input> is the boundary element (appearance: none, size.switchWidth by size.indicator). Refs and checked reach it.
  • The wrapping <label> extends the hit target to the whole row, at least the density’s control height. The knob is pointer-events: none and lies over the input.
  • A label on the other side of the track is a reverse prop; the DOM order stays, so the focus and reading order do not change.
  • No overlay.
Key Action
Space Toggle (native)
Tab, Shift+Tab Move focus (native order)
  • Role switch (explicit), with the checked state exposed as aria-checked by the browser from checked. Screen readers say “on” or “off”. The label must not contain “on” or “off”.
  • Name from the label text. Help and error through aria-describedby (Field without label).
  • Enter does not toggle, as the native checkbox.

The well recipe as a pill, the on-track as accent with a glow shadow for the emissive material, and the knob as a circle with a spherical gradient and a cast shadow from the light. The input is size.switchWidth by size.indicator; the row is at least the density’s control height. Under forced-colors: the track has a ButtonBorder border; off is Field with a ButtonText knob at the start; on is Highlight with a HighlightText knob at the end; GrayText when disabled; no glow. Position and fill both show the state.

  • Plain DOM: click on the track and on the label toggles once; Space toggles; role is switch; form data follows checked; disabled does not toggle.
  • GPU: at Relief and Objects, GPU stays active through the slide; pixels are fresh at the end position and the track glows when on; a hit at the track’s edge pixel and on the label reaches the input, at both knob positions.
  • Accessibility: axe on on, off, disabled and invalid, in light, dark and high contrast; accessibility snapshot equal in DOM and GPU modes (switch, checked); focus ring in decoded pixels; knob contrast on both tracks.
  • Parity: Relief and CSS within the P0.6 budget at rest, hover, on and focus.

The same native switch drawn as hardware (Instrument plan, design language section 6): form (prop) or data-strata-form (on the row) chooses slide, rocker, bat or wall. Without it the Switch is the Strata switch above, at every level; the premium character makes slide its default once characters land (design language section 19; no character is on main yet). Sections 1, 3, 6 and 8 hold: the native <input type="checkbox" role="switch"> is the control, its label row is the target, the state is checked (aria-checked), and the label must not contain “on” or “off”.

<label class="ug-switch" data-strata-form="rocker">
<input class="ug-switch__input" type="checkbox" role="switch" name="power" />
<span class="ug-switch__object" aria-hidden="true">
<span class="ug-switch__bezel" data-strata-face="raised"></span>
<span class="ug-switch__paddle">
<span class="ug-switch__facet ug-switch__facet--on" data-strata-face="raised"></span>
<span class="ug-switch__facet ug-switch__facet--off" data-strata-face="raised"></span>
<span class="ug-switch__flag" data-strata-face="flat"></span>
</span>
</span>
<span class="ug-switch__label">Power</span>
</label>
  • The <input> is a transparent, square box over the object’s whole travel (the I0.2 box rule), with no face: a press anywhere on the object is a press on the input, and the hit area never moves. Its corners are square, so no corner of the box lets a press fall to the row.

  • .ug-switch__object (aria-hidden; it and every part pointer-events: none) lies over the input, in paint order:

    Form Parts (switchFormMarkup in shared/switch-forms.ts)
    slide slot (groove), flag (flat), block (raised) with its grip
    rocker bezel (raised); paddle with facet--on and facet--off (raised) and flag (flat)
    bat nut, bushing and lever (raised), ball (knob)
    wall plate (raised); paddle with facet--on and facet--off (raised) and flag (flat)
  • The bindings render it from form. Any other form string is still the native form attribute (the id of the input’s form owner), so existing uses keep working. reverse, material, level, Field wiring and refs are as for the Strata switch; size does not change a form’s box.

On is to the right for a slide and up for the others: an on rocker or wall switch has its top (On) end pressed in and its bottom end raised, and an on bat points up. Numbers are the premium character’s at 50% depth (design language section 6, the second prototype iteration’s).

Form Box (the input) Static parts Moving part Throw Spring
slide 64 × 28 The slot, 50 × 8, printed A block 24 × 20 (6 high, taper 0.4, 2 px chamfer); its lit flag (21 × 4, in the slot’s left half) shows once the block has left it Slides 28 spring.slide 600 / 49 (ratio 1.00): at rest in 568 ms
rocker 54 × 70 A bezel 38 × 62, 2 high A paddle 28 × 50, facets 12 / 4, a lit flag 18 × 9 on its On end (1.0 on, 0.12 off) Tilts ±17.7° spring.rocker 1,200 / 58 (0.84): at its stop in 160 ms
bat 48 × 92 A nut Ø24 (3 high) and bushing Ø14 (7 high) A lever 40 long (× the depth scale, at most 1.15), base r 3.4, neck r 2.7, ball r 5.6, on a pivot 5 up Tilts ±40°: head-on the ball stands 18.9 px from the pivot spring.bat 1,400 / 45 (0.60): 72 ms
wall 66 × 92 A plate 58 × 84 (3 high, radius 6) A paddle 30 × 54 (7 high, taper 0.6, chamfer 2, crowned 1.6), a lit dot 12 × 4 near its On end Tilts ±9° spring.wall 1,200 / 58: 160 ms
  • Boxes. Each box passes core’s containment check: every vertex at least 0.5 px inside, over the whole travel, at the reference stage’s corners and edge middles, at 0, 50 and 100% depth. The prototype’s rocker box, 38 × 62, does not: tipped ±17.7° at full depth the paddle’s raised corners stand about 29 px high and drift up to 12 px sideways at the stage’s edges. The box is therefore 54 × 70, with the bezel still 38 × 62 in its middle. The prototype’s wall plate filled its 66 × 92 box, leaving no room for its base edge or its shadow, so the plate is 58 × 84, 4 px inside. The closest any vertex comes to its box: slide 2.68, rocker 1.38, bat 1.88, wall 3.09 px.
  • The throw is the input’s native change, once. The snap is visual: --strata-switch-t, a registered number (0 off, 1 on), transitions on the form’s spring, clamped at the stops (section 12: the velocity into a stop is dropped, so nothing passes it) and sampled every 4 ms into a linear() easing (switchSpringCurve). Under reduced motion it is instant.
Input Action
Click or tap anywhere on the box Throws it (native)
A drag released past the box’s centre towards the other stop Throws it (attachStrataSwitches; touch too)
A drag released short of centre Nothing, and no event: the lever springs back
Click on the label Throws it (native)
Space Throws it (native); Enter does not, as on the Strata switch

A press that moves less than 4 px is a click. A drag keeps the pointer captured, so it may end outside the box; a touch drag, which makes no click, is thrown by the behaviour itself. The one event the behaviour stops is the click a held drag ends with (at the document, in the capture phase), so no framework sees a toggle that did not happen. The input is touch-action: pan-x (pan-y for a slide), so a drag along the throw never scrolls the page.

State Relief Objects CSS
Off, on The moving part at its stop; a paddle’s raised half taller than its pressed half The same, as slabs The block translated, the paddle tipped with rotateX, the lever’s projection scaled by sin θ
Throw Boxes and heights follow --strata-switch-t frame by frame Same The spring’s linear() transition
Lit flag or dot A flat face: its captured accent drawn unshaded, full when on, 12% off Same The accent; mixed with the metal when off
Hover Moving parts × state.hoverHeight Caps raised by a quarter --_lift, --_tone
Pressed (:active) × state.pressHeight Sinks —
Focus-visible The input’s outline round the box (captured content); parts take no focus edge Same Outline
Disabled Parts flat, the object at 38% Same opacity: 0.38

At Relief and Objects the GPU helper (gpu/switch-forms.ts) writes each part from its laid-out box, so it follows the CSS throw: the slot a groove; the block, bezel, plate, nut, bushing, the lever’s projected shaft and the paddle’s halves raised faces (Relief) or slabs (Objects), each half standing on its bezel or plate; the ball a knob face (Relief) or a dome in the knob colour (Objects); the flags flat, which the model never lights (Rendering Model 2.3), so the lit accent reads at full strength on a shaded part. A paddle’s tilt reads as height: its raised half stands 4 px high and its pressed half 1.5 (2 on the wall plate at Relief), mixed by t through the throw, so the shadow moves to the raised end; the CSS draws the paddle head-on in face mode. Every part stands only as high as its box lets its cast shadow reach (2.8, hovered, under the scene’s light): the slide’s block and the wall plate, 4 px inside their boxes, stand 1.24 px high under the default light. Native hits never leave the input.

gpu/instrument-switch.ts defines each form as an object of parts for core’s instrument material, bounded by the input’s box and validated by core: the slide’s chamfered brushed carriage, polished chamfer, brushed cap, ink grip and lit segment; the rocker’s polished bezel, faceted paddle (an extrusion turned onto its length, tilting about its pivot on the bezel) and lit segment; the bat’s polished nut, bushing, lever and ball; the wall’s plate (brushed aluminium on light faceplates, gunmetal on dark), chamfered paddle, its chamfer, a crowned cap and lit segment. Every moving part of a form slides or tilts about one pivot, mix(from, to, t).

The helper’s instrument level draws each form with it. t is the CSS throw itself (--strata-switch-t, read each frame from the root’s computed style, transitioned on the form’s spring), so CSS, Relief, Objects and Instrument move on one spring, and the frames of a throw are uniform updates: the native change is captured once, and the frames after it add no capture, upload or hit-geometry update. The CSS paint and motion of the parts the GPU draws (everything but the slot, which stays a groove on the face) is hidden and held (data-strata-dome), so a throw repaints nothing. The object takes the input’s hover and disabled state; pressing does not sink the parts, and the GPU draws no focus edge (the input’s outline is captured content), as for the Knob and the Fader.

  • The structure never depends on the state, which would rebuild the layer: both lit flags of a rocker or wall switch are always parts (the lit one on its facet or crown, the unlit one sunk 1 px into the solid under it, out of sight, its pivot kept), and the slide’s flag (a hair inside the block’s base at its Off stop, flagAt −13) is always there, covered by the block while it is off and shown as the block leaves it. The lit flags use the premium glowOn amber; the CSS and Relief flags use the theme’s accent until characters bring glowOn into the tokens.
  • Shadows stay in the box. The parts that stand still or slide (the slide’s block, the rocker’s bezel, the bat’s nut, the wall plate) cast their footprint at their mean height (core’s own caster) unless that shadow (2.8) would leave the input’s box, 1 px short of the edge: then the caster is the same footprint at the tallest height that fits (the slide’s block, 4 px inside its box above and below, casts at under a pixel). The tilting parts (paddles, lever, ball) cast none: their shadows fall on the bezel, plate or nut they stand on, whose own cast the face’s. The wall plate is gunmetal on a dark faceplate.

With feedback on (design language section 20), every throw plays the snap sound and a 12 ms pulse. It is off by default: createStrataTheme({ feedback: { sound: true } }), a scope’s feedback, or the feedback.sound and feedback.haptics tokens turn it on, and attachStrataFeedback({ sound, haptics, muted }) sets the page’s options. The sound is synthesised (no assets), and the audio context is created only on the visitor’s first gesture on a control whose sound is on; pulses answer only the visitor’s own input, and stop under reduced motion. The throw, the label and aria-checked always carry the state.

From the tokens: brushed metal from colour.knob and colour.edge, the slot and grip in the edge’s ink, polished bezels and plates as gradients, the paddles tipped with perspective() rotateX(), the lever as its head-on projection. Under forced-colors every part is a Canvas shape with a CanvasText edge on a CanvasText-bordered box, the slot, grip and lever CanvasText, and the moving part and flag Highlight when on, so the state shows by position and by fill; GrayText when disabled. prefers-contrast: more (and the contrast themes) ink the slot and grip in the text colour and edge every part in it.

  • Plain DOM (ui-switch-forms.test.mjs): for each form, six clicks anywhere (centre, corners, the moving part), two drags past centre (one ending outside the box), a drag held short of centre (no event), Space, Enter (no change) and the label, one input and one change per throw; 121 hits over each box, 0 misses; t in between during the throw; a disabled one stays. Touch at 390 px: taps and touch drags throw it, a held one does not, the page does not scroll. At 320 and 390 px: no overflow, boxes of at least 28 px, labels whole.
  • Preferences: axe in light, dark and face mode and under forced colours (every part drawn, Highlight when on), more contrast, reduced motion (instant).
  • Instrument (ui-switch-forms-instrument.test.mjs, DPR 1 and 2, headless and headful): the scene is version 4 with the documented parts per form and the same parts off and on; at t = 0, 0.5 and 1 at depth 0, 0.5 and 1, and in flight, every pixel the objects draw (against them hidden) lies inside an input’s box and native hits on those pixels find an input; a control run without the casters’ caps puts 1,240 shadow pixels outside the boxes at full depth (0 with them); real clicks and drags give fresh pixels inside the boxes only; the frames of a throw add 0 captures and 0 hit-geometry updates and nothing is stepped down; hover and disabled reach the parts and pressing does not; reduced motion snaps at once; touch at 390 and 320 px throws every form; the accessibility snapshot equals the DOM’s and axe passes.
  • GPU at Relief and Objects: the documented primitives; 0 hit mismatches on every form at DPR 1 and 2; fresh pixels after each throw, only in the boxes; 0 px outside the boxes at nine poses along the throw and in flight; the accessibility snapshot equal in DOM and GPU modes; axe on the GPU page.
  • Bindings and Astro (ui-switch-forms-bindings, ui-switch-forms-astro): the Forms example rendered exactly; controlled, uncontrolled, refs, attributes, Field, disabled, and form as the native form owner; without JavaScript the Astro forms are native switches that submit.
  • Feedback (ui-switch-forms-feedback): off by default (no AudioContext); on, created only after a gesture on a form; pulses only for the visitor’s own throws and not under reduced motion; muted silent; a scope’s token alone.
  • Unit (strata-switch-forms.unit.test.mjs): the springs and their CSS easing, the drag rule, the shadow fit, the Instrument parts with core’s validator and containment check, the synthesised sounds and the feedback tokens.