<h1>Draggable autoScroll</h1>
<p>As of version 0.12.0, Draggable has auto-scrolling capabilities! Add <code>autoScroll:1</code> to the config object for normal-speed scrolling, or <code>autoScroll:2</code> would be twice as fast, etc. When your mouse moves within 40px of the edge of a scrollable container, it will start scrolling. The closer you get to the edge, the faster it will scroll. Try it out below...</p>
<div class="wrapper">
  <div id="container">
    <div class="box green" id="box">Drag me</div>
  </div>
</div>
<div class="controls">
  <ul>
    <li class="controlsTitle">Options</li>
    <li>
      <label><input type="checkbox" name="snap" id="snap" value="1" /> Snap end position to grid</label>
    </li>
    <li>
      <label><input type="checkbox" name="liveSnap" id="liveSnap" value="1" /> Live snap</label>
    </li>
  </ul>
</div>
body {
  padding: 1rem;
}

body {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  flex-direction: column;
}

.wrapper {
  width: 60vw;
  height: 1020px;
  overflow: scroll;
}

#container {
  height: 801px;
  overflow: visible;
  padding: 0;
  position: relative;
}

.box {
  text-align: center;
  font-size: 18px;
  width: 196px;
  line-height: 100px;
  color: black;
  position: absolute;
  top: 0;
  -webkit-border-radius: 10px;
  -moz-border-radius: 10px;
  border-radius: 10px;
}

p {
  max-width: 70ch;
}

.controls {
  background-color: #222;
  border: 1px solid #555;
  color: #bbb;
  font-size: 18px;
  margin: 20px 0;
}
.controls ul {
  list-style: none;
  padding: 0;
  margin: 0;
}
.controls li {
  display: inline-block;
  padding: 8px 0 8px 10px;
  margin: 0;
}
.controls input {
  vertical-align: middle;
  cursor: pointer;
}
.controls .controlsTitle {
  border-right: 1px solid #555;
  border-bottom: none;
  padding-right: 10px;
}
// See https://greensock.com/gsap-1-16 for details.

const snapCheckbox = document.getElementById("snap");
const liveSnapCheckbox = document.getElementById("liveSnap");
const container = document.getElementById("container");

const gridWidth = 74;
const gridHeight = 50;
const gridRows = 28;
const gridColumns = 40;

// Create the grid cells
for (let i = 0; i < gridRows * gridColumns; i++) {
  const y = Math.floor(i / gridColumns) * gridHeight;
  const x = (i * gridWidth) % (gridColumns * gridWidth);

  const cell = document.createElement("div");
  cell.style.position = "absolute";
  cell.style.border = "1px solid #454545";
  cell.style.width = `${gridWidth - 1}px`;
  cell.style.height = `${gridHeight - 1}px`;
  cell.style.top = `${y}px`;
  cell.style.left = `${x}px`;

  container.prepend(cell);
}

// Set container and box sizes
gsap.set(container, {
  height: gridRows * gridHeight + 1,
  width: gridColumns * gridWidth + 1
});
gsap.set("#box", { width: 150, height: 100, lineHeight: "100px" });

function update() {
  const snap = snapCheckbox.checked;
  const liveSnap = liveSnapCheckbox.checked;

  Draggable.create("#box", {
    bounds: container,
    autoScroll: 1,
    edgeResistance: 0.65,
    type: "x,y",
    throwProps: true,
    liveSnap: liveSnap,

    onRelease: function () {
      this.tween.progress(1);
      const wrapper = document.querySelector(".wrapper");
      const tBounds = this.target.getBoundingClientRect();
      const wBounds = wrapper.getBoundingClientRect();
      let wCenter = wBounds.left + wBounds.width / 2;
      let tCenter = tBounds.left + tBounds.width / 2;
      const scroll = {};

      if (tBounds.right > wBounds.right || tBounds.left < wBounds.left) {
        scroll.x = wrapper.scrollLeft + (tCenter - wCenter);
      }

      if (tBounds.bottom > wBounds.bottom || tBounds.top < wBounds.top) {
        wCenter = wBounds.top + wBounds.height / 2;
        tCenter = tBounds.top + tBounds.height / 2;
        scroll.y = wrapper.scrollTop + (tCenter - wCenter);
      }

      gsap.to(wrapper, this.tween.duration(), { scrollTo: scroll });
      this.tween.progress(0);
    },

    snap: {
      x: function (endValue) {
        return snap || liveSnap
          ? Math.round(endValue / gridWidth) * gridWidth
          : endValue;
      },
      y: function (endValue) {
        return snap || liveSnap
          ? Math.round(endValue / gridHeight) * gridHeight
          : endValue;
      }
    }
  });
}

function applySnap() {
  if (snapCheckbox.checked || liveSnapCheckbox.checked) {
    document.querySelectorAll(".box").forEach((element) => {
      const transform =
        element._gsTransform || gsap.getProperty(element, "x,y");
      gsap.to(element, 0.5, {
        x: Math.round(transform.x / gridWidth) * gridWidth,
        y: Math.round(transform.y / gridHeight) * gridHeight,
        delay: 0.1,
        ease: Power2.easeInOut
      });
    });
  }
  update();
}

// Hook up event listeners
snapCheckbox.addEventListener("change", applySnap);
liveSnapCheckbox.addEventListener("change", applySnap);

// Initial setup
update();
Run Pen

External CSS

  1. https://codepen.io/GreenSock/pen/wvZQKQP.css

External JavaScript

  1. https://unpkg.com/gsap@3/dist/gsap.min.js
  2. https://unpkg.com/gsap@3/dist/Draggable.min.js
  3. https://assets.codepen.io/16327/InertiaPlugin.min.js