Authors: Nesh Gandhe, 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-feedback property that fires haptic effects when an element enters a user-interaction pseudo-class state, plus a scroll-snap-haptic property for scroll-snap landings.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. Instead, haptics will be sent to the last input device if haptics-capable.
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%. |
none |
Explicitly disables haptic feedback. | Suppressing haptics on a "quiet" variant of a component. |
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. If sticky user activation has expired, the call is silently ignored.
The declarative API introduces two CSS properties that provide haptic feedback without requiring JavaScript:
haptic-feedback — fires a haptic when an element enters a user-interaction pseudo-class state (e.g. :active, :checked, :focus-visible).scroll-snap-haptic — fires a haptic each time the scroll position snaps to a snap point.haptic-feedback propertyThe haptic-feedback property fires a haptic when an element enters a pseudo-class state.
Syntax:
haptic-feedback: <effect-name> <intensity>?
<effect-name> — one of hint, edge, tick, align, none. Initial value: none.<intensity> (optional) — a <number> between 0.0 and 1.0, or a <percentage> between 0% and 100%. Defaults to 1.0 (or 100%).The haptic fires once when the element transitions into the matching pseudo-class state. It does not fire when leaving the state, nor does it fire repeatedly while the state persists. haptic-feedback applies to any pseudo-class that represents a dynamic state change — pseudo-classes an element can enter or leave due to user interaction, script-mediated changes, or browser-mediated actions (e.g. :active, :checked, :focus-visible, :open, :user-invalid, :popover-open). Structural pseudo-classes that reflect document position (:first-child, :nth-of-type(), etc.) do not trigger haptics. This pseudo-class categorization is a novel CSS concept — no existing property's behavior depends on what kind of pseudo-class appears in the selector — and the set of eligible pseudo-classes would need to be defined by the specification.
Example — button press:
button:active {
haptic-feedback: align;
}
For interactions that have no corresponding pseudo-class (e.g. drag thresholds, custom gestures), use the imperative API.
scroll-snap-haptic propertyThe scroll-snap-haptic property is set on a scroll container that uses CSS Scroll Snap. It configures the browser to produce a haptic effect each time the scroll position snaps to a defined snap point due to a user-initiated scroll gesture.
Syntax:
scroll-snap-haptic: <effect-name> <intensity>?
<effect-name> — one of hint, edge, tick, align, none. Initial value: none.<intensity> (optional) — a <number> between 0.0 and 1.0, or a <percentage> between 0% and 100%. Defaults to 1.0 (or 100%).The property applies to the scroll container, not individual snap children.
Note: If the
:snappedpseudo-class from CSS Scroll Snap 2 ships, scroll-snap haptics could instead be expressed viahaptic-feedbackon snap children (e.g..slide:snapped { haptic-feedback: tick; }), potentially making this property unnecessary. See Alternatives Considered and Open Questions.
.carousel {
scroll-snap-type: x mandatory;
scroll-snap-haptic: tick 0.6;
}
.carousel > .slide {
scroll-snap-align: center;
}
Feature detection works via @supports (e.g. @supports (haptic-feedback: tick)).
The following examples demonstrate how the pseudo-class model, scroll-snap model, and the imperative API together cover common haptic use cases.
A tactile press confirmation — one line of CSS:
.add-to-cart:active {
haptic-feedback: align 0.8;
}
A horizontal story carousel with a tactile tick on each swipe — one line of CSS:
.story-carousel {
scroll-snap-type: x mandatory;
scroll-snap-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.
@keyframes descriptors)Properties haptic-effect and haptic-intensity inside @keyframes blocks would embed haptic cues at specific keyframe offsets, hooking into the existing CSS Animations lifecycle. This enables multi-step haptic choreography — a capability that cannot be expressed with pseudo-class or scroll-snap haptics.
@keyframes bounce-settle {
0% { transform: translateY(-100%); }
40% { haptic-effect: edge; transform: translateY(0); }
60% { transform: translateY(-20%); }
100% { haptic-effect: align; haptic-intensity: 0.5; transform: translateY(0); }
}
An edge fires at 40% when the element hits the baseline, and a softer align at 100% when it settles.
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.
Transition-based haptics (transition-haptic-effect / transition-haptic-intensity). An alternative declarative model where haptics are integrated into the CSS Transitions lifecycle. Longhands transition-haptic-effect and transition-haptic-intensity would pair with transition-property to fire haptics when transitions complete (transitionend):
.sidebar {
transition: transform 300ms ease;
transition-haptic-effect: align;
}
Strengths: This model reuses established transition-* conventions and handles bidirectional state changes naturally — a sidebar opening and closing both produce a tactile cue from a single declaration, with no JavaScript needed.
Weaknesses: It couples haptic feedback to visual transitions. Interactions with no animated property (button presses, checkbox toggles) would need a synthetic transition. Haptics can target a specific property (e.g. only fire on transform, not opacity), but transitionend does not distinguish what caused the transition — so a hover and a class toggle that both animate transform both fire the haptic.
Why we chose pseudo-class haptics as the primary model: It covers the most common discrete interactions (presses, toggles, focus) with minimal syntax, and the trigger is scoped to a specific pseudo-class rather than any property change. Its main tradeoffs: it introduces a novel CSS concept — a property whose behavior depends on what category of pseudo-class appears in the selector (no existing CSS property works this way) — and it cannot declaratively express haptics for class-toggled state changes, which require the imperative API. We welcome feedback on whether the pseudo-class model is sufficient on its own, or whether transition-based haptics should also be included — as a complement or a replacement.
HTML attributes (e.g. <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.
Extending 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.
Using the :snapped pseudo-class instead of a separate scroll-snap-haptic property — CSS Scroll Snap 2 defines a :snapped pseudo-class that applies to snap children when the scroll container is snapped to them. Because :snapped is a dynamic state-change pseudo-class, it would work directly with haptic-feedback — e.g. .slide:snapped { haptic-feedback: tick 0.6; } — eliminating the need for a separate scroll-snap property. The main consideration is that :snapped is still a Working Draft and not yet widely implemented, so scroll-snap-haptic provides a self-contained path that does not depend on another in-progress spec.
Pointer-event based API (previous explainer) — This was the team's earlier proposal and has the advantage of simplicity for pointer-driven interactions — events are familiar to developers and the mapping from input to haptic is explicit. However, it is purely imperative; it tightly couples haptics to specific input events, so common interactions like checkbox toggles and scroll-snap landings always require JavaScript. No declarative path, no cascade composition.
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.
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: No user activation required. Pseudo-class state changes and scroll-snap landings fire haptics regardless of whether the change was user- or script-initiated. This keeps the model simple — user agents need not distinguish the source.
scroll-snap-haptic be dropped in favor of :snapped + haptic-feedback? The :snapped pseudo-class from CSS Scroll Snap 2 would let authors write .slide:snapped { haptic-feedback: tick; }, unifying scroll-snap haptics with the pseudo-class model and reducing the API surface to a single CSS property. However, :snapped is not yet widely implemented. We welcome feedback on whether to keep scroll-snap-haptic as a self-contained feature, drop it in anticipation of :snapped, or support both.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:
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: