Skip to content

feat: x-on.passive.false modifier#4610

Merged
calebporzio merged 6 commits intoalpinejs:mainfrom
hirasso:feat/x-on-passive-false
Feb 9, 2026
Merged

feat: x-on.passive.false modifier#4610
calebporzio merged 6 commits intoalpinejs:mainfrom
hirasso:feat/x-on-passive-false

Conversation

@hirasso
Copy link
Contributor

@hirasso hirasso commented Apr 25, 2025

Replaces #4404

Make events that are passive by default cancelable:

<div x-on.touchmove.passive.false="console.log($event.cancelable)"></div>

Use Case

Detect the axis of a touchmove gesture and call preventDefault if it's the x-axis (useful for preventing document scolling if interacting with a slider).

@hirasso hirasso mentioned this pull request Apr 25, 2025
2 tasks
hirasso and others added 3 commits April 25, 2025 17:36
- Fix doc text to say "touch and wheel" instead of listing specific events
- Change example from touchstart to touchmove to match the use case described
- Use more practical example ($event.preventDefault() vs console.log)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@calebporzio
Copy link
Collaborator

PR Review: #4610 — feat: x-on.passive.false modifier

Type: Feature
Verdict: Merge

What's happening (plain English)

  1. Modern browsers made certain touch/wheel events (touchstart, touchmove, wheel) passive by default for scrolling performance. This means preventDefault() silently does nothing on these events.
  2. Alpine already has a .passive modifier that explicitly sets { passive: true } on event listeners. But there's no way to go the other direction — explicitly setting { passive: false } so you can call preventDefault().
  3. This PR adds .passive.false which sets { passive: false } on the listener, making the event cancelable.
  4. Real use case: building a horizontal slider where you want to detect x-axis touch movement and prevent vertical scrolling. Without passive: false, you can't call preventDefault() on touchmove.

Other approaches considered

  1. A separate .nonpassive or .cancelable modifier — Would work but adds new vocabulary. .passive.false reads naturally as "passive: false" and follows the existing pattern of modifiers consuming the next modifier as an argument (like .debounce.250ms).
  2. Doing nothing — tell users to use raw addEventListener — Defeats the purpose of Alpine's declarative event handling. This is a legitimate gap in the API.
  3. Only .passive.false (no standalone .passive) — Not applicable since .passive already exists and works. This PR just extends it.

The contributor's approach is the right one. It follows Alpine's established "peek at the next modifier" pattern used by debounce and throttle.

Changes Made

  • Fixed docs: changed text from listing specific events (wheel and touchmove) to the more general "touch and wheel event listeners" since touchstart is also passive by default
  • Fixed docs example: changed from @touchstart.passive.false="console.log($event.cancelable)" to @touchmove.passive.false="$event.preventDefault()" to match the described use case with a more practical example

Test Results

  • All 42 x-on spec tests pass with the fix ✓
  • Verified the new test fails without the fix (41 pass, 1 fail) ✓
  • CI checks: build passing ✓

Code Review

packages/alpinejs/src/utils/on.js:21-23 — The implementation is clean and follows the exact same pattern as debounce/throttle modifier argument consumption. One minor note: the false string will remain in the modifiers array and could theoretically leak into isListeningForASpecificKeyThatHasntBeenPressed if someone wrote @keydown.passive.false.enter, but this is a nonsensical combination (keyboard events aren't passive by default) and not worth guarding against.

tests/cypress/integration/directives/x-on.spec.js:82-97 — Good test. Fires a touchmove event (natively passive in browsers), verifies preventDefault() works and defaultPrevented is true. Correctly mirrors the existing .passive test above it.

Style: No semicolons, uses let conventions. Clean. The diff includes one unrelated trailing whitespace removal (line 76) — harmless.

Security

No security concerns identified. This only affects event listener options — it doesn't touch expression evaluation, DOM mutation, or user-provided content.

Verdict

Merge. This fills a genuine gap in Alpine's event API. The implementation is minimal (3 lines of logic), follows established patterns, includes a proper test that verifies regression, and has docs. The use case is real and common in touch-interactive UIs. Zero reactions on the PR, but this replaces #4404 which indicates prior interest. No risk of breaking existing behavior — .passive alone still sets passive: true as before.


Reviewed by Claude

@calebporzio calebporzio merged commit 309b74e into alpinejs:main Feb 9, 2026
1 check passed
@hirasso
Copy link
Contributor Author

hirasso commented Feb 9, 2026

Oh wow – completely forgot about this one. Thanks for taking care of it 💖

@hirasso hirasso deleted the feat/x-on-passive-false branch February 9, 2026 11:03
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.

2 participants