Handling the dragstart Event

1

Handling the dragstart Event

▶

When a user begins dragging an element on a web page, the browser fires a sequence of drag-related events. The very first event in that sequence is dragstart, and understanding it deeply is the foundation of building any drag-and-drop interaction in the browser. Everything that makes a drag operation meaningful — what data is being moved, what visual feedback the user sees, and what operations are permitted — is configured right here, in the dragstart handler. Getting this event right means the rest of the drag-and-drop pipeline flows naturally.

The dragstart event fires on the source element — that is, the element the user actually grabbed and started dragging. This is an important distinction: it does not fire on the drop target where the element will eventually land, nor does it fire on any intermediate element the cursor passes over. It fires exactly once, at the very beginning of the drag gesture, before any dragover events start streaming in and certainly before any drop event occurs. Think of it as a single initialization moment: the browser is essentially asking your code, "A drag is starting — what do you want to communicate about it?"

For dragstart to fire on an element at all, that element must opt in to being draggable. You do this by setting the draggable="true" HTML attribute on it. Without this attribute, most elements are not draggable (with the notable exception of anchor tags with an href and images, which are draggable by default). Setting draggable="true" is what signals to the browser that user interaction with that element should initiate a drag gesture.

<!-- This paragraph will not fire dragstart without draggable="true" -->
<p>I cannot be dragged.</p>

<!-- This one is draggable and will fire dragstart when the user drags it -->
<p draggable="true" id="card-1">Drag me!</p>

Once an element has draggable="true", the browser will fire dragstart as soon as the user presses the mouse button and moves the pointer a small amount (the "drag threshold"). That threshold prevents accidental drags from simple clicks.

Attaching a dragstart Event Listener

The standard and recommended way to listen for dragstart is with addEventListener('dragstart', handler). Using addEventListener rather than the older ondragstart property assignment is best practice because it allows multiple independent listeners to coexist on the same element without overwriting each other. This matters in larger applications where different modules may each want to react to the same drag event.

const draggableCard = document.getElementById('card-1');

draggableCard.addEventListener('dragstart', function(event) {
  console.log('Drag started on:', event.target.id);
});

The event object passed into the handler is a DragEvent, which extends the standard MouseEvent. Its most important property for dragstart handling is event.dataTransfer, which gives you access to the DataTransfer object — the mechanism for storing and communicating drag data. We will cover that in detail shortly.

In real applications you rarely have just one draggable element. More often you have a list of cards, rows in a table, or tiles in a grid — all of which need to be draggable. Rather than attaching a listener to each element individually (which is inefficient), you can iterate over a NodeList and attach the same handler to every element at once:

// HTML:
// <ul id="task-list">
//   <li draggable="true" data-id="task-1">Write report</li>
//   <li draggable="true" data-id="task-2">Review PR</li>
//   <li draggable="true" data-id="task-3">Update docs</li>
// </ul>

const draggableItems = document.querySelectorAll('[draggable="true"]');

draggableItems.forEach(function(item) {
  item.addEventListener('dragstart', handleDragStart);
});

function handleDragStart(event) {
  console.log('Dragging item with id:', event.currentTarget.dataset.id);
}

Alternatively, for very large or dynamically generated lists, you can use event delegation — attach a single dragstart listener to a common ancestor and check event.target inside the handler to determine which child element initiated the drag. This is particularly efficient when items are added and removed from the list at runtime.

const taskList = document.getElementById('task-list');

taskList.addEventListener('dragstart', function(event) {
  if (event.target.matches('[draggable="true"]')) {
    console.log('Delegated drag start:', event.target.dataset.id);
  }
});

Introduction to the DataTransfer Object

The DataTransfer object is the communication channel of the drag-and-drop system. It travels with the drag operation and serves two key purposes: it holds data that the drag source wants to share with the drop target, and it carries configuration about what kinds of operations are permitted. You access it through event.dataTransfer inside any drag event handler, but it is in the dragstart handler where you write to it. By the time the drop event fires, you can only read from it.

Internally, the DataTransfer object stores data as a collection of key-value pairs where each key is a MIME type string (such as 'text/plain', 'text/html', or 'application/json') and the value is a string payload. Multiple formats can be registered for the same drag operation, and the drop target can choose whichever format it understands. This mirrors how the clipboard works in operating systems, where a copy operation might store both plain text and rich text simultaneously so that different paste destinations can pick their preferred format.

An important subtlety: for security reasons, the values stored on the DataTransfer object are only accessible in the drop handler (and during dragstart` itself). During dragover, only the types (keys) are readable, not the values. This lets a drop target decide whether to accept a drop based on format without exposing the full payload prematurely.

Storing Drag Data with setData()

The primary method for writing to the DataTransfer object is setData(format, data). It takes exactly two string arguments: the MIME type (the format key) and the data value to store. It must be called during the dragstart event; calling it at any other point in the drag lifecycle has no effect.

function handleDragStart(event) {
  // Store the element's ID as plain text
  event.dataTransfer.setData('text/plain', event.target.id);
}

You can call setData() multiple times with different format strings to make the same payload available in multiple representations. The browser will store all of them, and a drop target can request whichever format suits it best using getData(format):

function handleDragStart(event) {
  const elementId = event.target.id;
  const elementText = event.target.innerText;

  // Plain text version — compatible with any drop target, including text inputs
  event.dataTransfer.setData('text/plain', elementText);

  // HTML version — preserves markup if dropped into a rich-text editor
  event.dataTransfer.setData('text/html', event.target.outerHTML);

  // Custom application format — carries the element's ID for DOM manipulation
  event.dataTransfer.setData('text/x-card-id', elementId);
}

The most widely used pattern in single-page application drag-and-drop is storing the element's ID (or some other unique identifier). When the drop event fires, the handler retrieves this ID and uses it to look up the actual DOM element, then performs the necessary DOM manipulation — moving the node, updating application state, making an API call, and so on. This keeps the DataTransfer payload lightweight (just an ID string) while giving the drop handler everything it needs to act.

// dragstart handler on a kanban card
function handleDragStart(event) {
  event.dataTransfer.setData('text/plain', event.currentTarget.dataset.taskId);
}

// drop handler on a kanban column
function handleDrop(event) {
  event.preventDefault();
  const taskId = event.dataTransfer.getData('text/plain'); // "task-42"
  const draggedCard = document.querySelector(`[data-task-id="${taskId}"]`);
  event.currentTarget.appendChild(draggedCard); // move the card into this column
}

A common mistake is trying to share JavaScript objects directly through setData(). Since the value must be a string, you need to serialize any complex data first:

function handleDragStart(event) {
  const payload = {
    taskId: event.target.dataset.taskId,
    columnId: event.target.closest('.column').dataset.columnId,
    priority: 'high'
  };
  // Serialize the object to a JSON string
  event.dataTransfer.setData('application/json', JSON.stringify(payload));
}

function handleDrop(event) {
  event.preventDefault();
  const raw = event.dataTransfer.getData('application/json');
  const payload = JSON.parse(raw);
  console.log(payload.taskId, payload.priority);
}

Setting the Drag Effect

Beyond the data payload, the DataTransfer object carries configuration about what kind of drag operation is taking place. The effectAllowed property, set on the drag source during dragstart, declares which operations are valid for this particular drag. The dropEffect property, set on the drop target during dragover, specifies which of those allowed operations will actually be performed when the user releases the mouse.

Together, these two properties control the cursor appearance and communicate intent. The table below shows all valid values:

Property Value Meaning
effectAllowed 'none' No drag operation is permitted.
effectAllowed 'copy' The data may be copied to the drop target.
effectAllowed 'move' The data may be moved to the drop target (source should be removed).
effectAllowed 'link' A link to the source data may be created at the drop target.
effectAllowed 'copyMove' Either copy or move is permitted.
effectAllowed 'copyLink' Either copy or link is permitted.
effectAllowed 'linkMove' Either link or move is permitted.
effectAllowed 'all' Any operation is permitted (the default).
dropEffect 'copy' A copy of the source element is placed at the drop target.
dropEffect 'move' The element is moved from source to target.
dropEffect 'link' A link from the drop target back to the source is created.
dropEffect 'none' The item cannot be dropped here.

Setting effectAllowed during dragstart is how a drag source constrains what drop targets are allowed to do. For example, a file attachment widget might allow only 'copy' (the file stays in its source location and a copy appears in the drop zone), whereas a kanban card should use 'move' (the card leaves its original column). If you do not set effectAllowed, it defaults to 'all', giving drop targets complete freedom to choose any operation.

function handleDragStart(event) {
  event.dataTransfer.setData('text/plain', event.target.dataset.taskId);

  // Only a move is meaningful here — the card must leave its current column
  event.dataTransfer.effectAllowed = 'move';
}

On the drop target side, setting dropEffect = 'move' inside a dragover handler both updates the cursor icon (to a move cursor, typically an arrow with a small box) and communicates intent. After the drop completes, it is your JavaScript code's responsibility to actually remove the source element if a move was intended — the browser will not do that automatically.

Customizing the Drag Image

When a drag begins, the browser automatically creates a ghost image — a semi-transparent snapshot of the dragged element — that follows the cursor during the drag. In many cases this default ghost is perfectly adequate. But sometimes you want something more polished: a smaller thumbnail, a styled badge showing a count ("Moving 3 items"), a completely custom graphic, or an off-screen element that the user never sees in the normal document flow.

You can replace the default ghost image by calling event.dataTransfer.setDragImage(element, xOffset, yOffset) inside the dragstart handler. This must be called before the dragstart handler returns.

The three arguments are:

  • element — Any visible HTML element or an Image object. The browser renders this element as the ghost. The element does not have to be in the viewport, but it does need to be part of the document (or at least rendered) for the browser to generate its visual. A common trick is to append it to the body with absolute positioning far off-screen, use it as the drag image, then remove it after a brief delay.
  • xOffset — The horizontal offset in pixels from the left edge of the image to the cursor's hotspot. An offset of 0 puts the cursor at the far left of the image; half the image width centers the cursor horizontally.
  • yOffset — The vertical offset in pixels from the top edge of the image to the cursor's hotspot.
function handleDragStart(event) {
  event.dataTransfer.setData('text/plain', event.target.id);
  event.dataTransfer.effectAllowed = 'move';

  // Create a custom drag image
  const ghost = document.createElement('div');
  ghost.textContent = 'Moving task...';
  ghost.style.cssText = `
    position: absolute;
    top: -9999px;
    left: -9999px;
    background: #4f46e5;
    color: white;
    padding: 8px 16px;
    border-radius: 8px;
    font-size: 14px;
    font-family: sans-serif;
    pointer-events: none;
  `;
  document.body.appendChild(ghost);

  // Place cursor at horizontal center, 10px from top
  event.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, 10);

  // Clean up after a brief delay (dragstart has already captured the image by then)
  setTimeout(() => ghost.remove(), 0);
}

You can also use an Image object preloaded with a URL. This is useful when you want a specific icon or thumbnail without having to create a DOM element:

function handleDragStart(event) {
  event.dataTransfer.setData('text/plain', event.target.id);

  const img = new Image();
  img.src = '/icons/drag-card.png';

  // Position cursor at center of a 48x48 icon
  event.dataTransfer.setDragImage(img, 24, 24);
}

If setDragImage() is never called, the browser simply takes a screenshot of the dragged element at the moment the drag begins and uses that as the ghost. This default is usually acceptable for simple cases, but the customization capability is invaluable for polished UIs — for instance, when dragging multiple selected items at once, you might show a stacked-cards ghost with a count badge rather than a snapshot of a single item.

Pulling all these concepts together, a complete, production-quality dragstart handler for a kanban card might look like this:

function handleDragStart(event) {
  const card = event.currentTarget;

  // 1. Store the unique identifier so the drop handler knows which card to move
  event.dataTransfer.setData('text/plain', card.dataset.taskId);

  // 2. Also store richer data for drop targets that can use it
  event.dataTransfer.setData('application/json', JSON.stringify({
    taskId: card.dataset.taskId,
    sourceColumn: card.closest('.column').dataset.columnId
  }));

  // 3. Declare that only a move operation makes sense for kanban cards
  event.dataTransfer.effectAllowed = 'move';

  // 4. Show a compact ghost instead of the full card
  const ghost = document.createElement('div');
  ghost.className = 'drag-ghost';
  ghost.textContent = card.querySelector('.card-title').textContent;
  document.body.appendChild(ghost);
  event.dataTransfer.setDragImage(ghost, 75, 20);
  setTimeout(() => ghost.remove(), 0);

  // 5. Visually dim the original card during the drag
  setTimeout(() => card.classList.add('dragging'), 0);
}

Each step serves a clear purpose: the setData() calls give drop targets what they need, effectAllowed constrains the operation type, setDragImage() polishes the visual feedback, and the class addition provides an in-place visual cue that the card is "in flight." This is the kind of cohesive, well-structured dragstart handler that underpins smooth drag-and-drop experiences in modern web applications.

NotesThe section on event delegation under "Attaching a dragstart Event Listener" goes slightly beyond the listed subtopics but is practically important and commonly needed alongside NodeList iteration. The JSON serialization example under setData() is an additional elaboration that aids real-world understanding. Browser support for setDragImage() is excellent across modern browsers but has historically had quirks in some versions of Safari; instructors may wish to flag this.