Modern CRM users expect fluid, interactive interfaces to organize workloads quickly. Static picklist dropdowns and manual edit screens slow down sales reps and support agents when moving records through complex stages. Building a custom drag-and-drop experience in LWC allows teams to organize pipelines visually, reorder task queues, and trigger automated Apex DML updates or GraphQL mutations when cards are dropped.
1. Understanding the HTML5 Drag & Drop Lifecycle in LWC
Native browser drag-and-drop operations rely on standard event listeners that communicate through a shared data payload:
draggable="true": Enables an HTML element to be dragged by the user.ondragstart: Fires when the user starts dragging an item. The handler captures the record ID or object payload and stores it inevent.dataTransfer.setData().ondragover: Fires continuously as the dragged element hovers over a valid drop target. You must callevent.preventDefault()here to allow the browser to drop the element.ondrop: Fires when the item is released over the drop target. The handler reads the ID usingevent.dataTransfer.getData(), updates the underlying reactive array, and saves changes back to Salesforce.
- Standard API: HTML5 Drag and Drop API (
dataTransferobject). - DOM Security: Fully supported under Lightning Web Security (LWS) and Lightning Locker.
- State Management: Always update reactive JavaScript arrays/data properties instead of manipulating DOM nodes directly with
appendChild(). - Backend Persistence:
updateRecordfromlightning/uiRecordApior Apex controller calls.
2. Step-by-Step Implementation: Building a Status Board
In enterprise LWC development, modifying the DOM directly with appendChild() breaks LWC's reactive rendering engine. The production-ready pattern below updates reactive component state and leverages the Salesforce Lightning Design System (SLDS).
dragAndDropBoard.html)Create draggable card containers and drop targets styled with SLDS cards.
<template>
<lightning-card title="Project Task Board" icon-name="standard:kanban">
<div class="slds-grid slds-gutters slds-p-around_medium">
<!-- In Progress Column / Drop Target -->
<div class="slds-col slds-size_1-of-2">
<div class="stage-column"
data-stage="In Progress"
ondragover={handleDragOver}
ondrop={handleDrop}>
<h3 class="slds-text-heading_small slds-m-bottom_small slds-text-color_weak">
In Progress ({inProgressTasks.length})
</h3>
<template for:each={inProgressTasks} for:item="task">
<div key={task.id}
id={task.id}
data-id={task.id}
class="task-card slds-box slds-box_small slds-theme_default slds-m-bottom_small"
draggable="true"
ondragstart={handleDragStart}>
<p class="slds-text-title_bold">{task.title}</p>
<p class="slds-text-body_small slds-text-color_weak">Owner: {task.owner}</p>
</div>
</template>
</div>
</div>
<!-- Completed Column / Drop Target -->
<div class="slds-col slds-size_1-of-2">
<div class="stage-column"
data-stage="Completed"
ondragover={handleDragOver}
ondrop={handleDrop}>
<h3 class="slds-text-heading_small slds-m-bottom_small slds-text-color_weak">
Completed ({completedTasks.length})
</h3>
<template for:each={completedTasks} for:item="task">
<div key={task.id}
id={task.id}
data-id={task.id}
class="task-card slds-box slds-box_small slds-theme_default slds-m-bottom_small"
draggable="true"
ondragstart={handleDragStart}>
<p class="slds-text-title_bold">{task.title}</p>
<p class="slds-text-body_small slds-text-color_weak">Owner: {task.owner}</p>
</div>
</template>
</div>
</div>
</div>
</lightning-card>
</template>
dragAndDropBoard.js)Manage state reactively and handle transfer events cleanly.
import { LightningElement, track } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class DragAndDropBoard extends LightningElement {
@track taskList = [
{ id: 'TSK-101', title: 'Implement OAuth Token Handshake', owner: 'Alex Rivera', stage: 'In Progress' },
{ id: 'TSK-102', title: 'Refactor Batch Apex Callouts', owner: 'Jordan Lee', stage: 'In Progress' },
{ id: 'TSK-103', title: 'Configure Salesforce Data Cloud', owner: 'Taylor Kim', stage: 'Completed' }
];
get inProgressTasks() {
return this.taskList.filter(task => task.stage === 'In Progress');
}
get completedTasks() {
return this.taskList.filter(task => task.stage === 'Completed');
}
handleDragStart(event) {
// Pass record identifier via dataTransfer
const recordId = event.target.dataset.id;
event.dataTransfer.setData('text/plain', recordId);
event.dataTransfer.effectAllowed = 'move';
}
handleDragOver(event) {
// Prevent default browser behavior to enable dropping
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}
handleDrop(event) {
event.preventDefault();
const recordId = event.dataTransfer.getData('text/plain');
const targetStage = event.currentTarget.dataset.stage;
if (!recordId || !targetStage) {
return;
}
// Reactively update array state
this.taskList = this.taskList.map(task => {
if (task.id === recordId) {
return { ...task, stage: targetStage };
}
return task;
});
this.dispatchEvent(
new ShowToastEvent({
title: 'Task Updated',
message: `Task ${recordId} moved to ${targetStage}`,
variant: 'success'
})
);
// In production, invoke updateRecord from lightning/uiRecordApi here to persist changes in Salesforce
}
}
dragAndDropBoard.css)
.stage-column {
background-color: #f3f3f3;
border: 2px dashed #dddbda;
border-radius: 6px;
padding: 16px;
min-height: 280px;
transition: background-color 0.2s ease;
}
.stage-column:hover {
background-color: #eef4ff;
border-color: #0176d3;
}
.task-card {
cursor: grab;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.task-card:active {
cursor: grabbing;
transform: scale(0.98);
}
.task-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
3. Common Traps & Developer Best Practices
Calling
event.target.appendChild(draggedElement) bypasses LWC's virtual DOM reconciliation. While the item may physically move on the screen, LWC's internal data state remains unchanged, leading to data desynchronization, broken wire adapters, and phantom UI bugs. Always update the underlying JavaScript data array and let LWC re-render the template automatically.
- Use
event.currentTargetfor Drop Targets: InhandleDrop, referenceevent.currentTarget.dataset.stagerather thanevent.target. If a user drops a card onto an existing card inside the column,event.targetwill reference the child card instead of the parent drop zone. - Ensure Mobile Touch Support: The HTML5 Drag and Drop API is designed for desktop mouse pointers. If your component runs on the Salesforce Mobile App, consider integrating lightweight touch polyfills or providing alternative click-to-move buttons for tablet users.
- Enforce Backend Security: When persisting stage changes via Apex or
updateRecord, verify that the active user possesses edit permissions for the target object and field.
Summary
Implementing drag-and-drop functionality in Lightning Web Components provides an intuitive, high-velocity experience for Salesforce users. By pairing the native HTML5 Drag and Drop API with LWC's reactive state engine, developers can build robust Kanban boards, sorting utilities, and stage managers that keep Salesforce data synchronized and secure.