Craig Gidney Craig Gidney - 5 months ago 6x
Javascript Question

Supporting scrolling and drag actions on a canvas

I have a javascript circuit simulator where you edit the circuit by dragging gates around. Sometimes the circuit gets very large, so the canvas element it's being drawn on exceeds the viewport of the browser and scrolling becomes necessary.

When a user with a touchscreen slides their finger across the screen, I want a slide that starts on a circuit element to be interpreted as a drag. But a slide that starts on empty space (as determined by my custom logic determining whats being shown the canvas) should be interpreted as scrolling.

How do I make the dragging and scrolling behaviors live together in harmony? How do I do it without poorly simulating the browser's usual scrolling behavior? Users hate it when the scrolling is wonky w.r.t. the native behavior.

(Bonus: How do I do it without changing the usual scrolling behavior but while still preventing the scrolling from triggering navigation actions [e.g. on Android pulling down when already at the top of the page will reload it]?)

I've tried calling

only when I want the canvas to take control and show a drag. That stops working well as soon as scroll bars appear (the browser sends the touch, but then immediately cancels it). The CSS property
might also be useful, but I haven't experimented with it much.


The workaround I ended up using was blocker divs.

For every draggable element within the canvas, I added a div with touch-action: none, opacity: 0, and position: absolute over the location of that element. When the user touches down on one of the blocker divs, scrolling is blocked without interfering with the drag events.

setBlockers(blockers, overrideCursorStyle) {
    // ensure enough blocker divs exist
    while (this._blockerDivs.length < blockers.length) {
        let blockerDiv = document.createElement('div'); = 'none'; = 'absolute'; = 0;

    // reposition blocker divs
    for (let i = 0; i < blockers.length; i++) {
        let s = this._blockerDivs[i].style;
        let {x, y, w, h, cursor} = blockers[i];
        [s.left,, s.width, s.height] = [x, y, w, h].map(e => e + "px");
        s.cursor = overrideCursorStyle || cursor || 'auto';
        s.display = 'inline';

    // hide unused blocker divs
    for (let i = blockers.length; i < this._blockerDivs.length; i++) {
        this._blockerDivs[i].style.display = 'none';

I've only tested this in Chrome on Android.