[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-drag-drop-api-dragover-preventdefault-trap":44,"search-suggestions":60,"quiz-article-drag-drop-api-dragover-preventdefault-trap":107},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a09fda-b888-7792-9ca1-4d3cff9b376e","drag-drop-api-dragover-preventdefault-trap","PRACTICE_QUIZ","Drag and Drop — the dragover trap","Seven questions on why native HTML drag-and-drop refuses to work until you cancel the right default, and the landmines that show up right after you fix it.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,100,104],{"slug":62,"name":63,"articles":64},"webdev","Webdev",103,{"slug":66,"name":67,"articles":68},"javascript","Javascript",87,{"slug":70,"name":71,"articles":72},"frontend","Frontend",71,{"slug":74,"name":75,"articles":76},"css","Css",34,{"slug":78,"name":79,"articles":80},"tutorial","Tutorial",32,{"slug":82,"name":83,"articles":84},"typescript","Typescript",15,{"slug":86,"name":87,"articles":88},"react","React",13,{"slug":90,"name":91,"articles":92},"performance","Performance",12,{"slug":94,"name":95,"articles":96},"browser","Browser",9,{"slug":98,"name":99,"articles":96},"node","Node",{"slug":101,"name":102,"articles":103},"html","Html",6,{"slug":105,"name":106,"articles":103},"grammar","Grammar",{"id":108,"slug":46,"title":109,"subtitle":52,"excerpt":110,"coverUrl":111,"locale":13,"readingMinutes":103,"publishedAt":112,"viewCount":113,"likeCount":19,"commentCount":19,"author":114,"vertical":119,"topic":120,"tags":122,"_count":127,"playground":129,"body":130,"bodyMd":273,"seo":274,"translationGroupId":277,"series":52,"podcastUrl":52,"verticalId":5,"thread":278,"assessments":280,"translations":283,"quiz":285},"01a09fda-b750-710a-9a40-8113c1d054b3","Your Browser Is Rejecting Every Drop On Purpose","A drop zone with a correctly wired drop listener that never fires — no error, no typo, nothing in the console. The browser is silently refusing the drop by design, and only one specific line stops it.","\u002Fmedia\u002Fcovers\u002Fdrag-drop-api-dragover-preventdefault-trap.png","2026-09-20T16:26:35.914Z",57,{"id":115,"name":116,"username":117,"avatarUrl":52,"headline":118},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":66,"name":121},"JavaScript",[123,124,125,126],{"slug":66,"name":67,"color":52},{"slug":62,"name":63,"color":52},{"slug":101,"name":102,"color":52},{"slug":78,"name":79,"color":52},{"assessments":128},1,{"slug":46,"title":48},{"blocks":131,"version":128},[132,136,139,145,148,151,156,159,162,165,168,171,174,178,181,184,187,190,194,197,200,204,207,210,213,216,219,223,226,229,233,236,239,242,245,248,251,254,257,260,263,266],{"id":133,"html":134,"type":135},"b1","\u003Cp>A teammate pings you: the drag-and-drop on the Kanban board doesn&#39;t work. You can pick a card up — it goes translucent, follows the cursor, everything looks right — but dropping it on a column does nothing. No error. No red text in the console. The card just snaps back like the drop never happened.\u003C\u002Fp>","paragraph",{"id":137,"html":138,"type":135},"b2","\u003Cp>You open the code. The listener is right there:\u003C\u002Fp>",{"id":140,"code":141,"type":142,"language":143,"highlight":144},"b3","dropzone.addEventListener(\"drop\", (e) => {\n  const id = e.dataTransfer.getData(\"text\u002Fplain\");\n  dropzone.appendChild(document.getElementById(id));\n});","code","js",[],{"id":146,"html":147,"type":135},"b4","\u003Cp>Right element. Right event name, spelled correctly. \u003Ccode>dataTransfer.setData\u003C\u002Fcode> was called in \u003Ccode>dragstart\u003C\u002Fcode> with the same key. By every reasonable check, this should work. It doesn&#39;t — and it isn&#39;t going to, no matter how long you stare at this exact function, because the bug isn&#39;t in it.\u003C\u002Fp>",{"id":149,"html":150,"type":135},"b5","\u003Cp>\u003Cstrong>Guess before you scroll:\u003C\u002Fstrong> the fix isn&#39;t in the \u003Ccode>drop\u003C\u002Fcode> handler at all. It&#39;s a handler that isn&#39;t there yet.\u003C\u002Fp>",{"id":152,"html":153,"text":154,"type":155,"level":31},"b6","The obvious things you&#39;d check first — and why they don&#39;t help","The obvious things you'd check first — and why they don't help","heading",{"id":157,"html":158,"type":135},"b7","\u003Cp>The instinct is to suspect \u003Ccode>draggable\u003C\u002Fcode>. Divs aren&#39;t draggable by default the way images and links are, so you add \u003Ccode>draggable=&quot;true&quot;\u003C\u002Fcode> to the card. Now \u003Ccode>dragstart\u003C\u002Fcode> fires, the ghost image appears, everything about \u003Cem>picking up\u003C\u002Fem> the card works exactly as it should.\u003C\u002Fp>",{"id":160,"html":161,"type":135},"b8","\u003Cp>Dropping it still does nothing.\u003C\u002Fp>",{"id":163,"html":164,"type":135},"b9","\u003Cp>Next instinct: maybe \u003Ccode>dataTransfer\u003C\u002Fcode> needs a different MIME key, or \u003Ccode>setData\u003C\u002Fcode>\u002F\u003Ccode>getData\u003C\u002Fcode> need to match more exactly. You double, triple check the strings. They already matched. That was never the problem either.\u003C\u002Fp>",{"id":166,"html":167,"type":135},"b10","\u003Cp>Here&#39;s the part that&#39;s easy to miss: \u003Cstrong>the browser doesn&#39;t consider your \u003Ccode>dropzone\u003C\u002Fcode> a valid place to drop anything, and it never told you.\u003C\u002Fstrong> Its default behavior for \u003Cem>any\u003C\u002Fem> element receiving a drag is to refuse the drop — the same way it refuses to run a \u003Ccode>submit\u003C\u002Fcode> button&#39;s default action once you call \u003Ccode>preventDefault()\u003C\u002Fcode> on the form&#39;s \u003Ccode>submit\u003C\u002Fcode> event, except inverted: here, doing nothing \u003Cem>is\u003C\u002Fem> the default, and you have to actively cancel it to unlock the alternative.\u003C\u002Fp>",{"id":169,"html":170,"text":170,"type":155,"level":31},"b11","The one handler nobody adds because its name gives nothing away",{"id":172,"html":173,"type":135},"b12","\u003Cp>The event that carries this default is \u003Ccode>dragover\u003C\u002Fcode> — the one that fires repeatedly while the dragged item hovers over your element, several times a second, for as long as the drag continues. It sounds like a bookkeeping event, the kind you&#39;d only need for a hover highlight. It is actually the gate:\u003C\u002Fp>",{"id":175,"code":176,"type":142,"language":143,"highlight":177},"b13","dropzone.addEventListener(\"dragover\", (e) => {\n  e.preventDefault(); \u002F\u002F \u003C-- this is the whole fix\n  e.dataTransfer.dropEffect = \"move\";\n});",[],{"id":179,"html":180,"type":135},"b14","\u003Cp>Add that one handler — nothing fancier, no new logic in \u003Ccode>drop\u003C\u002Fcode> — and the exact same \u003Ccode>drop\u003C\u002Fcode> listener from before starts firing. The browser was checking, on every single \u003Ccode>dragover\u003C\u002Fcode> tick, whether \u003Cem>something\u003C\u002Fem> had canceled its &quot;refuse this drop&quot; default. Nothing had. So by the time the cursor was released, there was nothing to drop \u003Cem>onto\u003C\u002Fem>, and the \u003Ccode>drop\u003C\u002Fcode> event was simply never dispatched. Not swallowed, not caught somewhere — never sent at all.\u003C\u002Fp>",{"id":182,"html":183,"type":135},"b15","\u003Cp>Most tutorials also cancel \u003Ccode>dragenter\u003C\u002Fcode>&#39;s default alongside \u003Ccode>dragover\u003C\u002Fcode>&#39;s, and it&#39;s worth doing — it lets you flip a highlight class the instant the drag enters instead of waiting for the first \u003Ccode>dragover\u003C\u002Fcode> tick, and it papers over a few older-Safari quirks. But \u003Ccode>dragover\u003C\u002Fcode> is the one that&#39;s load-bearing. Cancel only \u003Ccode>dragenter\u003C\u002Fcode> and skip \u003Ccode>dragover\u003C\u002Fcode>, and you&#39;re back to the same silent nothing you started with.\u003C\u002Fp>",{"id":185,"html":186,"text":186,"type":155,"level":31},"b16","The second landmine, right behind the first",{"id":188,"html":189,"type":135},"b17","\u003Cp>Fix that, and a Kanban board&#39;s columns usually need a hover highlight — &quot;you&#39;re currently over a valid drop zone&quot; — added and removed as the drag moves. The naive version:\u003C\u002Fp>",{"id":191,"code":192,"type":142,"language":143,"highlight":193},"b18","dropzone.addEventListener(\"dragenter\", () => dropzone.classList.add(\"over\"));\ndropzone.addEventListener(\"dragleave\", () => dropzone.classList.remove(\"over\"));",[],{"id":195,"html":196,"type":135},"b19","\u003Cp>This flickers if the drop zone has \u003Cem>any\u003C\u002Fem> child elements — a card, a label, an icon — because \u003Ccode>dragenter\u003C\u002Fcode>\u002F\u003Ccode>dragleave\u003C\u002Fcode> fire per element, and they bubble. Move the cursor from the column onto a card sitting inside it, and the browser fires \u003Ccode>dragleave\u003C\u002Fcode> on the column (you left it for the card) immediately followed by \u003Ccode>dragenter\u003C\u002Fcode> on the card. Your highlight class turns off and back on, several times a second, every time the cursor crosses a child boundary. It isn&#39;t a bug in your logic — it&#39;s the DOM correctly reporting boundary crossings you didn&#39;t think to account for.\u003C\u002Fp>",{"id":198,"html":199,"type":135},"b20","\u003Cp>The fix is to stop trusting &quot;did I leave&quot; and check &quot;did I leave into something outside this element,&quot; using the leave event&#39;s \u003Ccode>relatedTarget\u003C\u002Fcode>:\u003C\u002Fp>",{"id":201,"code":202,"type":142,"language":143,"highlight":203},"b21","dropzone.addEventListener(\"dragleave\", (e) => {\n  if (!dropzone.contains(e.relatedTarget)) dropzone.classList.remove(\"over\");\n});",[],{"id":205,"html":206,"type":135},"b22","\u003C!-- playground:start -->",{"id":208,"html":209,"text":209,"type":155,"level":31},"b23","🎮 Try it yourself",{"id":211,"html":212,"type":135},"b24","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdrag-drop-api-dragover-preventdefault-trap\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":214,"html":215,"type":135},"b25","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":217,"html":218,"type":135},"b26","\u003C!-- playground:end -->",{"id":220,"html":221,"text":222,"type":155,"level":31},"b27","The data you can&#39;t read yet, either","The data you can't read yet, either",{"id":224,"html":225,"type":135},"b28","\u003Cp>One more surprise waits for anyone who tries to log the dragged payload early, to debug the two problems above: call \u003Ccode>e.dataTransfer.getData(&quot;text\u002Fplain&quot;)\u003C\u002Fcode> inside \u003Ccode>dragover\u003C\u002Fcode> or \u003Ccode>dragenter\u003C\u002Fcode>, and you get back an empty string — even though \u003Ccode>dataTransfer.types\u003C\u002Fcode> correctly lists \u003Ccode>&quot;text\u002Fplain&quot;\u003C\u002Fcode> as available, and even though the exact same call works fine one line later, inside \u003Ccode>drop\u003C\u002Fcode>.\u003C\u002Fp>",{"id":227,"html":228,"type":135},"b29","\u003Cp>That&#39;s not a race condition and it&#39;s not something you configured. The drag data store runs in what the spec calls &quot;protected mode&quot; for the whole drag except the \u003Ccode>dragstart\u003C\u002Fcode> moment (when it&#39;s writable) and the \u003Ccode>drop\u003C\u002Fcode> moment (when it&#39;s finally readable) — a deliberate guard so that a page you&#39;re merely dragging \u003Cem>over\u003C\u002Fem>, which never receives the drop, can&#39;t read what you&#39;re carrying. If you need to know what&#39;s being dragged before the drop — to decide, say, whether this drop zone should even light up for this drag — read \u003Ccode>dataTransfer.types\u003C\u002Fcode> (always available) instead of the data itself.\u003C\u002Fp>",{"id":230,"html":231,"text":232,"type":155,"level":31},"b30","Where this doesn&#39;t reach","Where this doesn't reach",{"id":234,"html":235,"type":135},"b31","\u003Cp>All of this — \u003Ccode>dragstart\u003C\u002Fcode>, \u003Ccode>dragover\u003C\u002Fcode>, \u003Ccode>drop\u003C\u002Fcode>, the whole \u003Ccode>dataTransfer\u003C\u002Fcode> dance — is a mouse-and-desktop story. No major mobile browser fires these events for a finger drag; the touch input model and the HTML Drag and Drop spec were never wired together. If your Kanban board needs to be reorderable on a phone, this API isn&#39;t the one that gets you there — you&#39;re reaching for Pointer Events with your own hit-testing, or a library like SortableJS that&#39;s already done that work. Ship native drag-and-drop as a desktop enhancement, not the only way in.\u003C\u002Fp>",{"id":237,"html":238,"type":135},"b32","\u003Cp>The teammate&#39;s bug report wasn&#39;t wrong, and neither was their code. It was missing one handler whose entire job is invisible until you know to look for it — the browser was waiting, tick after tick, for a \u003Ccode>preventDefault()\u003C\u002Fcode> call that never came, and nothing about a missing \u003Ccode>drop\u003C\u002Fcode> event tells you that&#39;s what&#39;s missing.\u003C\u002Fp>",{"id":240,"html":241,"type":135},"b33","\u003C!-- quiz:start -->",{"id":243,"html":244,"text":244,"type":155,"level":31},"b34","🧠 Test yourself",{"id":246,"html":247,"type":135},"b35","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdrag-drop-api-dragover-preventdefault-trap\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":249,"html":250,"type":135},"b36","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":252,"html":253,"type":135},"b37","\u003C!-- quiz:end -->",{"id":255,"html":256,"type":135},"b38","\u003Cp>Go check your own drop zones. If you&#39;ve got a \u003Ccode>drop\u003C\u002Fcode> listener with no \u003Ccode>dragover\u003C\u002Fcode> listener next to it, it&#39;s not broken by accident — it&#39;s working exactly as specified, refusing every drop on purpose. What&#39;s the last time a &quot;the listener&#39;s just not firing&quot; bug turned out to be a default you needed to cancel somewhere else entirely?\u003C\u002Fp>",{"id":258,"type":259},"b39","divider",{"id":261,"html":262,"type":135},"b40","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":264,"html":265,"type":135},"b41","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":267,"type":268,"items":269,"ordered":18},"b42","list",[270,271,272],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","A teammate pings you: the drag-and-drop on the Kanban board doesn't work. You can pick a card up — it goes translucent, follows the cursor, everything looks right — but dropping it on a column does nothing. No error. No red text in the console. The card just snaps back like the drop never happened.\n\nYou open the code. The listener is right there:\n\n```js\ndropzone.addEventListener(\"drop\", (e) => {\n  const id = e.dataTransfer.getData(\"text\u002Fplain\");\n  dropzone.appendChild(document.getElementById(id));\n});\n```\n\nRight element. Right event name, spelled correctly. `dataTransfer.setData` was called in `dragstart` with the same key. By every reasonable check, this should work. It doesn't — and it isn't going to, no matter how long you stare at this exact function, because the bug isn't in it.\n\n**Guess before you scroll:** the fix isn't in the `drop` handler at all. It's a handler that isn't there yet.\n\n## The obvious things you'd check first — and why they don't help\n\nThe instinct is to suspect `draggable`. Divs aren't draggable by default the way images and links are, so you add `draggable=\"true\"` to the card. Now `dragstart` fires, the ghost image appears, everything about *picking up* the card works exactly as it should.\n\nDropping it still does nothing.\n\nNext instinct: maybe `dataTransfer` needs a different MIME key, or `setData`\u002F`getData` need to match more exactly. You double, triple check the strings. They already matched. That was never the problem either.\n\nHere's the part that's easy to miss: **the browser doesn't consider your `dropzone` a valid place to drop anything, and it never told you.** Its default behavior for *any* element receiving a drag is to refuse the drop — the same way it refuses to run a `submit` button's default action once you call `preventDefault()` on the form's `submit` event, except inverted: here, doing nothing *is* the default, and you have to actively cancel it to unlock the alternative.\n\n## The one handler nobody adds because its name gives nothing away\n\nThe event that carries this default is `dragover` — the one that fires repeatedly while the dragged item hovers over your element, several times a second, for as long as the drag continues. It sounds like a bookkeeping event, the kind you'd only need for a hover highlight. It is actually the gate:\n\n```js\ndropzone.addEventListener(\"dragover\", (e) => {\n  e.preventDefault(); \u002F\u002F \u003C-- this is the whole fix\n  e.dataTransfer.dropEffect = \"move\";\n});\n```\n\nAdd that one handler — nothing fancier, no new logic in `drop` — and the exact same `drop` listener from before starts firing. The browser was checking, on every single `dragover` tick, whether *something* had canceled its \"refuse this drop\" default. Nothing had. So by the time the cursor was released, there was nothing to drop *onto*, and the `drop` event was simply never dispatched. Not swallowed, not caught somewhere — never sent at all.\n\nMost tutorials also cancel `dragenter`'s default alongside `dragover`'s, and it's worth doing — it lets you flip a highlight class the instant the drag enters instead of waiting for the first `dragover` tick, and it papers over a few older-Safari quirks. But `dragover` is the one that's load-bearing. Cancel only `dragenter` and skip `dragover`, and you're back to the same silent nothing you started with.\n\n## The second landmine, right behind the first\n\nFix that, and a Kanban board's columns usually need a hover highlight — \"you're currently over a valid drop zone\" — added and removed as the drag moves. The naive version:\n\n```js\ndropzone.addEventListener(\"dragenter\", () => dropzone.classList.add(\"over\"));\ndropzone.addEventListener(\"dragleave\", () => dropzone.classList.remove(\"over\"));\n```\n\nThis flickers if the drop zone has *any* child elements — a card, a label, an icon — because `dragenter`\u002F`dragleave` fire per element, and they bubble. Move the cursor from the column onto a card sitting inside it, and the browser fires `dragleave` on the column (you left it for the card) immediately followed by `dragenter` on the card. Your highlight class turns off and back on, several times a second, every time the cursor crosses a child boundary. It isn't a bug in your logic — it's the DOM correctly reporting boundary crossings you didn't think to account for.\n\nThe fix is to stop trusting \"did I leave\" and check \"did I leave into something outside this element,\" using the leave event's `relatedTarget`:\n\n```js\ndropzone.addEventListener(\"dragleave\", (e) => {\n  if (!dropzone.contains(e.relatedTarget)) dropzone.classList.remove(\"over\");\n});\n```\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdrag-drop-api-dragover-preventdefault-trap\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## The data you can't read yet, either\n\nOne more surprise waits for anyone who tries to log the dragged payload early, to debug the two problems above: call `e.dataTransfer.getData(\"text\u002Fplain\")` inside `dragover` or `dragenter`, and you get back an empty string — even though `dataTransfer.types` correctly lists `\"text\u002Fplain\"` as available, and even though the exact same call works fine one line later, inside `drop`.\n\nThat's not a race condition and it's not something you configured. The drag data store runs in what the spec calls \"protected mode\" for the whole drag except the `dragstart` moment (when it's writable) and the `drop` moment (when it's finally readable) — a deliberate guard so that a page you're merely dragging *over*, which never receives the drop, can't read what you're carrying. If you need to know what's being dragged before the drop — to decide, say, whether this drop zone should even light up for this drag — read `dataTransfer.types` (always available) instead of the data itself.\n\n## Where this doesn't reach\n\nAll of this — `dragstart`, `dragover`, `drop`, the whole `dataTransfer` dance — is a mouse-and-desktop story. No major mobile browser fires these events for a finger drag; the touch input model and the HTML Drag and Drop spec were never wired together. If your Kanban board needs to be reorderable on a phone, this API isn't the one that gets you there — you're reaching for Pointer Events with your own hit-testing, or a library like SortableJS that's already done that work. Ship native drag-and-drop as a desktop enhancement, not the only way in.\n\nThe teammate's bug report wasn't wrong, and neither was their code. It was missing one handler whose entire job is invisible until you know to look for it — the browser was waiting, tick after tick, for a `preventDefault()` call that never came, and nothing about a missing `drop` event tells you that's what's missing.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdrag-drop-api-dragover-preventdefault-trap\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\nGo check your own drop zones. If you've got a `drop` listener with no `dragover` listener next to it, it's not broken by accident — it's working exactly as specified, refusing every drop on purpose. What's the last time a \"the listener's just not firing\" bug turned out to be a default you needed to cancel somewhere else entirely?\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":109,"canonical":275,"description":276},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdrag-drop-api-dragover-preventdefault-trap","A drop zone with a correctly wired drop listener that never fires — no error, no typo, nothing in the console. The browser is silently refusing the drop by design, and only one spe","01a09fda-b750-710a-9a40-85fc2fa01bff",{"id":279,"locked":18},"01a09fda-b85f-7669-b8e3-12cf8ea15e9c",[281],{"id":45,"slug":46,"title":48,"_count":282},{"questions":51},[284],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":286,"questionCount":51},{"questions":51}]