Documents v0.9.0 · previews run the real runtime · experimental features are marked exp

:on(drag)

rulesince 0.9.0experimentalrequires runtime

Pointer drag gestures as event rules: :on(drag) runs on every move, drag(start), drag(end) and drag(cancel) at the edges. event(x) and event(y) give positions that continue across drags.

On this page · 10 sections+

Syntax

<selector>:on(drag | drag(start | move | end | cancel) [, prevent()]) {  --<state>: set(event(x));}
:on(drag)Rule · experimental · hover any red tokenPointer drag gestures as event rules: :on(drag) runs on every move, drag(start), drag(end) and drag(cancel) at the edges. event(x) and event(y) give positions that continue across drags.Reference →

registry syntax: :on(drag) | :on(drag(start | move | end | cancel))

Parameters

phasestart, move, end or canceloptional
:on(drag) is :on(drag(move)).
prevent()modifieroptional
Cancels the default of the pointerdown that starts the gesture. stop() is a compile error with drag.

Effect

No value. Each phase runs the rule’s mutations, as any :on() rule does.

Example

.card {  --x: state(0px);  --y: state(0px);  --dragging: state(false);  translate: var(--x) var(--y);  touch-action: none;}.card:on(drag(start)) {  --dragging: set(true);}/* event(x): the state at drag start plus the distance moved. */.card:on(drag) {  --x: set(clamp(-110px, event(x), 110px));  --y: set(clamp(-55px, event(y), 55px));}.card:on(drag(end)) {  --dragging: set(false);}.card:when(--dragging) {  cursor: grabbing;  scale: 1.05;}/* The same state from the keyboard. */.card:on(keydown(ArrowRight), prevent()) { --x: set(min(var(--x) + 10px, 110px)); }.card:on(keydown(ArrowLeft), prevent()) { --x: set(max(var(--x) - 10px, -110px)); }.card:on(keydown(ArrowDown), prevent()) { --y: set(min(var(--y) + 10px, 55px)); }.card:on(keydown(ArrowUp), prevent()) { --y: set(max(var(--y) - 10px, -55px)); }
state()Function · hover any red tokenMakes each matching element the owner of an independent state with an initial value.Reference →
Resultfocusable card, arrow keys
waiting for interaction

Correct and incorrect usage

correctA slider thumb clamped to its track

slider.cssreactive
.slider { --v: state(0px); }.slider .thumb {  translate: var(--v) 0;  touch-action: none;}.slider .thumb:on(drag) { --v: set(clamp(0px, event(x), 200px)); }.slider .thumb:on(keydown(ArrowRight), prevent()) { --v: set(min(var(--v) + 10px, 200px)); }.slider .thumb:on(keydown(ArrowLeft), prevent()) { --v: set(max(var(--v) - 10px, 0px)); }

The state lives on the track. event(x) starts from the current --v at each drag, and the arrow keys change the same state. The thumb needs tabindex="0" in the HTML to receive keys.

incorrectstop() in a drag rule

card.cssreactive
.card { --x: state(0px); }.card:on(drag, stop()) { --x: set(event(x)); }

A compile error: a drag is not a bubbling DOM event, so there is nothing to stop.

Behavior

  • Experimental: no real touch or pen hardware has been tested yet.
  • Arming. A press with the primary button arms a gesture on the innermost element on the path that a drag rule matches, unless it is :disabled, inside [inert] or already being dragged. The gesture starts after 3 CSS px of movement. Below that, the press and the click stay native and no phase runs.
  • Phases. start runs once, then move (:on(drag)) on each pointer move. Moves run synchronously; state writes are coalesced into one style write per frame, and the last position is never dropped. pointerup runs end. pointercancel or a lost pointer capture runs cancel, then end, so --dragging: set(false) in drag(end) always runs.
  • Positions continue. event(x) is the value of the mutated state when the drag started, plus the pointer distance on that axis. .card:on(drag) { --x: set(event(x)); } therefore moves the card on from where it was, over any number of drags, and never jumps back to the pointer. For raw displacement use event(delta-x); event(movement-x) is the distance since the previous move and event(start-x) the press position in viewport coordinates. See event().
  • Pointer capture. The element captures the pointer, so moves keep arriving when the pointer leaves it. Each gesture follows its own pointer: concurrent pointers drag different elements independently, and a second pointer on an element that is already being dragged is ignored.
  • Fields inside. A press on an input, textarea, select or editable element inside the draggable never starts a drag, so selecting text and moving sliders keep working. Buttons and links inside can start one.
  • The click after a drag does not run :on(click) rules on that element or its ancestors. The native click is not changed.
  • DOM changes. Moving the element to another parent during a drag makes the browser release the capture, so the gesture is cancelled: cancel, then end. Removing the element, or a class change that makes it stop matching, ends the gesture without running any phase; a state set in drag(start) keeps its value.
  • Cleanup. One capturing pointerdown listener exists while drag rules exist. Move listeners exist only during a gesture, and the capture is released at its end. ReactiveCSS.stop() ends every gesture.

Supported contexts

  • Give the draggable element touch-action: none, or touch scrolling takes over the gesture.
  • The same placement rules as :on(). A custom bundle needs the gestures feature.

Limitations

  • Accessibility. A drag has no keyboard equivalent by itself. Change the same state from the keyboard as well, as the example does with :on(keydown(ArrowRight), prevent()) rules, or offer a native range input.
  • Not HTML drag and drop: no dragstart or drop, no sortable lists and no drop targets.
  • There is no drag(x) value function; event(x) covers it.
  • Touch and pen are covered by synthetic events only. Real devices are on the manual test list.

Examples

Experimental · syntax may change between minor versions