# Drag handle

> The bar that says a thing can be dragged — and nothing that does the dragging.

## Anatomy

A drag handle is the affordance that makes something legible as draggable: the grabber on a [bottom sheet](/bottom-sheet.html.md), the bar in the gutter between two [panes](/panes.html.md), the grip on a row that can be reordered.

Which element you write *is* the semantic. A `<span class="drag-handle" aria-hidden="true">` is decoration — the usual case. A `<button class="drag-handle">` with a name is a control, and you write that one only when something is genuinely wired to it. Nothing else about the markup changes.

Editor

Preview

```
<span class="drag-handle" aria-hidden="true"></span>
```

## Dragging is not included

This component is a bar. Nothing in it starts a drag, moves anything, or reorders anything. The one exception is not the handle's doing: a handle written as a `<button>` inside a [bottom sheet](/bottom-sheet.html.md) dismisses that sheet, because the sheet's own behaviour claims it. Everywhere else the bar is inert, and that is worth being blunt about.

Provide keyboard operation and a single-pointer alternative without dragging. Clickable "Move up" / "Move down" controls in a [menu](/menu.html.md), or a position input, can serve both paths. Arrow keys alone cover only keyboard operation. [WCAG 2.2 SC 2.5.7](https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html) requires the pointer alternative unless dragging is essential or the unmodified user agent owns the functionality. Test each path.

ExpressiveCSS ships no reordering behaviour, so there is nothing here that supplies that path for you. The handle makes the gesture discoverable to people who can use it; the rest is yours.

## Decorative or control

A handle draws a bar and contains no text, so it reports nothing to a screen reader. Left exposed it arrives in the reading order as an unlabelled blank. Hide it:

```
<span class="drag-handle" aria-hidden="true"></span>
```

Make it a `<button>` only when activating it does something — and then it needs a name, because there is no text inside it to be named by. The interactive states (hover, focus ring, and the swell to 12×52dp while it is held) are scoped to the button spelling for exactly this reason: a decorative handle must not light up under a pointer that merely crosses it.

```
<button type="button" class="drag-handle" aria-label="Resize the editor"></button>
```

What you must not write is a hidden button. `aria-hidden` takes an element out of the accessibility tree without taking it out of the tab order, so focus lands on something that, as far as assistive technology is concerned, is not there.

## Between two panes

This is the shape Material tokenised: a vertical bar in the gutter of a two-column layout, 4×48dp inside a 24dp hit target. The layout is yours — a flex or grid row of your own, or a [pane layout](/panes.html.md). The handle is whatever sits between the columns; it brings its own width and needs no wrapper class.

```
<section aria-label="Editor">…</section>
<span class="drag-handle" aria-hidden="true"></span>
<section aria-label="Preview">…</section>
```

The handle is `cursor: grab` by default. A splitter that resizes rather than moves reads better as `cursor: col-resize` — one declaration, and the component does not guess which of the two you meant.

## On a bottom sheet

A [bottom sheet](/bottom-sheet.html.md) paints its grabber with `::before` and needs no markup at all. Write a handle yourself only when you want a real element in that slot — and it takes the sheet's shape rather than the vertical one, because Material gives the sheet's grabber its own size and colour (32×4dp at 40% `on-surface-variant`). One class, whichever host it lands in.

```
<dialog class="bottom-sheet" aria-labelledby="sheet-title">
  <span class="drag-handle" aria-hidden="true"></span>
  <h2 id="sheet-title">Share</h2>
  …
  <form method="dialog"><button type="submit">Close</button></form>
</dialog>
```

A decorative handle needs a separate non-drag dismiss path, such as the close button above. Native Escape behavior depends on the opening mode, browser, and `closedby` policy; Escape is not a pointer alternative. Scrim dismissal also depends on the installed shared runtime and `closedby` policy. Test it before relying on it.

Write a `<button>` there instead when you want a visible dismiss control. It is the one place a drag handle is wired to anything: the sheet closes on click, tap, Enter, or Space. A drag that snaps back does not also dismiss because the runtime distinguishes the ending click from a tap.

```
<dialog class="bottom-sheet" aria-labelledby="share-title">
  <button type="button" class="drag-handle" aria-label="Dismiss"></button>
  <h2 id="share-title">Share</h2>
  …
</dialog>
```

`.handle` is the pre-1.0 spelling of the same slot and still works, decorative or wired.

## Tokens

Set these on the handle itself, or in a rule of your own that picks out the handles you want to change. They are declared on the component, so a value set on an ancestor *element* is shadowed by the handle's own default rather than inherited.

| Token | Default |
| --- | --- |
| `--md-comp-drag-handle-container-width` | `24px` |
| `--md-comp-drag-handle-width` | `4px` |
| `--md-comp-drag-handle-height` | `48px` |
| `--md-comp-drag-handle-shape` | `9999px` |
| `--md-comp-drag-handle-color` | `var(--md-sys-color-outline)` |
| `--md-comp-drag-handle-pressed-width` | `12px` |
| `--md-comp-drag-handle-pressed-height` | `52px` |
| `--md-comp-drag-handle-pressed-shape` | `12px` |
| `--md-comp-drag-handle-pressed-color` | `var(--md-sys-color-on-surface)` |
| `--md-comp-drag-handle-state-layer-color` | `var(--md-sys-color-inverse-on-surface)` |

The container is sized to the *pressed* bar rather than the resting one, so holding the handle swells the bar without moving anything beside it. Inside a bottom sheet none of these apply — the sheet's own `--md-comp-bottom-sheet-drag-handle-color` does.
