QA How-To
How to Use Playwright drag and drop (2026)
Learn Playwright drag and drop with dragTo, steps, positions, mouse control, DataTransfer, and file drops using reliable 2026 TypeScript tests in real suites.
25 min read | 3,449 words
TL;DR
Use source.dragTo(target) for normal Playwright drag and drop. Add steps or relative positions when the widget needs them, use page.mouse for exact geometry, and use Locator.drop for external files or MIME data.
Key Takeaways
- Start element-to-element workflows with Locator.dragTo and assert the state afterward.
- Use steps for widgets that require intermediate mouse movement or dragover activity.
- Choose sourcePosition and targetPosition only from meaningful component geometry.
- Use page.mouse for coordinate-driven canvases and spatial controls.
- Use Locator.drop for external files or MIME data, not for moving a visible element.
- Reserve DataTransfer event dispatch for specialized contracts and keep real gesture coverage.
- Verify arrival, departure, persistence, feedback, and keyboard alternatives according to risk.
Playwright drag and drop is most reliable when you start with Locator.dragTo(), choose stable source and target locators, and assert the resulting application state. In 2026, the dragTo options include sourcePosition, targetPosition, steps, trial, force, and timeout. For external file or clipboard-style drops, use Locator.drop() rather than simulating a draggable page element.
Not every interface implements dragging the same way. A sortable board may react to pointer movement, an HTML5 widget may inspect DataTransfer, a canvas may require exact coordinates, and a file zone expects external data. The correct Playwright API depends on that interaction contract.
This guide builds a decision process from the simplest locator action to precise mouse control and synthetic external drops. All examples use Playwright Test with TypeScript and web-first assertions.
TL;DR
import { test, expect } from '@playwright/test';
test('moves a task to Done', async ({ page }) => {
await page.goto('/board');
const task = page.getByTestId('task-TASK-42');
const done = page.getByTestId('column-done');
await task.dragTo(done, { steps: 10 });
await expect(done.getByTestId('task-TASK-42')).toBeVisible();
await expect(page.getByRole('status')).toHaveText('Task moved to Done');
});
| Interaction | Best first choice |
|---|---|
| Element to element | source.dragTo(target) |
| Selector-based legacy helper | page.dragAndDrop(source, target) |
| Exact path or canvas | hover plus page.mouse methods |
| File dropped from desktop | target.drop({ files }) |
| Text or URL dropped externally | target.drop({ data }) |
| Custom HTML5 payload | dispatchEvent with a DataTransfer handle |
1. Understand What Playwright Drag and Drop Must Reproduce
A drag gesture is a sequence, not a single DOM event. For a typical element drag, Playwright moves to the source, presses the mouse, moves toward the target, and releases. Application code may listen for pointer, mouse, or HTML drag events and update visual state during that sequence.
Before automating, inspect the UI contract:
- What exact element owns the drag handle?
- Is the whole card draggable or only an icon?
- Does the target accept a drop at its center?
- Must the pointer cross intermediate zones?
- Is the interface an HTML element, SVG, canvas, or external file target?
- What observable state proves success?
- Is there a keyboard alternative for accessibility?
These answers determine the method. Locator.dragTo is the default for element-to-element workflows because locators re-resolve elements and receive actionability checks. page.dragAndDrop remains a current API, but it accepts selector strings and can select the first match unless strict mode is requested. Locator-based code communicates ownership more clearly.
Locator.drop is a different operation. It simulates an external drop of files or MIME-typed data onto a target and dispatches dragenter, dragover, and drop with a synthetic DataTransfer. It does not drag a visible source element across the page.
| API | Visible source element | External payload | Auto-waited locator |
|---|---|---|---|
| locator.dragTo | Yes | No | Yes |
| page.dragAndDrop | Yes | No | Selector based |
| page.mouse sequence | Optional | No automatic payload | Manual |
| locator.drop | No | Files or MIME data | Yes |
| locator.dispatchEvent | Optional | Custom handle possible | Event-level |
Choosing the right model prevents most flaky workarounds.
2. Set Up a Cross-Browser Drag Test Project
Install Playwright Test and browsers:
npm init playwright@latest
npx playwright install
A focused configuration can run drag behavior in Chromium, Firefox, and WebKit:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
retries: process.env.CI ? 2 : 0,
use: {
baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
Use deterministic data. A board test should seed known columns and task IDs, not depend on whatever a shared environment currently contains. If the drop persists to a backend, cleanup or create a unique task for each test and retry.
Prefer semantic roles for controls and test IDs for domain objects without a stable accessible identity. A task card might contain changing text but have data-testid="task-TASK-42". The target column might be located by heading plus container relationship. Keep locators unique because drag actions should never guess among duplicates.
For broader locator guidance, see Playwright getByRole and Playwright getByTestId.
Do not disable parallel execution merely because a drag test is unstable. First isolate backend state, output, and browser context. Parallelism usually exposes shared-data defects rather than a limitation of dragTo.
3. Use Locator.dragTo for the Basic Workflow
The simplest correct test identifies one draggable source and one drop target, asserts preconditions, performs dragTo, and verifies a durable outcome.
import { test, expect } from '@playwright/test';
test('moves a backlog card into In Progress', async ({ page }) => {
await page.goto('/kanban');
const card = page.getByTestId('card-TASK-42');
const backlog = page.getByTestId('column-backlog');
const inProgress = page.getByTestId('column-in-progress');
await expect(backlog.getByTestId('card-TASK-42')).toBeVisible();
await expect(inProgress.getByTestId('card-TASK-42')).toHaveCount(0);
await card.dragTo(inProgress);
await expect(inProgress.getByTestId('card-TASK-42')).toBeVisible();
await expect(backlog.getByTestId('card-TASK-42')).toHaveCount(0);
await expect(page.getByRole('status'))
.toHaveText('TASK-42 moved to In Progress');
});
The assertions verify both arrival and departure. This catches a UI bug that visually clones the card instead of moving it. The status assertion verifies user feedback and often synchronizes with persistence.
dragTo performs actionability checks unless force is true. It waits for the source and target to be actionable and scrolls them as needed. Avoid preceding it with manual visibility polling or fixed sleeps.
If a drag handle owns the gesture, use that locator as the source while asserting the card's new location:
const card = page.getByTestId('card-TASK-42');
const handle = card.getByRole('button', { name: 'Drag task' });
const target = page.getByTestId('column-done');
await handle.dragTo(target);
await expect(target.getByTestId('card-TASK-42')).toBeVisible();
The source locator must match the element whose pointer behavior starts the drag. Dragging the card container can fail when only the handle registers listeners.
4. Tune steps and Source or Target Positions
Some widgets require multiple move events before they activate a drop zone. Current dragTo provides steps, which sends interpolated mousemove events between source and destination. Start with a modest value such as 5 or 10 only when the default single move does not exercise the component correctly.
await source.dragTo(target, {
steps: 10,
});
Positions are relative to the top-left corner of each element's padding box. Use them when the source has a small handle or the target is divided into meaningful regions:
await source.dragTo(target, {
sourcePosition: { x: 12, y: 12 },
targetPosition: { x: 30, y: 80 },
steps: 12,
});
Do not choose unexplained coordinates copied from a local screen. Inspect component geometry and express why the point is meaningful, such as the center of a drag handle or the lower half of a sortable row.
trial: true performs actionability checks without executing the drag:
await source.dragTo(target, { trial: true });
await source.dragTo(target, { steps: 8 });
This can be useful in diagnosis or when a workflow explicitly separates readiness from action. It is not normally necessary because the real action already waits.
force: true bypasses actionability checks. Use it rarely and only after proving the application accepts a real gesture at that point. Force can hide overlays, disabled state, or a wrong target. It should not be the first response to a timeout.
The noWaitAfter option is deprecated and has no effect. Do not add it to new examples.
5. Use page.dragAndDrop When Selector Strings Are Required
page.dragAndDrop is a valid current API that accepts a source selector, target selector, and similar coordinate and movement options. Locator.dragTo is generally clearer in new test code, but page.dragAndDrop can fit a selector-driven helper or a gradual migration.
import { test, expect } from '@playwright/test';
test('reorders a simple list with page.dragAndDrop', async ({ page }) => {
await page.goto('/priority-list');
await page.dragAndDrop(
'[data-testid="priority-low"]',
'[data-testid="priority-high"]',
{
strict: true,
steps: 8,
targetPosition: { x: 20, y: 10 },
},
);
await expect(page.getByTestId('priority-list').getByRole('listitem'))
.toHaveText(['Low', 'High', 'Medium']);
});
strict: true requires each selector to resolve to one element rather than quietly using the first source or target match. That makes selector-based dragging safer.
Do not mix selector engines into long opaque strings when locators can express intent. A locator version makes relationships easier to maintain:
const list = page.getByTestId('priority-list');
const low = list.getByRole('listitem', { name: 'Low' });
const high = list.getByRole('listitem', { name: 'High' });
await low.dragTo(high, { steps: 8 });
Choose one style per helper layer. Wrapping locator inputs by converting them back to selectors is unnecessary and loses locator benefits.
6. Fall Back to Precise Mouse Control for Custom Widgets
Canvas editors, spatial timelines, and unusual drag libraries sometimes need an exact pointer path. page.mouse operates in main-frame CSS pixels relative to the viewport. Get fresh bounding boxes immediately before the action, calculate points, then send move, down, movement, and up.
import { test, expect } from '@playwright/test';
test('moves a timeline marker by 120 pixels', async ({ page }) => {
await page.goto('/timeline');
const marker = page.getByTestId('marker-release');
const track = page.getByTestId('timeline-track');
await marker.scrollIntoViewIfNeeded();
await expect(marker).toBeVisible();
await expect(track).toBeVisible();
const markerBox = await marker.boundingBox();
const trackBox = await track.boundingBox();
if (!markerBox || !trackBox) {
throw new Error('Marker or track has no bounding box');
}
const startX = markerBox.x + markerBox.width / 2;
const startY = markerBox.y + markerBox.height / 2;
const endX = Math.min(startX + 120, trackBox.x + trackBox.width - 5);
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.mouse.move(endX, startY, { steps: 12 });
await page.mouse.up();
await expect(page.getByTestId('marker-value')).toHaveText('12:00');
});
Use a try/finally around mouse.up if intermediate assertions or helper code can throw while the button is held. Leaving the mouse down can corrupt later steps.
For HTML drag implementations that depend on dragover in all browsers, Playwright's input guidance recommends at least two moves over the target. With the low-level sequence, hover the target twice before releasing, or use mouse.move with multiple steps.
Coordinates are sensitive to layout, scrolling, zoom, and responsive breakpoints. Set a deliberate viewport and keep calculation relative to live bounding boxes. Manual mouse code should be a documented fallback, not a default abstraction.
7. Drop Files or MIME Data With Locator.drop
Locator.drop, available in current Playwright releases, simulates external file or clipboard-style data being dropped onto a target. It constructs a DataTransfer in the page context and dispatches dragenter, dragover, and drop. It is ideal for a drop zone that accepts files from the desktop.
Use an in-memory file to keep the fixture self-contained:
import { test, expect } from '@playwright/test';
test('drops a text file on the upload zone', async ({ page }) => {
await page.goto('/imports');
const zone = page.getByTestId('file-drop-zone');
await zone.drop({
files: {
name: 'customers.csv',
mimeType: 'text/csv',
buffer: Buffer.from(
'email,name\nasha@example.test,Asha Patel\n',
'utf8',
),
},
});
await expect(zone.getByText('customers.csv')).toBeVisible();
await expect(page.getByRole('status'))
.toHaveText('1 valid customer ready to import');
});
You can pass a file path, multiple paths, one in-memory file, or an array of file payloads. For clipboard-style drops, provide MIME keys:
await page.getByTestId('link-drop-zone').drop({
data: {
'text/plain': 'Playwright documentation',
'text/uri-list': 'https://playwright.dev/',
},
});
await expect(page.getByRole('link', { name: 'Playwright documentation' }))
.toHaveAttribute('href', 'https://playwright.dev/');
If the target's dragover handler does not call preventDefault(), the target rejects the drop. Playwright dispatches dragleave and drop() throws. That behavior is useful evidence that the zone does not implement a valid drop contract.
Do not use drop() to reorder two existing cards. It supplies external DataTransfer data and has no visible source locator. Use dragTo for element movement.
8. Use DataTransfer Dispatch Only for a Custom HTML5 Contract
A legacy or highly customized HTML5 widget may require data set during dragstart. If dragTo cannot reproduce it and Locator.drop does not match because there is a visible source, dispatch events with one shared DataTransfer handle. This is event simulation, not a trusted user gesture, so treat it as a narrow fallback.
import { test, expect } from '@playwright/test';
test('moves a legacy asset with a custom data type', async ({ page }) => {
await page.goto('/legacy-assets');
const source = page.getByTestId('asset-A-42');
const target = page.getByTestId('folder-archive');
const dataTransfer = await page.evaluateHandle(() => {
const transfer = new DataTransfer();
transfer.setData('application/x-asset-id', 'A-42');
return transfer;
});
await source.dispatchEvent('dragstart', { dataTransfer });
await target.dispatchEvent('dragenter', { dataTransfer });
await target.dispatchEvent('dragover', { dataTransfer });
await target.dispatchEvent('drop', { dataTransfer });
await source.dispatchEvent('dragend', { dataTransfer });
await dataTransfer.dispose();
await expect(target.getByTestId('asset-A-42')).toBeVisible();
});
Keep the same handle across events so the payload survives. Dispose it after use. If an assertion can fail before disposal, put disposal in finally.
This approach bypasses pointer actionability and may pass while the real UI is unusable. Maintain at least one real dragTo or manual mouse test for the user path when possible. Use event dispatch mainly to cover data-contract branches that cannot be reached reliably through public gestures.
A better product design may expose keyboard movement or explicit Move controls. Those paths improve accessibility and provide stable alternatives, but they do not automatically replace a critical pointer-drag test.
9. Assert State, Persistence, and Accessibility After the Drop
The drag action returning successfully only means Playwright completed input. It does not prove that the application accepted the drop. Assertions should cover the requirement at multiple useful levels.
| Risk | Example assertion |
|---|---|
| Visual placement | Target contains the item |
| No duplication | Source no longer contains it |
| Ordering | List items have expected text order |
| Feedback | Status or toast describes the move |
| Persistence | Reload retains the new position |
| Network contract | Matching response succeeds |
| Accessibility | Keyboard alternative produces same result |
A persistence test can wait for the specific response and reload:
const update = page.waitForResponse(response =>
response.url().endsWith('/api/tasks/TASK-42') &&
response.request().method() === 'PATCH',
);
await card.dragTo(done, { steps: 8 });
expect((await update).ok()).toBe(true);
await page.reload();
await expect(done.getByTestId('card-TASK-42')).toBeVisible();
Do not wait for a generic network idle state. Match the operation or use a user-visible saved indicator. Then reload only when persistence belongs to the acceptance criteria, because reloading every drag test increases duration.
A drag-only interface excludes keyboard and assistive technology users. If the component offers a keyboard pattern or Move menu, test it:
await card.getByRole('button', { name: 'Move task' }).click();
await page.getByRole('option', { name: 'Done' }).click();
await expect(done.getByTestId('card-TASK-42')).toBeVisible();
Pointer and keyboard tests should converge on the same domain outcome.
10. Debug Playwright Drag and Drop Failures
Start by observing the source, target, and event contract. Confirm the source locator targets the actual handle and the target is the element whose dragover listener accepts the drop. Use Playwright Inspector and Trace Viewer to see action points.
npx playwright test tests/drag-and-drop.spec.ts --debug
npx playwright test tests/drag-and-drop.spec.ts --trace=on
npx playwright show-trace test-results/path-to-trace/trace.zip
When dragTo completes but nothing moves, try steps before coordinates or force. A component may need intermediate movement. When it drops in the wrong place, specify a targetPosition based on the component's insertion zones.
Instrument DOM events temporarily in a test environment:
await page.evaluate(() => {
const types = ['dragstart', 'dragenter', 'dragover', 'drop', 'dragend'];
for (const type of types) {
document.addEventListener(type, event => {
console.log('DND_EVENT', type, (event.target as HTMLElement).dataset.testid);
}, true);
}
});
Trace console output to learn which events occur. Remove instrumentation from committed tests unless it provides durable diagnostic value.
If behavior differs across browsers, do not immediately skip the project. Compare the component library's browser support, dragover handling, pointer-event overlays, and animation. Keep steps and exact positions consistent. File drops with Locator.drop are designed to construct DataTransfer cross-browser.
If a strict-mode error or multiple match prevents the action, fix the locator instead of selecting first without understanding why. Fixing Playwright strict mode violations provides a systematic approach.
11. Design a Maintainable Drag Helper
A helper should express the domain action and keep options available, rather than hiding every drag behind raw selectors.
// tests/pages/KanbanPage.ts
import { expect, type Locator, type Page } from '@playwright/test';
export class KanbanPage {
constructor(private readonly page: Page) {}
private card(id: string): Locator {
return this.page.getByTestId('card-' + id);
}
private column(status: string): Locator {
return this.page.getByTestId('column-' + status);
}
async moveCard(
id: string,
from: string,
to: string,
): Promise<void> {
const sourceColumn = this.column(from);
const targetColumn = this.column(to);
const card = sourceColumn.getByTestId('card-' + id);
await expect(card).toBeVisible();
await card.dragTo(targetColumn, { steps: 8 });
await expect(targetColumn.getByTestId('card-' + id)).toBeVisible();
await expect(sourceColumn.getByTestId('card-' + id)).toHaveCount(0);
}
}
The helper validates immediate movement. A test can add persistence, permissions, or audit assertions based on the scenario. Avoid a generic drag(sourceSelector, targetSelector) wrapper that merely renames Playwright and hides useful locator reporting.
Keep coordinate-specific helpers close to the custom widget they support. A universal mouse-drag utility tends to accumulate flags and loses the reason for each movement.
Document whether the helper expects status slugs, visible names, or IDs. Typed unions can restrict known states when the domain is stable. Good test architecture makes invalid movement calls harder to write while preserving Playwright's trace-friendly actions.
Interview Questions and Answers
Q: What is the preferred Playwright API for dragging one element to another?
Locator.dragTo is the preferred starting point. It uses locator actionability, resolves source and target, and supports positions, steps, trial, force, and timeout. The test should then assert the application's new state.
Q: What does the steps option change?
It sends a specified number of interpolated mousemove events between the source and destination. Some drag libraries need intermediate movement or dragover activity to activate a target. Use a modest value when the default movement does not match the component.
Q: When should you use page.mouse instead of dragTo?
Use page.mouse for canvas, spatial controls, exact paths, or widgets whose behavior cannot be represented by source and target locators. Calculate coordinates from current bounding boxes and send move, down, stepped move, and up. It is more layout-sensitive, so keep it a documented fallback.
Q: What is Locator.drop used for?
It simulates external files or MIME-typed data being dropped onto a target. It creates DataTransfer in the page context and dispatches dragenter, dragover, and drop. It is not a replacement for dragging a visible card to another column.
Q: How do you verify that a drag succeeded?
Assert the item appears in the target, disappears from the source, and has the expected order or status. For persisted moves, wait for the specific save signal and reload to confirm. Input completion alone is not a business assertion.
Q: Why is force usually a poor first fix?
force bypasses actionability checks that can expose overlays, hidden elements, disabled state, or incorrect targeting. It may make a synthetic action pass while a user cannot perform it. Diagnose locator and layout problems first.
Q: How do you handle a custom HTML5 DataTransfer payload?
Create one DataTransfer with page.evaluateHandle, set the required MIME data, and pass the same handle through dragstart, dragenter, dragover, drop, and dragend dispatches. Dispose it afterward. Because this bypasses real pointer behavior, retain user-gesture coverage where possible.
Q: What cross-browser issue is common in manual drag sequences?
Some implementations require more than one movement over the target to dispatch the expected dragover behavior across browsers. Use stepped movement or hover the target twice before mouseup. Also verify the component itself supports all configured browsers.
Common Mistakes
- Dragging the card container when only its handle listens for the gesture.
- Using Locator.drop for an element-to-element reorder. It models external files or MIME data.
- Treating a completed input action as proof of a successful state change.
- Adding force before investigating overlays, actionability, and locator identity.
- Using unexplained absolute coordinates that break with layout or viewport changes.
- Reading bounding boxes long before the action, then using stale geometry.
- Sending only one manual move when a widget requires dragover activity.
- Using dispatchEvent as the only coverage for a user gesture.
- Forgetting to dispose a DataTransfer JSHandle.
- Relying on page.dragAndDrop without strict selectors when duplicates exist.
- Testing only Chromium even though the product claims Firefox and WebKit support.
- Adding fixed delays for animation instead of asserting an observable ready or saved state.
- Verifying arrival but not departure, which can miss duplicate cards.
- Skipping the accessible keyboard or explicit Move path.
Conclusion
Use Locator.dragTo for normal Playwright drag and drop, then add steps or meaningful positions only when the component contract requires them. Move to page.mouse for coordinate-driven widgets, Locator.drop for external file or MIME payloads, and DataTransfer event dispatch only for specialized legacy behavior.
A maintainable test starts with unique locators and deterministic data, performs the closest available user interaction, and asserts movement, non-duplication, feedback, and persistence according to risk. Run critical flows across the browsers you support, and use traces plus event instrumentation to diagnose the component rather than hiding failures with force or sleeps.
Interview Questions and Answers
Which Playwright API would you choose for a basic element drag?
I would start with Locator.dragTo because it combines locator resolution, actionability checks, and useful options. I would locate the actual drag handle and target, perform the action, and assert both arrival and departure.
How is Locator.drop different from Locator.dragTo?
dragTo models moving one visible page element to another with mouse input. drop models an external payload such as files, text, or a URL entering a target through DataTransfer. They test different product contracts.
When do the dragTo steps matter?
Some libraries activate a drop target only after intermediate mousemove or dragover behavior. steps creates an interpolated path rather than one destination move. I add it based on observed component behavior, not as a universal delay.
How would you test a canvas drag?
I get fresh bounding boxes, calculate points relative to the live viewport, and send a precise page.mouse sequence with stepped movement. I set a stable viewport and always assert a resulting value, object position, or persisted model state.
Why should force not be the default solution for failed drag actions?
It bypasses actionability signals that often reveal a real overlay, disabled state, or wrong locator. A forced test can pass even when users cannot drag. I diagnose the component and use force only when the exact nonstandard behavior is intentional.
How would you validate successful reordering?
I assert the full item order or the moved item's target index, verify it no longer occupies the old position, and check any save feedback. If persistence matters, I wait for the save operation, reload, and assert the order again.
When is DataTransfer event dispatch justified?
It is justified for a custom HTML5 payload contract that normal dragTo cannot populate and external drop does not represent. I reuse one DataTransfer handle across events and dispose it. I treat it as contract-level coverage, not a substitute for all user-gesture testing.
How do you debug browser-specific drag failures?
I compare source and target geometry, emitted events, overlays, and component-library support in each browser. I use Trace Viewer and temporary event logging, and test stepped movement. I avoid hiding the difference with a browser skip until the support decision is explicit.
Frequently Asked Questions
How do I drag and drop an element in Playwright?
Create locators for the draggable source and target, then call await source.dragTo(target). Assert that the item moved into the target and left its original location.
What does steps do in Playwright dragTo?
steps sends multiple interpolated mousemove events along the path. It helps widgets that need intermediate movement or dragover events rather than one move directly to the destination.
When should I use sourcePosition and targetPosition?
Use them when only part of the source is draggable or the target has insertion zones. Coordinates are relative to each element's padding box, so derive them from meaningful component geometry.
How do I drag on a canvas with Playwright?
Read current bounding boxes, calculate viewport-relative points, and use page.mouse.move, down, stepped move, and up. Assert a visible or model-backed result because coordinates alone do not prove the operation succeeded.
How do I drop a file into a Playwright drop zone?
Use target.drop({ files }) with a file path or an in-memory object containing name, mimeType, and buffer. This simulates an external DataTransfer and dispatches the relevant drop events.
What is the difference between dragTo and drop in Playwright?
dragTo moves a visible source locator to a target through mouse input. drop supplies external files or MIME data directly to a target and has no visible source element.
Why does Playwright dragTo run but the item does not move?
The source may not be the actual handle, the target may need intermediate movement or a specific region, or the component may reject dragover. Try correct locators, steps, meaningful positions, and event diagnostics before force.
Can Playwright test accessible alternatives to drag and drop?
Yes. Test the component's keyboard reordering pattern or explicit Move menu with normal locator actions. Both pointer and accessible paths should reach the same domain outcome.