Fader
Instrument, wave 1 (plan). Follows the anatomy template and the conventions.
1. Purpose and native element
Section titled “1. Purpose and native element”Picks one number from a range by sliding, as on mixing desks and control hardware: a channel’s level, a send, a
crossfade. Renders a native <input type="range">, vertical by default (writing-mode: vertical-lr,
direction: rtl, aria-orientation="vertical"), following the WAI-ARIA
Slider pattern. The native element has the role, the value, the
orientation, the keys, the drag and form participation; Strata adds aria-valuetext for units and the behaviour in
section 8.
orientation="horizontal" lays it along the page (minimum at the left). detents makes it stepped: that many
positions, and the native step is the range over detents − 1. The Fader is the Slider’s physical
sibling: the same native element, attributes and keys, drawn as hardware.
2. Object
Section titled “2. Object”A slot in the faceplate with a capped carriage, and the scale printed beside the slot. Labels are printed on the face, never on the carriage (design language principle 2).
| Part | Shape | Height at 50% | Moves |
|---|---|---|---|
| Track (the native box) | 46 across for the slot band plus the scale; along the travel, the travel plus the thumb (74): 90 × 254 at medium | 0 | No |
| Slot | A dark cut 5 wide, from 3 px before the carriage centre’s first stop to 3 px past its last, in the band’s middle | Relief: elevation.well (a groove) |
No |
| Scale | A tick (7 long, 1.2 thick) and a numeral at the carriage centre’s place for each mark, beside the band on the side its shadow falls (right of a vertical fader, below a horizontal one) | 0 | No |
| Carriage | 30 across, 42 along, radius 3, a 1 px edge border |
Relief and Objects: elevation.raised (3) |
Slides size.fader (180 at medium) with the value |
| Grip | An index line 1.6 wide across the carriage, 6 px in from each side | On the carriage | With the carriage |
-
The thumb is the carriage. The native thumb is transparent, as wide as the track and as long as the carriage plus a 16 px margin at each end (74), and the track is the travel plus the thumb, so the thumb’s centre and the carriage’s are the same point at every value (I0.2): a press on the drawn carriage is a press on the native thumb, and the value follows the pointer one to one.
-
Why the margin is 16, not 8. The box rule’s margin for the carriage’s own drift is 8 (band 46 for a carriage 30 across). Along the travel the box also holds the carriage’s shadow on the face at the Instrument level, which reaches 15.5 px down at 50% depth under the default light, and the scale sits on the side the shadow falls, so it holds it across (
faderShadowReachingpu/instrument-fader.ts). At Relief and Objects the carriage’s shadow reaches 7 px. -
At the Instrument level (
level: "instrument", core’sinstrumentmaterial) the GPU helper draws the carriage as the Instrument object table’s Fader, built byfaderObjectingpu/instrument-fader.tsfrom the laid-out slot, bounded by the input’s box (the slot stays a groove on the face, the scale captured content):Part Profile (50% depth; heights × 0.6, 1, 1.6 with depth) Material (premium) Pose carriagePrism 30 × 42 (across × along), 12 high, radius 3, taper 0.6, a 2 px chamfer faderCap: brushed (239, 240, 241), roughness 0.3, a linear brush 0.9 with the hairline grain 0.15slidehalf the travel each way from its middlechamferThe carriage’s chamfer band, a hair outside it faderChamfer: the cap’s metal polished, roughness 0.06As the carriage capThe carriage’s top inside the chamfer, brushed faderCapAs the carriage gripPrism 18.8 × 1.6 (across the travel), 0.1 high on the top faderInk: (38, 40, 44), roughness 0.6As the carriage tis the value’s place along the range, driven byspring.fader(420 / 41, critically damped) in fixed 4 ms steps, clamped at the stops, instant under reduced motion; the slot and the scale stay on the face, and only the carriage casts. Every vertex stays at least 0.5 px inside the box over the whole travel in every corner of the reference stage at every depth (core’scheckContainment). The carriage’s caster is core’s own (its footprint at its mean height, following the pose) while that shadow stays in the box. Past that (above 50% depth under the default light)faderCasterHeightcaps it: the helper passes the carriage’s footprint where it stands now at the tallest mean height whose shadow stays inside the box from either stop, 6.2 for a vertical fader (16 px down) and 6.84 for a horizontal one (its 16 px right of the maximum), against 9.6 uncapped, whose shadow would reach 24 px down. The carriage takes its input’s hover and disabled state; core changes no uncaptured part with them, so it stays at its height on hover, and it does not sink while dragged.
3. Faces and materials
Section titled “3. Faces and materials”| Face | Colour role | Text role | Material role |
|---|---|---|---|
| Slot | edge mixed 70% towards black (text in the high-contrast themes) |
— | rail (metal), a groove |
| Carriage | knob, a 1 px edge border |
— | knob (satin); brushed hairlines and a light top edge in CSS |
| Grip | edge mixed 55% towards black (text in the high-contrast themes) |
— | None |
| Ticks | text |
— | None (printed) |
| Numerals | — | muted |
None (printed) |
| Legend | — | text (small, strong, uppercase) |
None (printed) |
The grip keeps at least 3:1 against the carriage in every theme, and the carriage’s edge border and the slot keep it
apart from the faceplate (non-text contrast). data-strata-material on the fader overrides the carriage’s material.
4. Elevation per state
Section titled “4. Elevation per state”| State | Relief | Objects | CSS |
|---|---|---|---|
| Rest | Carriage at elevation.raised; slot a groove at elevation.well |
Carriage slab on the panel; slot a groove on the face | The relief recipe on the carriage, the slot’s inner shadow |
| Hover | Carriage height × state.hoverHeight; interior +state.hoverLift |
Slab rises by a quarter of its height | Larger shadow, brightness(1.04) |
| Pressed (dragging) | As rest: a carriage does not sink under a finger | As rest | As rest |
| Focus-visible | The CSS ring round the track, captured (no GPU focus edge) | Same | outline: focus.width solid accent, offset focus.offset |
| Disabled | Height 0, no bevel gain, the track at 38% | Flattened | Flat, the track at 38% |
| Value change | The carriage moves: a new rect and fresh captured pixels | The slab moves | top (or left) follows --strata-fader-value; a detented carriage settles over motion.fast without overshoot |
Selected, invalid and busy do not apply. Under prefers-reduced-motion the carriage moves at once. At the Instrument
level the carriage stays at its height on hover and slides on its spring; focus is the same CSS ring.
5. Relief primitives
Section titled “5. Relief primitives”- One
groove: the slot, vertical (or horizontal) as laid out. Oneraisedface: the carriage, a rounded rectangle (radius 3). At the Objects level the carriage is aslabstanding on the panel, and the slot stays a groove on the face. - The
<input>carries nodata-strata-face: a range input with one is drawn as a Slider’s horizontal rail and puck, which is wrong for a vertical one (I0.7, finding 2). The slot and the carriage are their own faces instead, and take the input’s hover and disabled state (the GPU helper finds the input in the track); the carriage’s material role isknob, the slot’srail.
6. Tokens it reads
Section titled “6. Tokens it reads”| Token | Prop |
|---|---|
size.fader (120†, 180, 240†, --strata-fader-travel): the carriage centre’s travel |
size, travel (per fader) |
fader.snap (2 px) |
— |
colour.knob, colour.edge, colour.text, colour.muted, colour.accent (focus) |
— |
elevation.raised, elevation.well, state.*, interior.max, bevel.*, shadow.* |
— |
material.roles.knob, material.roles.rail |
material |
font.size.caption (scale), font.size.small (legend), font.weight.strong, space.1 |
— |
focus.width, focus.offset, motion.fast, motion.easing.standard |
— |
level |
level |
size.fader’s small and large values are this page’s (†, provisional); the medium travel is the design language’s
180. The travel grows by as much as 15em of the caption size grows past 180 px (12 px captions), so it grows with
text. Props: label (the legend), value / defaultValue (or v-model), min (0), max (100), step (1),
detents, orientation (vertical), travel, marks (a count, or values with labels; 5 by default, one per
detent up to 12), unit or format (aria-valuetext), resetValue (the double-click target; the initial value by
default), id, name, form, disabled, and in React inputRef (Vue exposes root and input).
7. DOM structure and overlay needs
Section titled “7. DOM structure and overlay needs”<div class="ug-fader" style="--strata-fader-value: 0.9091;"> <div class="ug-fader__track"> <span class="ug-fader__scale" aria-hidden="true"> <span class="ug-fader__mark" data-strata-value="6" style="--strata-fader-at: 1;">6</span> <span class="ug-fader__mark" data-strata-value="0" style="--strata-fader-at: 0.9091;">0</span> <span class="ug-fader__mark" data-strata-value="-20" style="--strata-fader-at: 0.6061;">−20</span> <span class="ug-fader__mark" data-strata-value="-40" style="--strata-fader-at: 0.303;">−40</span> <span class="ug-fader__mark" data-strata-value="-60" style="--strata-fader-at: 0;">−60</span> </span> <span class="ug-fader__slot" data-strata-face="groove" aria-hidden="true"></span> <span class="ug-fader__carriage" data-strata-face="raised" aria-hidden="true"><span class="ug-fader__grip"></span></span> <input class="ug-fader__input" type="range" id="level" name="level" min="-60" max="6" step="1" value="0" aria-orientation="vertical" data-strata-default="0" data-strata-unit=" dB" aria-valuetext="0 dB"> </div> <label class="ug-fader__label" for="level">Level</label></div>- The
.ug-faderroot is the boundary: React and Vue refs,classandstylereach it.--strata-fader-value(0–1) places the carriage; atravelprop adds--strata-fader-travel.ug-fader--horizontallays it along the page (the input then has noaria-orientation),ug-fader--detentedmakes the carriage settle. The behaviour keeps--strata-fader-valuein step with the value; callsyncStrataFader(input)after settingvaluefrom script. - The input is transparent (
appearance: none, its thumb the carriage’s footprint,touch-action: pan-x, orpan-ywhen horizontal) and covers the whole track, above the drawn parts, which take no pointer events: every press lands on the original control. - A mark is a numeral with its tick (
::before);data-strata-valueis what a tap near it sets exactly. data-strata-defaultis the double-click target;data-strata-unitmakes the behaviour keeparia-valuetextas the value and the unit (“−22 dB”, with U+2212). Without either, the number alone is read.- The legend is a
<label for>printed under the track. No overlay.
8. Keyboard and screen reader
Section titled “8. Keyboard and screen reader”| Input | Action |
|---|---|
| Up, Right / Down, Left | Increase / decrease by step (native, either orientation) |
| PageUp, PageDown | Larger step, as the browser defines it (native: 10% of the range) |
| Home, End | Minimum, maximum (native) |
| Drag the carriage (mouse, pen or touch) | Native thumb drag: the value follows the pointer one to one along the travel |
| Click or tap on the track (the slot, the scale or the thumb’s margin) | Sets the value under the carriage’s centre (native track click; on the thumb’s margin, on release); within fader.snap (2 px) of a printed mark, exactly its value. A tap on the carriage changes nothing |
| Wheel over the track | One notch (step, at least 1% of the range) per wheel notch, up or right raising it (touchpads add up to one); the page does not scroll. Ctrl+wheel zooms as usual |
| Double-click | Returns to data-strata-default |
| Tab, Shift+Tab | Move focus (native order) |
- Role
slider(native), named by the legend, with the value, minimum, maximum, the orientation andaria-valuetextwhen a unit or a format gives one. Automation cannot heararia-valuetexton a native range (the accessibility tree reports the number), so the manual screen-reader script for “a fader with units” is VAL-002’s, in open issues, until performed. - A click on the track is the single-pointer alternative to the drag (WCAG 2.5.7).
- Every change goes through the input’s
value:inputwhile it changes,changeat the end of a gesture, so forms and bindings see native events. The behaviour isattachStrataFadersfrom@underglass/ui/dom(one set of delegated listeners per document, among them the blocking document-level wheel listener);attachStrataand the React, Vue and Astro components attach it. Without script the fader is a native vertical slider whose drag, track click and keys work; the carriage stays where it was rendered. - Feedback (design language section 20): a detented fader’s carriage
passing a detent plays
tickthroughstrataFeedback(one per change, however many detents it passes). Sound and haptics are both off by default (feedback.sound,feedback.haptics, orattachStrataFeedback), so nothing plays unless the application or a scope turns them on; a fader without detents never ticks. disableduses the native attribute, which removes the fader from the tab order and from pointer input.
9. CSS presentation
Section titled “9. CSS presentation”The relief recipe on the carriage (raised, lit from the light, with a cast shadow) with the I1 prototype’s brushed
finish (hairlines across the travel and a light top edge, scaled by the bevel gain: 0 in the high-contrast themes), and
the slot as a dark cut with an inner shadow and a lit lower lip. In face mode the carriage is flat colour.knob and
the slot flat, and the GPU paints the relief. Marks sit on whole pixels (CSS round()). The track is at least 74 px
along and 90 across (vertical), far above target.min (24); the carriage, the target a finger aims at, is 30 × 42.
- Reflow: a vertical fader is an inline column 90 px wide, so several fit a 320 px viewport; a horizontal one is
as wide as its travel plus the thumb and shrinks to its container (
max-width: 100%), the carriage and the marks placed in percentages of the track. The legend wraps. - Text size: the travel grows by as much as 15em grows past 180 px, and the scale’s band is 2.5em + 14 px, so 200% text doubles a medium fader’s travel, keeps its marks apart and every numeral inside the track.
forced-colors: active: the slot isCanvasText, the carriageCanvaswith a 1 pxCanvasTextborder and aCanvasTextgrip, the ticks and numeralsCanvasText, focus aHighlightring,GrayTextwhen disabled.prefers-contrast: more: the high-contrast theme applies through the tokens; the grip, the slot and the numerals takecolour.text.prefers-reduced-motion: reduce: the detent settle is off; the carriage moves at once.
10. Tests
Section titled “10. Tests”- Plain DOM (
ui-fader.test.mjs, real input): a real drag of the drawn carriage (the native thumb) moves the value one to one with the carriage under the pointer; clicks on the slot and on printed numerals set their values, also on the thumb’s margin; a tap off a numeral withinfader.snapsnaps; the wheel steps and the page does not scroll; native keys; the double-click reset;aria-valuetextwith units; detents snap;inputandchangefire; the horizontal fader likewise; touch taps on numerals and a touch drag of the carriage at 390 px (Playwright touch emulation); 320 and 390 px viewports (no horizontal overflow, targets at least 24 px, print inside the viewport); 200% text; axe in light and dark (and face mode); forced colours draw a visible control; reduced motion. - GPU (
ui-fader.test.mjs): at Relief and Objects, GPU stays active through value changes; the slot is a vertical (or horizontal) groove and the carriage a raised face or slab; pixels change after each value change and only inside the track; the carriage and its shadow stay inside the track over the whole travel (decoded pixels against the carriage hidden, at both stops and mid-travel); native hits on a grid over the track land on the input (0 mismatches) at DPR 1 and 2; the accessibility snapshot is the same in DOM and GPU modes. - GPU, Instrument level (
ui-fader.test.mjs, DPR 1 and 2): the scene is version 4 with the three carriages and the Reset key as parts and the slots as grooves; GPU stays on and nothing is stepped down through every step; at both stops and mid-travel, and at both stops at full depth (the capped caster), the parts and their shadows change 0 pixels outside the inputs’ boxes against the carriages hidden (8,021–8,443 part pixels inside at DPR 1, 31,968–34,008 at DPR 2), and native hits on every part pixel land on a fader’s input (0 mismatches); real drags, clicks, keys, the wheel, the reset and the detented fader give fresh pixels in the track and none outside; hover and disabled reach the carriage; 300 frames of sliding with a native change every 15 add a capture only for the native changes (20 captures, 0 in sliding frames, 0 hit-geometry updates); reduced motion moves the carriage at once. Without the cap the bottom stop at full depth puts 561 shadow pixels outside the level fader’s box (a control run). Checked in a headful Canary window as well. - Feedback (
ui-fader.test.mjs): with feedback off a detent passed by a real key pulses nothing; with haptics on, each change of a detented fader by the visitor’s own key or click is one 5 ms pulse, none at a stop, and a fader without detents never pulses. - Bindings (
ui-fader-bindings.test.mjs,ui-fader-astro.test.mjs): React, Vue and Astro render the markup above; controlled and uncontrolled values,onValueChange/v-model, the root ref,classandstyle. - Unit (
strata-fader.unit.test.mjs): the value maths, marks and styles; the fader’s object validated by core’svalidateInstrumentScene, contained over its whole travel in every corner of the reference stage at every depth (core’scheckContainment), the carriage’s face shadow inside its box at 50% depth and, capped, at full depth over the whole travel, andspring.fader. - Not yet: a parity budget against I0.6’s, a low-end GPU, sound heard on a device, and the manual screen-reader script.