Skip to content

Revise Haptics explainer to include declarative method#1282

Open
liminzhu wants to merge 8 commits intomainfrom
user/limzh/haptics
Open

Revise Haptics explainer to include declarative method#1282
liminzhu wants to merge 8 commits intomainfrom
user/limzh/haptics

Conversation

@liminzhu
Copy link
Member

No description provided.


Both the imperative and declarative paths share the same effect vocabulary.

### Effect Vocabulary
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To help explain where this set came from and help those familiar with a particular platform's haptics already, consider adding a proposed mapping to the various platform presets:
e.g.

https://learn.microsoft.com/en-us/uwp/api/windows.devices.haptics.knownsimplehapticscontrollerwaveforms?view=winrt-26100#properties

https://developer.apple.com/documentation/appkit/nshapticfeedbackmanager/feedbackpattern#Constants

etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an example table.


Future work may address transition-end haptics (`haptic-transition`), multi-step haptic sequences (`@haptic` at-rule).

## Alternatives Considered
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you explore haptics being triggered as part of animations or transitions?

(Caveat that I haven't given it a lot of thought yet)

Initial spitball proposal (very rough):

  • New animation-haptic-effect and animation-haptic-intensity attributes. Assignable with the values you'd expect from your explainer, and assignable by all the rules of normal CSS attributes. These always describe what the behavior should be, just like animation-duration can be set anytime and computed anytime.
  • Anytime an animation is performed, the UA checks the effective animation-haptic-effect on the element being animated. If there is one, it starts the new haptic event with the start of the animation. If the animation is cancelled, so is the haptic event.
  • No restrictions on user interaction/etc. (other than the initial one for page engagement)

Open questions:

  • How should competing haptic events be handled? Probably should only have a single winner at any point in time. Last started one wins? Most intense one wins for ties? Longest duration one wins for ties?
  • If the element being animated is not visible, should the haptic event be suppressed?

Copy link
Member Author

@liminzhu liminzhu Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really interesting thought and I originally had this as a future extension to the proposal. I generated an alternative explainer with transition/animation as the primary declarative approach just so that we have something to compare. Been tweaking on a comparison table below which feels reasonable. I think they are complementary, and the question is what we lead with in the beginning (think we should scope to one for now).

Right now I'm leaning more on the pseudo-class, given it's more ergonomic and more broadly applicable but with drawback on standards novelty. Open to difference in opinion @mhochk , and I'm curious if @kbabbitt have thoughts on this too.

Regardless of which we lead with, I plan to flesh out the alternative and thought process a bit more in the alternatives considered section so that the reasoning is clear and we can easily course-correct if we get more feedback.


Shared Foundation

Both approaches propose the same imperative JS API (navigator.playHaptics), effect vocabulary (hint/edge/tick/align/none), and scroll-snap-haptic property. They diverge only in the declarative CSS surface and converge on the same long-term vision — each lists the other's constructs as a future extension.

Two Approaches

Pseudo-class primary (haptic-feedback property): Fires a haptic when an element transitions into a pseudo-class state (:hover, :active, :checked, :focus, etc.) due to direct user interaction. Two new CSS constructs. Core principle: "user changed state → fire haptic."

Transition primary (haptic-transition, @haptic/haptic-animation): Fires haptics at transition completion, and enables multi-step choreographed sequences synced to CSS animations. Three new CSS constructs. Core principle: "something moved visually → fire haptic."

Where They Overlap

For styled interactive elements — buttons with hover/active transitions, custom toggles with sliding thumbs, form validation with shake animations, scroll-snap carousels — either approach works declaratively. This is the majority of real-world haptics use cases.

Where They Diverge

Pseudo-class exclusively covers direct state-change interactions without requiring any authored motion: hover, active, focus, checked, and validation states on elements that have no CSS transition. This is broader than just unstyled native controls — many functional UIs have interactive elements without transitions.

Transition exclusively enables multi-step choreographed haptics synchronized to visual motion: a modal entrance ramping hint → tick → align, a progress bar with escalating pulses, page transitions with multi-beat feedback. Neither pseudo-class nor the imperative API can replicate this.

Key Tradeoffs

Dimension Pseudo-class first Transition first
No-JS coverage breadth Broader — covers every pseudo-class interaction regardless of whether the element has visual motion Narrower — requires an existing transition or animation
Unique new capability One-shot haptics on state entry (coverable by one-line JS) Multi-step choreography synced to motion (not replicable by other means)
CSS novelty / precedent risk Higher — introduces an unprecedented pattern: a CSS property that fires a discrete non-visual side-effect on state entry. Sets a precedent other proposals may follow, which the CSS WG may resist Lower — every construct maps to established patterns (transitionend, @keyframes, animation)
Spec/implementation surface Smaller (2 constructs, simpler semantics) Larger (3 constructs, timeline synchronization, at-rule)
Value of adding the other later Adding transition later provides genuinely new capability (choreography) Adding pseudo-class later provides syntactic convenience for interactions already coverable by imperative fallback
Developer mental model Simpler — "state change = haptic" Requires understanding transition lifecycle, sync-with() linking

Sequencing Arguments

Case for pseudo-class first: It covers a broader set of everyday interactions declaratively. A platform primitive should first handle the common cases authors encounter constantly. It ships a smaller, simpler v1 and leaves rich choreography as a strong follow-on that adds genuinely new expressive power — a clean "baseline then enrichment" progression.

Case for transition first: It front-loads the richer, harder-to-replicate capability on safer CSS ground. It avoids establishing an unprecedented CSS property precedent, giving the proposal a smoother standards path. If transition ships first and pseudo-class never materializes, the imperative API covers the gap adequately. If pseudo-class ships first and transition never materializes, the richest declarative use cases are lost — the downside risk is asymmetric.

Both are defensible. The choice depends on whether the team prioritizes broadest no-JS baseline coverage and smallest v1 (→ pseudo-class) or smoothest standards path and unique new capability (→ transition).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a PR for leaving comment on the alternate explainer, so I'll just leave my core comment here:

It looks like sync-with is still experimental, so I would advise not depending on it yet.

What about something closer to my above proposal of making this part of the animation, rather than something independent that syncs to it? It would certainly be a lot more restrictive, but would also mean a lot less edges to handle. (And if making that shift for animations, it would make sense to do the same for transitions as well).

Copy link
Member Author

@liminzhu liminzhu Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this (low-ish effort, I haven't gone through/edited the whole explainer yet) which I think is what you described?

I'm liking it more and more. We can start with that as the easier-to-standardize phase 1, and move the pseudoclass part as a future extension to allow for easier authoring/wider coverage. I will refactor the explainer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made a major update, would appreciate your thoughts


Future work may address transition-end haptics (`haptic-transition`), multi-step haptic sequences (`@haptic` at-rule).

## Alternatives Considered
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative for the imperative form - what about extending the existing navigator.vibrate API to also accept keywords instead of raw patterns? I don't know how common that is in the JavaScript land, but in CSS land this would be very normal (e.g. color can take explicit precise colors, or keywords supplied by the browser).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expanded the alt considered section to be clearer -

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants