Skip to main content
Participating Frequently
August 21, 2026

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.

  • August 21, 2026
  • 0 replies
  • 3 views

# 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.