In a UXP panel, the CSS cursor property stops being applied as soon as a pointer button is held and moved — it reverts to the default arrow regardless of what the element specifies — so drag cursors such as grabbing are impossible.
# UXP defect report — cursor handling, and four missing DOM features
Prepared 2026-08-20 from measurements taken inside a UXP hybrid plugin panel.
Everything below was **measured in the panel itself**, not inferred: the test
harness, its controls, and the raw verdicts are described at the end.
---
## Environment
| | |
|---|---|
| Photoshop | 27.9 (Windows) |
| OS | Windows 11 Pro, 10.0.26200 |
| Plugin type | UXP **hybrid** (JS panel + native `.uxpaddon`) |
| Manifest | `manifestVersion: 6`, `host.data.apiVersion: 2`, `minVersion 24.2.0` |
| Loaded via | UXP Developer Tool (side-loaded) |
| Panels | two (`type: panel`), one main, one log |
---
## Summary
Five defects, in rough order of impact. The first is the one that makes a class
of ordinary UI impossible.
1. **`cursor` is not re-evaluated once a pointer button is held and moved** —
it reverts to the default arrow regardless of what any element specifies.
2. **`cursor: url(...)` is discarded at parse time**, taking its fallback
keyword with it, through every route we could find.
3. **`document.elementFromPoint` does not exist.**
4. **`CSS.supports` does not exist.**
5. **`<img>` `load`/`error` events never fire**, although the images render.
A sixth, lesser one: **inline `element.style.cursor` assignments are silently
dropped for values that work from a stylesheet.**
None of these appear on the
[What's unsupported in UXP](https://developer.adobe.com/photoshop/uxp/2022/guides/uxp_guide/unsupported/)
page, and `cursor` does not appear in the
[CSS reference](https://developer.adobe.com/photoshop/uxp/2022/uxp-api/reference-css/)
at all — it is neither promised nor disclaimed, which is why this had to be
established by experiment.
---
## 1. `cursor` is not updated during a drag
### Steps to reproduce
1. Give any element a cursor, by any means: `el.style.cursor = "crosshair"`, or
a stylesheet rule.
2. Hover it. The crosshair appears — correct.
3. Press and hold the primary button without moving. The crosshair remains —
correct.
4. Move the pointer while the button is held.
### Expected
The cursor continues to be resolved from the element under the pointer (or the
pointer-capture target), as in any browser.
### Actual
It becomes the **default arrow** on the first movement and stays there until
the button is released. The element's `cursor` is unchanged and still reads
correctly through `getComputedStyle`.
### Also observed
While a button is held, the engine **keeps whatever cursor was already showing
but will not pick up a change**. Setting `cursor` on `pointerdown` has no
effect; the cursor that appears is the one that was showing before the press.
### What was ruled out
Each of these was tried and made no difference:
- inline `style.cursor` vs a stylesheet rule;
- setting it on the element, on an ancestor, and on `document.body`;
- re-asserting the value on every `pointermove`;
- `draggable="false"` plus a `dragstart` handler calling `preventDefault()`
(i.e. it is not a native image drag);
- removing `setPointerCapture` entirely;
- `<div>` vs `<img>` as the element.
`getComputedStyle` was used to confirm the value was present and correct at the
moment the arrow was on screen — so this is a painting/hit-testing behaviour,
not a failure to apply the style.
### Impact
Two entirely ordinary interactions are impossible in a UXP panel:
- `grabbing` while dragging a pannable image;
- a crosshair (or any modal cursor) that appears when a sampling gesture
starts.
This may be related to *drag and drop — coming soon* on the unsupported page.
If the drag subsystem is unfinished, cursor handling during a drag being
inoperative is consistent with that, and the two are probably one item.
---
## 2. `cursor: url(...)` is discarded at parse time
### Result
**45 verdicts: 5 routes × 9 values.** Every route's keyword control passed, and
**every** value containing `url()` resolved to `auto` — not to the fallback
keyword, to `auto`, meaning the whole declaration was dropped.
| route | keyword control | any `url()` value |
|---|---|---|
| `el.style.cursor = …` | **PASS** | FAIL ×8 |
| `el.style.setProperty("cursor", …)` | **PASS** | FAIL ×8 |
| `el.setAttribute("style", "cursor:…")` | **PASS** | FAIL ×8 |
| stylesheet rule | **PASS** | FAIL ×8 |
| injected `<style>` element | **PASS** | FAIL ×8 |
The nine values: `crosshair` (control), PNG + hotspot + fallback, PNG +
fallback, PNG alone, PNG 16×16, SVG, **native Windows `.cur`**, `data:` URI
PNG, and `-webkit-image-set`.
### The assets were proven to exist
Verified through `localFileSystem` — present and readable in the plugin folder,
with byte counts — *before* the cursor tests ran, so the failures cannot be
attributed to missing files. The `.cur` was a real Windows cursor: `ICONDIR`,
32-bit BGRA, AND mask, hotspot in the header.
### Note on the fallback
`cursor: url(x.png), pointer` resolving to `auto` rather than `pointer` is the
detail worth attention. Per CSS, an unrecognised `url()` should invalidate the
declaration — so this may be correct per spec. But it means a plugin cannot
write a progressive-enhancement rule that uses an image cursor where available
and a keyword elsewhere; the keyword has to be a **separate declaration** on a
preceding line. Worth documenting either way.
---
## 3. `document.elementFromPoint` does not exist
`typeof document.elementFromPoint === "undefined"`.
There is no way for a panel to ask what element is under a given point. This is
load-bearing for hit-testing in any custom control, and for diagnosing issues
like #1 — we wrote a diagnostic that depended on it and it could never have
reported anything.
---
## 4. `CSS.supports` does not exist
`CSS.supports` is unavailable, so a plugin cannot feature-detect CSS at all. In
a runtime whose CSS support differs from browsers in undocumented ways, this is
the one API that would let a plugin adapt rather than guess.
---
## 5. `<img>` load events never fire
`onload` and `onerror` were both silent for four separate image files over a
3-second window, while **the same images rendered correctly on screen**.
The images load. The events do not exist. Any code that waits for a load — to
size an element, to sequence work, to detect a broken asset — hangs forever
without erroring.
---
## 6. Inline `style.cursor` silently drops values that work in a stylesheet
Writing `element.style.cursor = "grab"` to five elements left
`element.style.cursor` reading **empty** on all five, immediately after the
assignment. `getComputedStyle` showed `grab` only where a *stylesheet* rule
supplied it, and `auto` on the element that had none.
`crosshair` assigned the same way does apply. So the inline path accepts some
keywords and silently drops others, with no error and no indication which.
---
## How this was measured
A test harness inside the panel, run against the live runtime:
- **A full cross-product**, not one combination — route × value, so the
variables could not be confounded.
- **A control on every row.** Each route applied a plain keyword first. Without
that, a failure cannot be told apart from "this route does not work". Every
control passed, so none of the `url()` failures are void.
- **Inputs proven real.** Asset existence and byte size verified through
`localFileSystem` before the cursor tests ran.
- **Verdicts, not values.** Each check states its expectation, then PASS/FAIL,
so a wrong expectation is visible as a wrong expectation.
- **Two halves reconciled.** `getComputedStyle` reports what the engine
resolved; what it *paints* was recorded by eye against the same numbered
cells. Where the two disagree — value present, arrow on screen — that
disagreement is the finding.
---
## Observed but not isolated
Reported for completeness and explicitly **not** measured to the standard
above. These may be our own errors:
- `border` on a flex-container `<div>` appeared not to paint, while the same
shorthand painted on a nearby non-flex `<div>`. Worked around with a
background fill; not isolated.
- `gap` between two flex items appeared to be applied at roughly a third of the
specified value at the container level, while behaving correctly inside a
row. Worked around with an explicit margin; not isolated.
- A `title` on a parent element does not produce a tooltip when the pointer is
over a child that has none — and the **previously shown tooltip remains on
screen**, so an element with no title displays a neighbouring element's
tooltip. This one is reproducible and reliable; it is here only because we
did not build a matrix for it. It is arguably the second-most disruptive
item in this document, because the wrong tooltip is worse than none.
---
## What would help most
1. State the position on `cursor` in the docs — supported, partially
supported, or not — so plugins can stop discovering it.
2. If drag-and-drop work is planned, treat cursor-during-drag as part of it.
3. `elementFromPoint` and `CSS.supports` are small surfaces with high value for
plugins doing custom controls.
4. `<img>` load events, or a documented statement that they do not fire.
