Expose the drag's live effectAllowed to drop targets

Author: nazar-chCreated Aug 12, 2026Updated Aug 28, 2026

The problem

We map a modifier to copy-vs-move: an Alt/Option drag copies, a plain drag moves. A drop target needs to know whether the modifier is down right now — during dragover to pick the drop effect it shows, and at drop to pick the operation it performs.

The input that drop-target callbacks receive carries the modifier flags, and in Chromium they are live. In WebKit they are not: dragover and drop report the last modifier state the engine observed outside a drag session, so a modifier edge made during a drag is never reflected. Press or release Option mid-drag in Safari and input.altKey keeps the stale value, so a drop target reading it shows one operation while the engine performs another.

The stale flags are a known WebKit bug — bug 202418, open since 2019 — so a platform fix is not near, and the flags on input inherit it.

The one signal both engines keep live on the drag event is dataTransfer.effectAllowed: each engine re-derives it on every drag event, folding the currently held modifier into it — it narrows to copy while the copy modifier is down and widens back on release. In WebKit it is the only live modifier signal on the event. But the library doesn't carry it: getInput copies modifier flags, buttons and coordinates, no callback receives the event or its dataTransfer, and effectAllowed occurs nowhere in the published dist (checked against 2.0.1 and the current get-input.ts on main).

What we need from the library

A way for a drop target to read the drag's current effectAllowed. One shape that covers it: one more field on input, copied off the event in getInput the way the modifier flags already are. At dragover it is live in both engines, and at drop the drop event's effectAllowed is live in both engines too — so the same field answers at the decisive moment, unlike the modifier flags, which WebKit reports stale there as well.

For illustration:

typescript
dropTargetForElements({
    element,
    // works cross-browser: effectAllowed is the one field WebKit keeps live
    getDropEffect: ({ input }) => (input.effectAllowed === 'copy' ? 'copy' : 'move'),
    onDrop: ({ location, self }) => {
        const isCopy = location.current.input.effectAllowed === 'copy';
        // ...
    },
});

What we do meanwhile

The library invokes getDropEffect synchronously from inside its own window dragover listener, so window.event during that call is the very event being answered, and we read window.event.dataTransfer.effectAllowed there. Measured working in both engines across all four modifier timings — but window.event is deprecated, and the route only holds while the callbacks stay synchronous with the dispatch.

Appendix — what we measured about drag modifiers cross-browser

Possibly useful input for this area (it touches what #137 ran into). Method: real OS-level drag gestures on macOS (CGEvent-driven, so the modifier is held by the window server exactly as a hand holds it), against Chrome 150 and Safari Technology Preview 27, 2026-07 and 2026-08. A † marks a value a person also produced by hand and read off the page — the WebKit ones on shipping Safari 26.4, the Chromium ones on Chrome 150. Unmarked values are the harness alone. No hand run contradicted a harness reading, which is why shipping Safari and Technology Preview share one column.

The second table's subject is an isolated page rather than an app: a bare draggable="true" element and a drop target whose only behavior is preventDefault() on dragenter/dragover — nothing sets dropEffect, so what it reports is the engine's own resolution. Its source declares no effectAllowed, which is the situation a pragmatic consumer is in by default, since the library never sets it.

The copy modifier (Option/Alt), per timing. altKey and effectAllowed are read off the same drag event. All four WebKit rows were re-run on Safari 26.4 by hand, and the copy/move outcome of all four was watched on screen in both engines:

modifier timing Chromium altKey Chromium effectAllowed WebKit altKey WebKit effectAllowed
none false all / copyMove false copyMove
held from before dragstart true copy true copy
pressed mid-flight true copy false (stale) † copy
released mid-flight false all / copyMove true (stale) † copyMove

In the Chromium column, all is a source with no declared mask and copyMove a source that declared copyMove at dragstart; WebKit reported copyMove in both cases.

Other modifiers and combinations (macOS naming), measured the same way:

modifier held Chromium effectAllowed Chromium drop delivered WebKit effectAllowed WebKit drop delivered
Option copy yes † copy yes †
Command none no move yes †
Control no drag starts no drag starts †
Shift all (unchanged) yes no drag starts †
Option+Command copy yes copyMove (unchanged) † yes †
Control+Command no drag starts no drag starts

Pressed mid-flight instead of held, the same values hold, with two exceptions: Control becomes reachable (Chromium link, drop delivered; WebKit none, no drop), and so does Shift in WebKit (copyMove, i.e. no effect). "No drag starts" for Control is macOS rather than either engine — a Ctrl-held primary press is a secondary click, so the page gets mousedown then contextmenu and no dragstart at all.

  1. On a WebKit drag event the modifier flags carry the last modifier state the engine observed outside a drag session. mousedown and dragstart are live and correct; dragover and drop are the stale ones. Pressed mid-flight the flag stays false through the drop, released mid-flight it stays true, because a modifier edge that happens during a drag is never observed.
  2. The stale value is not reset when the drag ends, so it is not the dragstart snapshot either. Isolated: hold Cmd from before a drag and release it mid-flight; then start a second drag holding nothing at all. That second drag reports metaKey: false on mousedown and dragstart, and true on every dragover and on the drop. Press and release any modifier between the two drags and it reports false throughout. Chromium is unaffected, and the declared mask makes no difference — the sequence reproduces on a source declaring nothing and on one declaring copyMove. So in Safari the flags can be wrong on a drag during which the user touched no modifier at all, and nothing on the event distinguishes that case.
  3. The engine itself tracks the key correctly; only the flags do not. In the mid-flight press case WebKit paints the copy badge on the drag image while handing the page altKey: false on the same dragover that reports effectAllowed: copy. Both were captured in one frame. So a drop target reading the flags contradicts what the user is being shown.
  4. effectAllowed is live in both engines: transitions observed within ~3 ms of the key edge, in both directions.
  5. dropEffect is not usable as a cross-browser verdict: Chromium reports the live move/copy at drop, WebKit reports none throughout, at dragover and drop alike.
  6. Declaring effectAllowed at dragstart does not pin what later events report: with copyMove declared, both engines still report copy while the modifier is down — each intersects the declared mask with the live modifier and reports the result.
  7. The intersection can kill the gesture asymmetrically. With effectAllowed = 'move' declared and Alt held, Chromium resolves the mask to empty and delivers no drop event at all; WebKit discards the declared mask, reports copy, and delivers the drop. The same shape reappears with a modifier nobody maps: copyMove declared and Ctrl pressed mid-flight intersects to empty in Chromium, and no drop is delivered.
  8. A library-side consequence of points 1 and 2: at drop, the lifecycle manager captures the final input off the drop event, whose modifier flags in WebKit are the stale ones — so location.current.input inside onDrop carries them in Safari even when the answer at that moment is what decides the operation.
  9. On #137's Chrome observation, both halves. The alt half is no longer what Chrome does: in Chrome 150 an Alt-held drag delivers its drop and copies. The meta half still is — a Cmd-held drag resolves effectAllowed to none and delivers no drop event at all, held from before dragstart or pressed mid-flight alike. Releasing Cmd mid-flight recovers the drag within the same gesture: the mask widens back and the drop lands. WebKit does not do this at all; Cmd there pins move and delivers.

Disclosure

Human posted. Prepared with Claude (Fable & Opus): the text and the numbers come from multiple research and measurement sessions — library source reading, OS-level drag-gesture harness runs in both engines, a hand pass on shipping Safari that corrected one of the claims above, and a re-verification of each claim against the published dist before this was written.

The testing bed: https://claude.ai/code/artifact/975fd291-73ad-4db8-a9e0-eb0e5eaefdb5

(I may add Firefox to the matrix or include additional tests if that will be useful.)

Source: atlassian/pragmatic-drag-and-drop