I have been building complex interfaces for over a decade, and custom drag-and-drop is the part that breaks most often. That is why the proposal for a native CSS :drag pseudo-class interests me for performance reasons and not just for convenience. For years the only way to tell a user they had actually grabbed an element was a heavy JavaScript listener plus some manual class toggling. It is messy, it invites race conditions, and the browser should have taken this over a long time ago.
On a WooCommerce product gallery or a Gutenberg block UI, calling classList.add() on every dragstart is enough overhead to drop frames on a cheap phone. And when dragend never fires, which happens more often than most of us like to admit, the “ghost” styles stay stuck on the element. CSS is finally growing selectors for these interactive states.
Why we are still toggling classes by hand
Right now, styling a dragged element means hooking into the HTML Drag and Drop API and writing several lines of JavaScript for what is really one state change. Your logic ends up split between a script file and a stylesheet, which makes it harder to maintain than it should be.
// The Naive Approach
const el = document.querySelector('.draggable-item');
el.addEventListener("dragstart", (e) => {
e.target.classList.add("is-dragging");
});
el.addEventListener("dragend", (e) => {
e.target.classList.remove("is-dragging");
});
That looks harmless until the list has fifty items in it. Each drag sends the browser into the JS engine, then into the DOM, then back through style recalculation. We have mostly just accepted that cost as part of the job.
Cutting the JavaScript middleman out with the CSS :drag pseudo-class
The W3C proposal for :drag pushes all of that scripting out of the way. The whole interaction state can live in your CSS or SCSS file with no JavaScript involved. The main thread does less work, and the styling stays in the style layer where you would look for it anyway.
/* The Future: No JS Required */
.menu-item:drag {
opacity: 0.5;
transform: scale(1.05);
cursor: grabbing;
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}
The element’s own state is only half of it. There is a second push for a ::drag-image pseudo-element. Anyone who has used setDragImage() in JavaScript knows the drill: build a hidden DOM node, style it, hand it to the event object. It is a workaround for something the platform should have handled years ago. I wrote more about why we are moving away from JS bloat in 2025 here.
Refactoring the drag preview
The proposed ::drag-image selector, discussed in W3C Issue 13198, would let us style the “ghost” image that trails the cursor. That means you can design a dedicated visual for the dragging state without adding markup that clutters the real page layout.
/* Customizing the ghost preview */
::drag-image {
content: "📦 Moving Item...";
padding: 10px;
background: #fff6d6;
border: 2px dashed #ff9800;
border-radius: 4px;
}
I have written before about modern CSS features that replace heavy libraries, and this belongs on that list. Native selectors tend to give you smoother animations and better accessibility than a hand-rolled JavaScript equivalent.
If the CSS :drag pseudo-class work is eating your dev hours, I can take it off your plate. I have been wrestling with WordPress since the 4.x days.
Ship less JavaScript
The less JavaScript you spend on basic UI states, the faster the site gets. The CSS :drag pseudo-class is still a draft, so nothing ships tomorrow, but it changes how interaction states get built. An interface styled this way keeps working even when a script fails to load. Watch the spec, and start marking the places in your code that will get simpler once it lands.