Skip to content

Drag any cell selection or fill handle outside the visible viewport to scroll the grid automatically while extending the selection.

Overview

The DragToScroll plugin watches for mouse drags that move outside the grid’s visible area. When the cursor crosses a viewport edge, the plugin scrolls the grid in that direction and extends the active selection to follow — exactly as spreadsheet applications behave.

The plugin is enabled by default. It activates during two types of drags:

  • Cell selection drag — click a cell, hold, and drag beyond the edge to extend the selection.
  • Fill handle drag — drag the fill handle (the small square in the bottom-right corner of a selection) beyond the edge to autofill into additional rows or columns.

Scroll speed follows a logarithmic curve: it starts slow when the cursor is just past the edge, then accelerates as the cursor moves farther away. This gives you precise control for small selections and fast navigation for large ones.

Enable drag to scroll

The plugin is enabled by default. To enable it explicitly, set dragToScroll to true:

dragToScroll: true,

To see drag-to-scroll in action, click any cell in the grid below, hold the mouse button, and drag past any edge of the viewport.

JavaScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
registerAllModules();
const container = document.querySelector('#example1');
// Build column headers: 'Cost Center' + 49 monthly labels (Jan 2021 … Jan 2025)
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const colHeaders = ['Cost Center'];
let year = 2021;
let monthIndex = 0;
while (colHeaders.length < 50) {
colHeaders.push(`${months[monthIndex]} ${year}`);
monthIndex += 1;
if (monthIndex >= months.length) {
monthIndex = 0;
year += 1;
}
}
// Build 50 rows of budget data
const data = [];
for (let row = 0; row < 50; row++) {
const rowData = [`CC-${1000 + row}`];
for (let col = 0; col < 49; col++) {
rowData.push(2000 + row * 100 + col * 50);
}
data.push(rowData);
}
new Handsontable(container, {
data,
colHeaders,
width: 500,
height: 220,
rowHeaders: true,
dragToScroll: true,
licenseKey: 'non-commercial-and-evaluation',
});
TypeScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
registerAllModules();
const container = document.querySelector('#example1') as HTMLElement;
// Build column headers: 'Cost Center' + 49 monthly labels (Jan 2021 … Jan 2025)
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const colHeaders: string[] = ['Cost Center'];
let year = 2021;
let monthIndex = 0;
while (colHeaders.length < 50) {
colHeaders.push(`${months[monthIndex]} ${year}`);
monthIndex += 1;
if (monthIndex >= months.length) {
monthIndex = 0;
year += 1;
}
}
// Build 50 rows of budget data
const data: (string | number)[][] = [];
for (let row = 0; row < 50; row++) {
const rowData: (string | number)[] = [`CC-${1000 + row}`];
for (let col = 0; col < 49; col++) {
rowData.push(2000 + row * 100 + col * 50);
}
data.push(rowData);
}
new Handsontable(container, {
data,
colHeaders,
width: 500,
height: 220,
rowHeaders: true,
dragToScroll: true,
licenseKey: 'non-commercial-and-evaluation',
});

Configure scroll speed

Pass an object to dragToScroll to control how quickly the viewport scrolls:

dragToScroll: {
interval: {
min: 20,
max: 500,
},
rampDistance: 120,
},

The three parameters work together to shape the acceleration curve:

ParameterTypeDefaultDescription
interval.minnumber20Minimum scroll interval in milliseconds. This is the fastest the scroll gets — reached when the cursor is at rampDistance pixels outside the edge. A lower value produces faster peak scrolling.
interval.maxnumber500Maximum scroll interval in milliseconds. This is the slowest the scroll starts — applied when the cursor first crosses the viewport edge. A higher value produces a more gradual start.
rampDistancenumber120Distance in pixels from the viewport edge over which the interval decreases from max to min. A shorter distance causes the scroll to reach peak speed more quickly.

The interval decreases on a logarithmic scale as the cursor moves away from the viewport edge. This means the biggest speed increase happens close to the edge, and the rate of acceleration gradually falls off farther out.

Chart showing scroll interval decreasing logarithmically from interval.max at the viewport edge to interval.min at rampDistance pixels outside

Use the sliders below to adjust all three parameters. The grid reloads with the new settings so you can drag a selection outside the viewport to feel the difference.

JavaScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
registerAllModules();
// Build column headers: 'Cost Center' + 49 monthly labels (Jan 2021 … Jan 2025)
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const colHeaders = ['Cost Center'];
let year = 2021;
let monthIndex = 0;
while (colHeaders.length < 50) {
colHeaders.push(`${months[monthIndex]} ${year}`);
monthIndex += 1;
if (monthIndex >= months.length) {
monthIndex = 0;
year += 1;
}
}
// Build 50 rows of budget data
const data = [];
for (let row = 0; row < 50; row++) {
const rowData = [`CC-${1000 + row}`];
for (let col = 0; col < 49; col++) {
rowData.push(2000 + row * 100 + col * 50);
}
data.push(rowData);
}
// Root layout
const root = document.querySelector('#example2');
root.innerHTML = `
<div id="dts-sliders" style="display:flex;gap:28px;flex-wrap:wrap;margin-bottom:16px;
font:13px/1.4 sans-serif;color:#334155;"></div>
<div id="dts-hot"></div>
`;
function addSlider(label, unit, min, max, step, initialValue, onChange) {
const wrapper = document.createElement('label');
wrapper.style.cssText = 'display:flex;flex-direction:column;gap:4px;';
const nameLabel = document.createElement('b');
nameLabel.style.fontFamily = 'monospace';
nameLabel.textContent = `${label}: ${initialValue} ${unit}`;
const input = document.createElement('input');
input.type = 'range';
input.min = String(min);
input.max = String(max);
input.step = String(step);
input.value = String(initialValue);
input.style.cssText = 'width:200px;cursor:pointer;';
input.addEventListener('input', () => {
nameLabel.textContent = `${label}: ${input.value} ${unit}`;
onChange(Number(input.value));
});
wrapper.append(nameLabel, input);
document.getElementById('dts-sliders').appendChild(wrapper);
}
let intervalMin = 20;
let intervalMax = 500;
let rampDistance = 120;
const hot = new Handsontable(document.getElementById('dts-hot'), {
data,
colHeaders,
width: 500,
height: 220,
rowHeaders: true,
dragToScroll: {
interval: { min: intervalMin, max: intervalMax },
rampDistance,
},
licenseKey: 'non-commercial-and-evaluation',
});
function sync() {
hot.updateSettings({
dragToScroll: {
interval: { min: intervalMin, max: intervalMax },
rampDistance,
},
});
}
addSlider('interval.min', 'ms', 10, 200, 10, intervalMin, (value) => {
intervalMin = value;
sync();
});
addSlider('interval.max', 'ms', 100, 1000, 50, intervalMax, (value) => {
intervalMax = value;
sync();
});
addSlider('rampDistance', 'px', 20, 300, 10, rampDistance, (value) => {
rampDistance = value;
sync();
});
TypeScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
registerAllModules();
// Build column headers: 'Cost Center' + 49 monthly labels (Jan 2021 … Jan 2025)
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const colHeaders: string[] = ['Cost Center'];
let year = 2021;
let monthIndex = 0;
while (colHeaders.length < 50) {
colHeaders.push(`${months[monthIndex]} ${year}`);
monthIndex += 1;
if (monthIndex >= months.length) {
monthIndex = 0;
year += 1;
}
}
// Build 50 rows of budget data
const data: (string | number)[][] = [];
for (let row = 0; row < 50; row++) {
const rowData: (string | number)[] = [`CC-${1000 + row}`];
for (let col = 0; col < 49; col++) {
rowData.push(2000 + row * 100 + col * 50);
}
data.push(rowData);
}
// Root layout
const root = document.querySelector('#example2') as HTMLElement;
root.innerHTML = `
<div id="dts-sliders" style="display:flex;gap:28px;flex-wrap:wrap;margin-bottom:16px;
font:13px/1.4 sans-serif;color:#334155;"></div>
<div id="dts-hot"></div>
`;
function addSlider(
label: string,
unit: string,
min: number,
max: number,
step: number,
initialValue: number,
onChange: (value: number) => void
): void {
const wrapper = document.createElement('label');
wrapper.style.cssText = 'display:flex;flex-direction:column;gap:4px;';
const nameLabel = document.createElement('b');
nameLabel.style.fontFamily = 'monospace';
nameLabel.textContent = `${label}: ${initialValue} ${unit}`;
const input = document.createElement('input');
input.type = 'range';
input.min = String(min);
input.max = String(max);
input.step = String(step);
input.value = String(initialValue);
input.style.cssText = 'width:200px;cursor:pointer;';
input.addEventListener('input', () => {
nameLabel.textContent = `${label}: ${input.value} ${unit}`;
onChange(Number(input.value));
});
wrapper.append(nameLabel, input);
document.getElementById('dts-sliders')!.appendChild(wrapper);
}
let intervalMin = 20;
let intervalMax = 500;
let rampDistance = 120;
const hot = new Handsontable(document.getElementById('dts-hot')!, {
data,
colHeaders,
width: 500,
height: 220,
rowHeaders: true,
dragToScroll: {
interval: { min: intervalMin, max: intervalMax },
rampDistance,
},
licenseKey: 'non-commercial-and-evaluation',
});
function sync(): void {
hot.updateSettings({
dragToScroll: {
interval: { min: intervalMin, max: intervalMax },
rampDistance,
},
});
}
addSlider('interval.min', 'ms', 10, 200, 10, intervalMin, (value) => {
intervalMin = value;
sync();
});
addSlider('interval.max', 'ms', 100, 1000, 50, intervalMax, (value) => {
intervalMax = value;
sync();
});
addSlider('rampDistance', 'px', 20, 300, 10, rampDistance, (value) => {
rampDistance = value;
sync();
});

Disable drag to scroll

Set dragToScroll to false to turn off the plugin entirely. The viewport will not scroll when the cursor leaves it during a drag.

dragToScroll: false,

Configuration options

Plugins