drag-and-drop
Implement drag-and-drop in LWC using HTML5 Drag and Drop API, keyboard alternatives, accessible announcements, and file drop zones. NOT for handling a file the user dropped onto the component - use lwc/file-upload-patterns.
Open the skill folder on GitHub
LWC Drag and Drop
Native HTML5 Drag and Drop works in LWC — no library needed. Three things make it harder than it looks, and all three fail silently: dragover must call preventDefault() or the drop is rejected; dataTransfer is only valid during the event; and LWC has no event modifiers, so the ondragover.prevent syntax people bring from Vue is ignored without a warning.
Build Order: Keyboard First, Drag Last
Drag is a pointer gesture with no fallback. HTML5 drag events do not fire on touch and are unreachable by keyboard, so a drag-only component is unusable by keyboard users, screen-reader users, and every mobile user at once.
- Explicit controls — Alt+Up / Alt+Down on a focusable row (plus Home/End
and Escape). Works everywhere.
- Announcements — an
aria-liveregion carrying pick-up, each move withposition and total, and cancellation.
aria-grabbedandaria-dropeffectare deprecated in ARIA 1.1; do not use them. - Drag — layered on a component that already works.
Built in that order the component is correct on every device from the first commit. Built drag-first, the keyboard path is retrofitted onto a state machine designed around pointer events, and it shows.
Adoption Signals
Any reordering UI (priority list, board columns, file drop zones). Not for complex trees or cross-window drag — wrap a specialised library in one component for those, and verify it under Lightning Web Security in a real org.
Recommended Workflow
- Template:
draggable="true"plusondragstart,ondragover,ondragenter,ondragleave,ondragend,ondrop, andonkeydown. Uselwc:ref/this.refsfor DOM access rather thanthis.template.querySelector(). event.preventDefault()inside thedragoverhandler — there is no.preventmodifier in LWC, and without the call
dropnever fires.dataTransfer.setDatasynchronously indragstart,getDatasynchronouslyin
drop. Outside those windows reads return an empty string.- Reorder by id array, copying before mutating and reassigning the property.
spliceon a reactive array in place does not re-render. - Cleanup in
dragend, notdrop—dragendfires whether or not a dropoccurred, so abandoned drags do not leave stuck state. Guard
dragleaveagainst descendant elements with an enter/leave counter. - Jest: assert
preventDefaultwas called ondragover, cover both interactionpaths, and assert the announcement contains the position and the total. jsdom has drag events but no
DataTransfer— stub it.
Key Considerations
- Touch devices do not fire drag events at all. The keyboard/explicit-control
path is what serves them; a pointer polyfill is not the answer.
- Inside
role="listbox", bare arrows belong to navigation. Reorder takes amodifier or the accessibility work makes the component less accessible.
- An
aria-liveregion only announces on text change — blank it and reset onthe next microtask, or two identical moves announce once.
- Keep the
dragoverhandler trivial. It fires at pointer frequency; puthighlighting in
dragenter/dragleave, which fire once per boundary. - Third-party drag libraries fight shadow DOM (global queries cannot reach the
shadow tree) and run under Lightning Web Security distortions — and most ship no keyboard path, so the hard part remains yours.
Worked Examples (see references/examples.md)
- Priority list reorder — the wrong version and the right one side by side,
with the keyboard path and live-region announcements.
- Announcement model — the three moments a screen-reader user needs, and the
reset-then-set trick that makes them fire.
- File drop zone — layered over a real
<input type="file">, with enter/leavecounting and the folder-drop case.
- Jest tests — both paths, plus the
DataTransferstub jsdom lacks.
Common Gotchas (see references/gotchas.md)
ondragover.preventis Vue syntax — LWC ignores it silently anddropnever fires.
dataTransferoutside the event returns an empty string — read and writesynchronously.
dragleavefires when entering a child — count enters and leaves.- Cleanup in
droponly — abandoned drags leave the dragging class stuck.
Top LLM Anti-Patterns (full list in references/llm-anti-patterns.md)
- Framework event modifiers LWC does not have.
- No keyboard path — which also means no touch support.
aria-grabbed/aria-dropeffect, both deprecated in ARIA 1.1.- Reaching for a drag library, which fights shadow DOM and LWS and still leaves
you to build the keyboard path.
Official Sources Used
- Lightning Web Components Developer Guide — https://developer.salesforce.com/docs/platform/lwc/guide/
- Access Elements the Component Owns (
lwc:ref) — https://developer.salesforce.com/docs/platform/lwc/guide/create-components-dom-work.html - Shadow DOM — https://developer.salesforce.com/docs/platform/lwc/guide/create-dom.html
- How LWS Works — https://developer.salesforce.com/docs/platform/lwc/guide/security-lwsec-architecture.html
- LWS Distortion Viewer — https://developer.salesforce.com/tools/lws-distortion-viewer
- LWC Recipes — https://github.com/trailheadapps/lwc-recipes