<br />

Web features: [Pointer Events](https://webstatus.dev/features/pointer-events-api) [touch-action](https://webstatus.dev/features/touch-action) [user-select](https://webstatus.dev/features/user-select)

> [!NOTE]
> **Note:** These guides are authored for AI coding agents (apologies for the robotic tone!). To have your coding agent adopt these patterns automatically, check out [Modern Web Guidance](https://github.com/GoogleChrome/modern-web-guidance).

Add dragging to an existing floating element such as a panel, palette, toolbar, inspector, or dialog. Dragging does not determine how the element is displayed, dismissed, stacked, or focused.

## Use a dedicated handle

Start dragging from a dedicated handle when the element contains selectable text, buttons, inputs, or links. This keeps its normal contents usable. Make the handle focusable for keyboard movement and give it an accessible name that mentions the arrow-key interaction.

Do not apply `role="application"` to the handle. It unnecessarily changes assistive-technology interaction.

Apply `cursor: grab`/`grabbing`, `touch-action: none`, and both `user-select: none` and `-webkit-user-select: none` to the handle---not the entire draggable element---so text outside it remains selectable and touch gestures retain their usual behavior:

```css
.drag-handle {
  cursor: grab;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}

.drag-handle.dragging {
  cursor: grabbing;
}
```

## Preserve pointer position and reachability

Use a viewport-relative positioning model such as `position: fixed` when clamping movement to the viewport.

- Start a drag only for the primary pointer button (`event.button === 0`).
- Record the pointer offset on `pointerdown` and use it on every move so the element does not jump when grabbed away from its corner.
- Keep the drag active after the pointer leaves the handle. Default to `setPointerCapture()` and clear the dragging state on `lostpointercapture`. If the handle may be removed or re-rendered mid-drag, use document-level `pointermove`, `pointerup`, and `pointercancel` listeners instead, since capture is lost in that case.
- Optionally, wait for roughly 4px of movement before entering the dragging state, so a click on the handle does not trigger drag styling.

With pointer capture, the movement listeners can remain on the handle:

```javascript
handle.addEventListener('pointerdown', (event) => {
  if (event.button !== 0 || event.target.closest('button, input, select, textarea, a')) return;

  const rect = draggable.getBoundingClientRect();
  offsetX = event.clientX - rect.left;
  offsetY = event.clientY - rect.top;
  handle.setPointerCapture(event.pointerId);
});

handle.addEventListener('pointermove', (event) => {
  if (!handle.hasPointerCapture(event.pointerId)) return;
  handle.classList.add('dragging');
  moveTo(event.clientX - offsetX, event.clientY - offsetY);
});

handle.addEventListener('lostpointercapture', () => {
  handle.classList.remove('dragging');
});

function moveTo(left, top) {
  const maxLeft = Math.max(0, window.innerWidth - draggable.offsetWidth);
  const maxTop = Math.max(0, window.innerHeight - draggable.offsetHeight);

  draggable.style.left = `${Math.max(0, Math.min(left, maxLeft))}px`;
  draggable.style.top = `${Math.max(0, Math.min(top, maxTop))}px`;
}
```

Clamp every pointer and keyboard position through the same helper so the element stays reachable. Re-clamp its current position after viewport or element-size changes. For layout-heavy elements, consider translating during the drag to avoid per-frame layout, then commit the final position to `left` and `top` on release.

## Provide keyboard movement

When the handle has focus, move the element with the arrow keys and call `preventDefault()` for those keys. Use the same clamped positioning helper as pointer movement. A 15px increment is a reasonable starting point; adapt it to the component's size and precision needs.

## Dialog-specific note

A `<dialog>` or `[popover]` can use this mechanism with its header as the handle. Set `margin: 0` before assigning `left` and `top`, because the browser's default margins interfere with manual positioning. For dialog and popover opening, closing, focus, and dismissal behavior, see [Declarative Dialog and Popover Control](https://developer.chrome.com/docs/modern-web-guidance/guides/ui-behaviors/declarative-dialog-popover-control.md.txt) and [Platform Controls Dismiss Dialog](https://developer.chrome.com/docs/modern-web-guidance/guides/ui-behaviors/platform-controls-dismiss-dialog.md.txt).

## Fallback strategies

When dragging only repositions a component, treat it as progressive enhancement: without JavaScript, keep the element, its content, and its controls visible, readable, and functional in a sensible default position. Do not expose a non-functional drag handle: if the handle is a standalone grip control, hide it by default and reveal it when JavaScript initializes; if a header doubles as the handle, attach its drag affordances when JavaScript initializes.

<iframe src="https:///frame/docs/modern-web-guidance/guides/ui-behaviors/drag_8a9ffda97bebe1b2c99b3d2f757f849189924eb309b6819d5165fe23d647b8f3.frame" class="framebox inherit-locale " allow="clipboard-write https://" allowfullscreen is-upgraded></iframe>

Include `-webkit-user-select: none` before `user-select: none` so text selection stays suppressed during dragging in Safari.
Source: [drag guide on GitHub](https://github.com/GoogleChrome/modern-web-guidance-src/blob/main/guides/ui-behaviors/drag/guide.md)