Haptics
LAW — locked, CI-gatedHaptics
Section titled “Haptics”Status: ratified chapter; the constitution’s §7 states the rule as law, and decision 0003 makes haptics a first-class variable wherever hardware exists. This page is rendered from the chapter, docs/foundations/haptics.md, which stays canonical. Items the chapter flags [proposal] are proposals inside ratified law and render flagged here too.
The vocabulary
Section titled “The vocabulary”Eight named events. Each earns its place below; cuts and their reasons are at the end.
1. select — “the lightest thing we do”
Section titled “1. select — “the lightest thing we do””- Character: one crisp tick, ~10–20 ms, low strength, high sharpness.
- Fires: when a highlighted/selected item changes under the user’s finger or cursor (picker rows, menu items, tabs).
- Never fires: on scroll position changes, on render, or for anything the user didn’t deliberately move between.
- Platform mapping: iOS —
UISelectionFeedbackGenerator; Android —PRIMITIVE_TICKat scale 0.5 (orCLOCK_TICK); web —navigator.vibrate(10); desktop — nothing (no actuator); macOS trackpad — HIG selection pattern.
2. toggle — “it moved”
Section titled “2. toggle — “it moved””- Character: one soft click, ~20–40 ms, low-medium strength.
- Fires: when a switch/toggle/checkbox the user touched changes state.
- Never fires: when state changes programmatically (sync, defaults loading), or repeatedly while dragging — that’s
detent’s job. - Platform mapping: iOS —
UIImpactFeedbackGenerator(.light); Android —PRIMITIVE_CLICKat 0.7 (orVIRTUAL_KEY); web —vibrate(20); desktop — nothing.
3. detent — “you’re passing the marks”
Section titled “3. detent — “you’re passing the marks””- Character: repeated light ticks, each like
select, rate-limited. - Fires: while scrubbing a slider or dial as it passes snap points or scale marks.
- Never fires: during free scrub with no marks; faster than ~10 ticks/second (coalesce instead — see Restraint law); on devices where ticks smear into buzz (most Android motors — degrade to fewer, stronger ticks or nothing).
- Platform mapping: iOS —
UISelectionFeedbackGeneratorreused; Android —PRIMITIVE_TICKsequences, following AOSP’s sliding-texture guidance (crisp, subtle, or the intervals wipe out into one long buzz); web —vibrate([8, 60, 8, 60, 8])-style short patterns; desktop — nothing. - Source for the rate rule: Android’s haptics UX foundation (source.android.com): buzzy-not-crisp repetition merges into one long buzz; energy builds up if amplitude isn’t subtle.
4. snap-to-place — “it landed”
Section titled “4. snap-to-place — “it landed””- Character: one confident click with a quick settle, ~40 ms.
- Fires: the moment a dragged/placed item aligns or docks.
- Never fires: when the placement is rejected or the item springs back — that’s
apply-failed. - Platform mapping: iOS —
UIImpactFeedbackGenerator(.medium); Android —PRIMITIVE_CLICKat 1.0; web —vibrate([20, 30, 10]); macOS trackpad — HIG alignment pattern; desktop — nothing.
5. apply-succeeded — “done, and it worked”
Section titled “5. apply-succeeded — “done, and it worked””- Character: a warm two-step confirm, ~120 ms total. The only event allowed to feel “nice.”
- Fires: when a user-initiated apply/save/export completes successfully.
- Never fires: for background syncs, autosaves, or anything the user didn’t ask for. Success they didn’t request is noise.
- Platform mapping: iOS —
UINotificationFeedbackGenerator(.success); Android —CONFIRM(HapticFeedbackConstants); web —vibrate([15, 40, 15]); desktop — nothing.
6. apply-failed — “that didn’t take”
Section titled “6. apply-failed — “that didn’t take””- Character: a low double-tap, ~200 ms, unmistakably not
apply-succeeded. - Fires: once per user-initiated failure or auto-revert — alongside the visual, never instead of it.
- Never fires: more than once per failure; for validation the user hasn’t submitted yet; on a loop.
- Platform mapping: iOS —
UINotificationFeedbackGenerator(.error); Android —REJECT; web —vibrate([60, 60, 60]); desktop — nothing.
7. destructive-confirm — “are you sure, physically”
Section titled “7. destructive-confirm — “are you sure, physically””- Character: one heavy press-feel, ~100 ms, the strongest event in the vocabulary. Deliberately heavier than anything else so weight means danger.
- Fires: only at the confirm step of a destructive action (delete, wipe, irreversible send).
- Never fires: for reversible actions, for the first tap (only the confirm), or more than once per action. Never as a substitute for a real confirmation dialog.
- Platform mapping: iOS —
UIImpactFeedbackGenerator(.heavy)(or.rigidon capable hardware); Android —PRIMITIVE_THUDat 1.0; web —vibrate([80, 50, 80]); desktop — nothing. - [proposal] On controllers with resistance triggers (DualSense L2/R2), map this to a trigger-resistance gate instead of vibration — pressure as the confirm. Open verdict; unverified for the device chapters.
8. countdown-final — “last call”
Section titled “8. countdown-final — “last call””- Character: one slow rise, ~300 ms, then a single sharp end. The only event allowed duration.
- Fires: once, at the final warning of a countdown the user started.
- Never fires: on every tick of the countdown (that way lies madness); for countdowns the user didn’t start; ever more than once per countdown.
- Platform mapping: iOS — custom Core Haptics pattern (continuous swell + transient end); Android —
PRIMITIVE_SLOW_RISEthenPRIMITIVE_CLICK; web —vibrate([120])as a degraded single pulse (web can’t swell); desktop — nothing.
Cut with the why
Section titled “Cut with the why”hover— cut: hover has no touch surface on most devices; firing on mouse hover would train users that haptics are random.scroll-tick— cut: continuous scroll produces exactly the smear Android warns about;detentcovers marked scrubbing, which is the only scroll-adjacent case that survives.key-tap(typing) — cut: the OS already owns keyboard feel; glaze layering its own on top doubles every keystroke.notification— cut: notifications belong to the OS, not to glaze; glaze never buzzes for attention uninvited.
Restraint law (applies to all eight)
Section titled “Restraint law (applies to all eight)”- Never ambient. A haptic only fires in direct response to something the user did or asked to be told about. No idle buzzing, no marketing buzzing, no “delight” loops.
- Never the only signal. Visual and copy always carry the meaning; haptics are the underline, never the sentence. (Apple’s HIG: haptics complement, never replace.)
- Debounce and coalesce. Rapid repeats merge: max ~10 discrete ticks/second; repeated
selectwithin 50 ms windows coalesce to one. Gaps under ~50 ms aren’t perceptible as gaps anyway (Android guidance) — so don’t pretend they are. - Three levels, max. Never design more than three perceptible intensities of the same primitive; strength ratios below ~1.4× aren’t felt as different (Android: scales like 0.5 / 0.7 / 1.0).
- Respect the OS and the off switch. Honor the system haptics toggle, silent/do-not-disturb posture, and a glaze-level haptics off switch that kills all eight events. Web: haptics require a user gesture and a visible page — design as if they might never fire.
- Degrade to nothing, gracefully. Most Android motors, all current desktops, and Safari have poor or no haptic expression. On those, glaze fires nothing rather than a sad buzz. Absence is a valid rendering.
- Sound doubling noted, not designed. Where a haptic would double a sound (success chimes, error tones, countdown beeps), the haptic stays the quieter partner and sound design itself is out of scope for this chapter.
Platform reality table
Section titled “Platform reality table”| Platform | What it can actually express | Glaze posture |
|---|---|---|
| iOS / iPadOS | Rich: UIFeedbackGenerator families (impact light/medium/heavy + soft/rigid, notification success/warning/error, selection) plus fully custom Core Haptics (transient + continuous events, intensity/sharpness 0–1, AHAP files, audio sync). | Full vocabulary. Source: Apple HIG (Haptics) + Core Haptics docs. |
| Android | Building blocks, hardware-dependent: HapticFeedbackConstants (CLOCK_TICK, TEXT_HANDLE_MOVE, KEYBOARD_TAP, LONG_PRESS, CONFIRM, REJECT, CONTEXT_CLICK…) and VibrationEffect.Composition primitives (CLICK, LOW_TICK, TICK, QUICK_FALL, QUICK_RISE, SLOW_RISE, SPIN, THUD), scalable 0–1, sequences with ≥50 ms gaps. Quality varies wildly by motor. Requires CAP_COMPOSE_EFFECTS for full compositions. | Full vocabulary on capable hardware; degrade per Restraint law 6. Sources: developer.android.com (VibrationEffect.Composition, custom haptic effects), source.android.com UX foundation. |
| Web | On/off only: navigator.vibrate() takes a duration or on/off pattern array. Requires user gesture, visible page; patterns capped (~10 s). Chromium-based browsers only — Safari doesn’t implement it, Firefox removed it. No intensity, no sharpness. | Progressive enhancement: single-pulse degradations only (select/toggle/apply-succeeded keep a pulse; countdown-final degrades to one pulse; the rest fire nothing). Source: MDN-adjacent coverage + swmansion.com web-haptics writeup. |
| Windows / macOS desktop | Effectively nothing: no vibration hardware in current desktops. macOS trackpads expose three HIG patterns (alignment, level-change, generic) via NSHapticFeedbackPerformer. | Nothing by default; snap-to-place may use the trackpad alignment pattern on macOS. Source: Apple HIG (macOS haptics). |
| Game controllers | Rich but app-specific: DualSense uses dual voice-coil actuators (waveform-driven, like tiny speakers fed by the audio engine) plus adaptive L2/R2 triggers with programmable resistance; Switch HD Rumble uses linear resonant actuators for textured sensations. No standard cross-title API glaze can rely on. | [proposal] Controller chapter later; only destructive-confirm has a proposed trigger mapping. Unverified for glaze’s scope — flagged, not asserted. |
Sources
Section titled “Sources”- Apple Human Interface Guidelines — Haptics (selection/notification/impact categories; macOS trackpad patterns; “complement, don’t replace” guidance).
- Apple Core Haptics documentation — transient vs continuous events, intensity/sharpness 0–1, AHAP format, audio event sync.
- Android Developers —
VibrationEffect.CompositionAPI reference (primitive constants and their documented sensations: CLICK, LOW_TICK, TICK, QUICK_FALL, QUICK_RISE, SLOW_RISE, SPIN, THUD). - Android Developers — “Create custom haptic effects” (scales 0–1, strength ratios ≥1.4, max three perceptible levels, ≥50 ms gaps).
- AOSP source.android.com — haptics UX foundation (sliding textures: crisp + subtle or repetition smears;
CLOCK_TICK/TEXT_HANDLE_MOVEfor swipe textures). - swmansion.com — “Why the web deliberately refuses to be as tactile as native apps” (user-gesture requirement, page-visibility suppression, ~10 s pattern cap, Chromium-only reality).
- Sony DualSense product materials and user manual — dual actuators replacing rumble motors, adaptive trigger resistance, per-title custom effects, three intensity levels + off in system settings.
open questions
Section titled “open questions”- Does glaze even have a controller device chapter? If not, the DualSense proposal in
destructive-confirmshould be cut, not kept as a proposal. - A fuller in-house research packet exists in the working record; fold in on ratification review, since this chapter may duplicate or contradict it.
- Is
countdown-final’s single allowed long event acceptable, or should the vocabulary stay strictly under ~200 ms per event?
Eight events, all named, all restrained.
Canonical source: docs/foundations/haptics.md; the constitution’s §7 is the summary of this chapter inside the language document.