Draggable objects: examples

 from Red Blob Games
1 Dec 2023

On the main page I show the event handler recipe I use for dragging objects around in my interactive diagrams. That’s only handles the input. It doesn’t draw or move anything. The  event handler  needs to be connected to a  state handler . The state handler handles the output.

diagram-state-and-event-handlers.svg

This structure gives me the flexibility I want for my projects. The state handler isn’t limited to dragging, or a fixed set of behaviors. This is unlike the design of libraries like jquery-ui-draggable or react-draggable. Those libraries give me a list of predefined behaviors like snapping to a grid or constrained movement or using a handle, but the behavior I want isn’t always in that list. In my projects I decouple the drag event handler from the state handler, so I can implement any of those, and more, such as scrubble numbers or a painting program. On this page I’ll give examples with code you can use.

 1  Vanilla JS examples#

These examples are starting points using global variables. You’ll have to adapt the recipe to fit into your own project.

Here's a basic state handler that works with the event handler. The position pos has a setter on it that also updates the circle’s position whenever the position variable is changed. The event handler sets the dragging CSS class, which changes the color of the circle while it’s being dragged.In this state handler the setter for pos clamps the x and y positions to stay within a rectangular area.In this state handler the setter for pos snaps the x,y position to the nearest gray dot. On the left is snap-to-grid as seen in other libraries but the right side shows the flexibility of being able to write my own state handler.Here's the classic dragging of a <div> by setting its transform CSS property. In this example eventToCoordinates doesn’t need to transform the coordinate system. The position is constrained to the container <div>. To switch to absolute positioning, the set pos() function would change, but the event handler code would not.There's a lot of flexibility in changing what's draggable separately from how the position is updated. Here's a recreation of one example from React-Draggable[1]. The event handlers connect to the red handle, but the state handler updates the position of the white containing box.The drag events don't have to involve moving anything. This example shows how they can be used for changing the number stored in an <input> element. I use this type of interaction on my Damage Rolls page. Since it’s a standard element I can change it with keyboard/mouse in addition to dragging. However, the standard input event has styling and text selection that may interfere with dragging, so some people use a <span> instead.Here's another example of how the drag events don't have to move anything. Here we don't want to keep track of the initial offset in the pointerdown event. Draw on the canvas:Here we're not dragging any DOM element. The canvas stays in place. But I'm moving a marker that's drawn on the canvas. I had to modify the event handler recipe to update the cursor whenever anything happened. This example also shows how the internal representation [x,y] can be different from the external type {x,y}.This example shows multi-state painting. It remembers the color you are painting with, and keeps using it until the pointer up event. The event handling in this example differs from the standard recipe enough that I don't know if it's really worth keeping the state separate from the events. Each square has an event handler, and we release the pointer capture so that the pointer move goes to other squares while painting. But that also means this doesn't behave properly when moving the mouse outside the painting area and then back in.State can be shared between draggables. Here, the x position is separate and the y position is shared.When nesting draggable elements, use stopPropagation() to prevent the inner draggable from sending events to the outer draggable handler.

 2  Vue component#

I use Vue in many of my projects. The event handlers can be put into a Vue component that wraps a draggable element. This example is an SVG <g> wrapper, but the code would be similar for a <div> wrapper.

To use this component, create position variables (data in Options API or ref in Composition API) of type {x, y}, and then draw the desired shape as the child slot of the draggable component. In this example, I have redCircle and blueSquare positions:

<svg viewBox="-100 -100 200 200" style="background: gray">
  <Draggable v-model="redCircle">
    <circle r="10" fill="red" />
  </Draggable>
  <Draggable v-model="blueSquare">
    <rect x="-10" y="-10" width="20" height="20" fill="blue" />
  </Draggable>
</svg>

Run it on the Vue Playground[2], with either the Options API or Composition API components. Or browse the component source code here:

<Draggable> component, Options API:
<template>
  <g
    v-bind:transform="`translate(${modelValue.x},${modelValue.y})`"
    v-bind:class="{dragging}"
    v-on:pointerdown.left="start"
    v-on:pointerup="end" v-on:pointercancel="end"
    v-on:pointermove="dragging ? move($event) : null"
    v-on:touchstart.prevent="" v-on:dragstart.prevent="">
    <slot />
  </g>
</template>

<script>
  import {eventToSvgCoordinates} from './svgcoords.js';

  export default {
    props: {modelValue: Object}, // should be {x, y}
    data() { return { dragging: false }; },
    methods: {
      start(event) {
        if (event.ctrlKey) return;
        let {x, y} = eventToSvgCoordinates(event);
        this.dragging = {dx: this.modelValue.x - x, dy: this.modelValue.y - y};
        event.currentTarget.setPointerCapture(event.pointerId);
      },
      end(_event) {
        this.dragging = null;
      },
      move(event) {
        let {x, y} = eventToSvgCoordinates(event);
        this.$emit('update:modelValue', {
          x: x + this.dragging.dx,
          y: y + this.dragging.dy,
        });
      },
    }
  }
</script>

<style scoped>
  g { cursor: grab; }
  g.dragging { user-select: none; cursor: grabbing; }
</style>
<Draggable> component, Composition API:
<template>
  <g
    v-bind:transform="`translate(${modelValue.x},${modelValue.y})`"
    v-bind:class="{dragging}"
    v-on:pointerdown.left="start"
    v-on:pointerup="end" v-on:pointercancel="end"
    v-on:pointermove="dragging ? move($event) : null"
    v-on:touchstart.prevent="" v-on:dragstart.prevent="">
    <slot />
  </g>
</template>

<script setup>
  import {ref} from 'vue';
  import {eventToSvgCoordinates} from './svgcoords.js';

  const props = defineProps({
    modelValue: Object // should be {x, y}
  });

  const emit = defineEmits(['update:modelValue'])

  const dragging = ref(false);

  function start(event) {
    if (event.ctrlKey) return;
    let {x, y} = eventToSvgCoordinates(event);
    dragging.value = {dx: props.modelValue.x - x, dy: props.modelValue.y - y};
    event.currentTarget.setPointerCapture(event.pointerId);
  }

  function end(event) {
    dragging.value = null;
  }

  function move(event) {
    let {x, y} = eventToSvgCoordinates(event);
    emit('update:modelValue', {
      x: x + dragging.value.dx,
      y: y + dragging.value.dy,
    });
  }
</script>

<style scoped>
  g { cursor: grab; }
  g.dragging { user-select: none; cursor: grabbing; }
</style>

If you’re using Vue v2 (whether Options API or Composition API), you’ll need to change the v-model prop from modelValue to value and the event from update:modelValue to input.

Add computed setters on the model value to apply constraints like bounds checking or grid snapping. In this Vue playground example[3] the blue square uses a computed setter to stay in orbit around the red circle, with the orbit distance snapping to one of three values.

In Vue, multiple elements can point to the same reactive data. In this Vue Playground example[4] there’s an <input> element with v-model bound to the same state to make the position editable by keyboard.

 3  More#

I feel like I should make the event handlers into a library but so far I’m treating it as a mere “recipe” that I copy and paste. This is in part because I have needed to modify it for the various examples on this page, and it seemed simpler to present something as something to copy and modify than to try to make one piece of code that handles all situations.

TODO: more examples I’m considering:

  1. invisible larger drag area for mobile (from my “little things” page)
  2. constrain position to a non-rectangular area, like my making-of curved-paths page
  3. .dragging class options (from “little things” page)
  4. maybe a React or Svelte example
  5. drop constraints, such as detecting which box something is dropped into, or dropped between two items

Although I think of the state handling and the event handling as separate concepts, I will sometimes “inline” the state handling into the event handling code.

Other libraries to consider

Email me , or tweet @redblobgames, or comment: