Authors: Nesh Gandhe, Kevin Babbitt, Limin Zhu
This document is a starting point for engaging the community and standards bodies in developing collaborative solutions fit for standardization. The API is in the early ideation and interest-gauging stage, and the solution/design will likely evolve over time.
Modern operating systems have embraced haptics as a core part of user experience — providing subtle, low-latency tactile cues that reinforce visual and auditory feedback. These signals improve confidence, precision, and delight in everyday interactions. The Web Haptics API proposes a semantic, cross-platform interface that connects web applications to native haptic capabilities. By focusing on intent-driven effects, the API enables web apps to deliver tactile feedback consistent with OS design principles, while preserving user privacy and security.
This proposal offers two complementary mechanisms:
@haptic at-rule inside style rules that fires haptic effects when the rule starts matching.navigator.playHaptics(effect, intensity) for interactions that require runtime logic or have no corresponding CSS state change.The navigator.vibrate() API exists today for basic haptics. However, it is mobile-centric, lacks broad engine and device support, and requires developers to manually program duration/pattern sequences — a low-level interface that doesn't map to the way designers think about haptic intent.
Beyond the limitations of the existing API, there is no declarative way for developers to add haptic feedback to common UI interactions — scroll-snap carousels, panel transitions, form validation — without JavaScript in the critical path.
The Web Haptics API uses a predefined list of effects with an optional intensity parameter, without exposing raw waveform authoring or low-level parameters like duration, sharpness, or ramp. Developers request a named effect, and the user agent maps it to the closest native capability (which may be a generic pattern if OS or hardware support is lacking). To minimize fingerprinting risks, the API does not currently allow developers to query haptics-capable hardware or available waveforms.
For both imperative and declarative APIs, target selection follows one shared model: dispatch to the most recent input device. If that device is not haptics-capable, no haptic is played. User agents do not reroute to another connected haptics-capable device.
Both the imperative and declarative paths share the same effect vocabulary.
| Value | Description | When to reach for it |
|---|---|---|
hint |
A light, subtle cue that signals something is interactive or an action may follow. | Focusing an input field, entering a drop zone during drag. |
edge |
A heavy boundary signal that indicates reaching the end of a range or hitting a limit. | Validation failure, pull-to-refresh threshold, scroll hitting a boundary. |
tick |
A firm pulse that marks discrete changes, like moving through a list or toggling a switch. | Scroll-snap landing, stepping through picker values, toggling a switch. |
align |
A crisp confirmation when an object locks into place or aligns with guides or edges. | Drag-to-snap, window snapping to screen edges, zoom snapping to 100%. |
The table below illustrates example mappings of the predefined effects (hint, edge, tick, align) to representative platform-native feedback patterns across Windows, macOS, iOS, and Android. These mappings are illustrative examples only. User agents may choose different mappings, including synthesizing custom effects from lower-level primitives and parameters. The API standardizes the developer-facing intent, while the underlying realization remains platform-defined.
| Web Haptics | Windows | macOS | iOS | Android |
|---|---|---|---|---|
| hint | hover | generic | light impact | gesture_threshold_deactivate |
| edge | collide | generic | soft impact | long_press |
| tick | step | generic | selection | segment_frequent_tick |
| align | align | alignment | rigid impact | segment_tick |
Intensity is always a normalized value between 0.0 and 1.0. If the platform exposes a system-level intensity setting, the effective intensity is system intensity × developer-specified intensity. Intensity defaults to 1.0 if left unspecified.
The imperative API is not gated behind a permission but requires sticky user activation.
navigator.playHaptics(effect, intensity);
Parameters:
effect — one of the predefined effect names: "hint", "edge", "tick", "align".intensity (optional) — a normalized value between 0.0 and 1.0. Defaults to 1.0.The API always returns undefined. No haptic is played if the last input device is not haptics-capable, and the user agent does not reroute to another connected haptics-capable device. If sticky user activation has expired, the call is silently ignored.
The declarative API introduces a nested @haptic at-rule that fires a haptic effect when the containing rule starts matching — no JavaScript required.
@haptic at-ruleThe @haptic at-rule nests inside a style rule and declares which effect to fire and at what intensity. The haptic fires once when the containing rule transitions into matching an element. It does not fire on initial style computation — only on subsequent transitions from not-matching to matching.
Syntax:
<selector> {
/* visual declarations */
@haptic <effect-name> <intensity>?;
}
<effect-name> — one of hint, edge, tick, align.<intensity> (optional) — a <number> between 0.0 and 1.0. Defaults to 1.0.Behavior:
@haptic tracks its own parent rule's selector independently. Two rules with the same effect on the same element both fire:
button:hover { color: blue; @haptic tick; }
button:active { scale: 0.95; @haptic tick; }
tick; pressing fires tick again — no collision.@starting-style does not trigger haptics as the explainer only scopes to reactive haptics feedback.@haptic rules start matching the same element in the same rendering update, at most one fires. Winner is determined by specificity, then document order.classList.add()) require sticky user activation.Example — button press:
button:active {
@haptic align;
}
@haptic can nest inside @keyframes blocks the same way, enabling multi-step choreography:
@keyframes bounce-settle {
0% { transform: translateY(-100%); }
40% { transform: translateY(0); @haptic edge; }
60% { transform: translateY(-20%); }
100% { transform: translateY(0); @haptic align 0.5; }
}
The following examples demonstrate how declarative haptics and the imperative API together cover common use cases.
A tactile press confirmation:
.add-to-cart:active {
scale: 0.95;
@haptic align 0.8;
}
A horizontal photo carousel with a tactile tick on each snap — using the :snapped pseudo-class from CSS Scroll Snap 2. Note: :snapped is currently draft-level and not yet widely implemented:
.carousel > .photo:snapped {
@haptic tick 0.5;
}
An app may want to use haptics when dragging a sidebar divider to a snap point. CSS can't express distance-threshold logic — this requires the imperative API:
// Sticky activation was granted by the pointerdown that started the drag.
divider.addEventListener('pointermove', (e) => {
const width = e.clientX;
if (!snapped && Math.abs(width - SNAP_POINT) < 4) {
snapped = true;
navigator.playHaptics?.('align', 0.8);
} else if (snapped && Math.abs(width - SNAP_POINT) >= 4) {
snapped = false;
}
});
The following extensions are out of scope for this initial proposal but represent natural next steps that could broaden the declarative surface.
The current set of four effects is intentionally small. If the effect vocabulary grows (e.g. platform-specific effects or developer-defined waveforms), the API should accommodate them without syntax changes.
prefers-haptics)A coarse user-preference media feature could be considered in a future phase (for example, prefers-haptics: reduce | no-preference) to help authors adapt non-essential feedback. This is deferred from v1 to avoid expanding API surface before concrete implementation and privacy review feedback.
exit / both)v1 declarative triggering is intentionally enter-only to keep the model simple and predictable. A future extension could add explicit phase control (e.g. exit and both) if concrete use cases justify the extra surface area and arbitration complexity.
We evaluated five declarative CSS models. All work with any selector type (pseudo-classes, classes, attributes). CSS has existing temporal mechanisms (transitions, animations) but no precedent for a one-shot, fire-and-forget side effect triggered by state change — so every model introduces some novelty. Given that, we prioritized syntax–semantics match — does the syntax honestly convey what the code does?
@haptic (primary) — an at-rule nested inside a style rule; fires when the parent selector starts matching. See Declarative API.@haptic-trigger — a top-level at-rule with a selector prelude; fires when the selector starts matching.
@haptic-trigger button:active { effect: align; intensity: 0.8; }
haptic-feedback) — a standard CSS property; fires when the computed value changes.
button:active { haptic-feedback: align 0.8; }
@haptic + haptic-name) — named effects defined in a top-level @haptic block, attached via haptic-name; fires when the computed haptic-name value changes.
@haptic bounce-land { effect: align; intensity: 0.8; }
button:active { haptic-name: bounce-land; }
transition-* family; fires on transitionend.
.sidebar { transition: transform 300ms; transition-haptic-effect: align; }
haptic-feedback: tick) can mislead developers into expecting the value to "stay active."tick on both check and uncheck of a toggle)? Computed-value models collapse to one value per element, so same-value transitions silently drop the haptic.✅ = good ⚠️ = limitation or workaround ❌ = problematic
| Model | Syntax–semantics match | Co-located | Concise | Re-trigger safe |
|---|---|---|---|---|
A. Nested @haptic (primary) |
✅ At-rule signals one-shot action | ✅ Same rule block | ✅ One-liner | ✅ Per-rule tracking |
B. Standalone @haptic-trigger |
✅ At-rule signals one-shot action | ❌ Separate block | ❌ Two blocks | ✅ Per-rule tracking |
| C. Computed-value property | ❌ Property syntax implies ongoing state | ✅ Same rule block | ✅ Most concise | ❌ Same-value collision |
| D. Animation-trigger | ⚠️ haptic-name reads as state, like C |
⚠️ Split (define + attach) | ❌ Define + attach | ⚠️ Workaround via distinct names |
| E. Transition-coupled | ⚠️ Natural with real transitions; synthetic 0ms hack without | ✅ Same rule block | ❌ Synthetic transitions | ⚠️ Needs distinct visual props |
navigator.vibrate — The existing vibrate() API accepts raw duration/pattern arrays (e.g. navigator.vibrate([100, 50, 200])) with no way to express semantic intent like "tick" or "align." Adding named effects would require method overloading or a new options-bag signature, complicating an already-shipped interface. Feature detection becomes awkward — typeof navigator.vibrate tells you the method exists but not whether it supports named effects. The pattern-based model also encourages developers to hand-tune durations per device, which is the opposite of the platform-adaptive approach this proposal targets. Finally, vibrate() lacks broad engine support (absent in Safari/WebKit) and carries existing abuse stigma that could slow adoption of legitimate haptic use cases.<button haptic-on-activate="tick">) — While HTML has adopted new attribute families (aria-*, popover, inert), haptic-on-* would require a separate attribute per interaction type (haptic-on-activate, haptic-on-toggle, haptic-on-focus, …), each needing dedicated spec text, parsing rules, and IDL definitions. More fundamentally, HTML attributes cannot compose with the cascade, media queries, or pseudo-class selectors — limiting expressiveness compared to a CSS-based approach.:snapped pseudo-class versus a dedicated scroll-snap trigger surface — CSS Scroll Snap 2 defines a :snapped pseudo-class that applies to snap children when the scroll container is snapped to them. In the primary model this naturally enables rules like .slide:snapped { @haptic tick; }. The main consideration is that :snapped is still a Working Draft and not yet widely implemented. If :snapped does not ship or is significantly delayed, a dedicated scroll-snap-haptic CSS property on the scroll container (e.g. .carousel { scroll-snap-haptic: tick 0.6; }) could be introduced as a self-contained fallback that does not depend on another in-progress spec.To avoid introducing a new fingerprinting vector, the API does not expose means to query haptics-capable devices, available effects, or whether a haptic was successfully played. No new media features are introduced in v1.
Anti-abuse: User agents may enforce throttling on both APIs. Haptics produce no lasting effect — the user can navigate away at any time. If abuse patterns emerge, user agents may suppress haptics entirely for the offending origin.
Imperative API: Requires sticky user activation. No permission gate.
Declarative API: Selector start-matching events from direct user interaction may fire haptics without additional activation checks. Activation checks apply to script-initiated selector start-matching events (e.g. classList.add() triggering a selector match), which require sticky user activation; if activation is not present, the trigger is ignored.
@haptic at-rule as the primary path — its at-rule syntax matches the one-shot, fire-and-forget nature of haptics, it co-locates with visual styles, and it avoids re-trigger pitfalls. The standalone @haptic-trigger separates haptics into dedicated blocks; the computed-value property is the most concise; the animation-trigger model offers reusable named patterns. We welcome feedback on which tradeoffs best serve developers.prefers-haptics: reduce | no-preference) if there is concrete use-case and privacy review support.scroll-snap-haptic property be added if :snapped does not ship? The current proposal relies on the :snapped pseudo-class from CSS Scroll Snap 2 for scroll-snap haptics (e.g. .slide:snapped { @haptic tick; }). If :snapped does not ship or is significantly delayed, a dedicated scroll-snap-haptic CSS property on the scroll container could serve as a self-contained fallback. We welcome feedback on whether the :snapped dependency is acceptable for v1.hint, edge, tick, align) is intentionally small. Feedback is needed on whether these four effects cover the most common interaction patterns and map well to native haptic primitives across platforms.playHaptics always returns undefined to avoid exposing device capabilities. Returning a boolean or promise could help developers debug, but risks leaking hardware information.This section provides reference to existing web and native haptics APIs to help inform the API design and platform supportability.
Known platform-specific native haptics APIs:
Relevant web APIs:
Relevant CSS specifications:
We have heard some early developer interest such as dragging divider to a snap point in Slack.
We intend to seek feedback via:
@haptic primary design and alternative declarative models.We acknowledge that this design will change and improve through input from browser vendors, standards bodies, accessibility advocates, and developers. Ongoing collaboration is essential to ensure the API meets diverse needs.
We only get here through the contributions of many — thank you to everyone who shares feedback and helps shape this work. Special thanks to: