A drag handle is the mark that shows something can be dragged: the grabber on a bottom sheet, the bar in the gutter between two panes, the grip on a row that can be reordered.
The element you write sets the semantics. A
<span class="drag-handle" aria-hidden="true"> is
decoration, which is the usual case. A
<button class="drag-handle"> with a name is a
control, and you write that one only when something is wired to it.
The rest of the markup is the same.
<span class="drag-handle" aria-hidden="true"></span>
Dragging is not included
This component is a bar. On its own it does not start a drag, move
anything, or reorder anything. These layouts wire it. A handle written
as a <button> inside a
bottom sheet dismisses that
sheet. One between the two panes of a
pane layout resizes them. A
decorative one in a sortable list
row drags the row. Everywhere else the bar is inert.
Provide keyboard operation and a single-pointer alternative without dragging. Clickable "Move up" / "Move down" controls in a menu, or a position input, can cover both paths. Arrow keys alone cover only keyboard operation. WCAG 2.2 SC 2.5.7 requires the pointer alternative unless dragging is essential or the unmodified user agent owns the functionality. Test each path.
A sortable list wires the handle for reordering and adds Move up and Move down buttons as the other two paths. Anywhere else, you supply those paths yourself. The handle only shows people who can drag that the gesture exists.
Decorative or control
A handle draws a bar and contains no text, so it reports nothing to a
screen reader. If you leave it exposed, a screen reader reaches it in
the reading order as an unlabelled blank. Hide it with
aria-hidden:
<span class="drag-handle" aria-hidden="true"></span>
Make it a <button> only when activating it does
something. The button then needs a name, because it has no text
inside to take one from. The interactive states (hover, focus ring,
and the swell to 12×52dp while it is held) apply only to the
button version, so a decorative handle does not react when a pointer
crosses it.
<button type="button" class="drag-handle" aria-label="Resize the editor"></button>
Do not write a hidden button. aria-hidden
takes an element out of the accessibility tree without taking it out
of the tab order. Focus then lands on something that assistive
technology treats as missing.
Between two panes
Put the handle between the columns of a two-column layout. You provide the layout, either a flex or grid row of your own or a pane layout. The handle sets its own width and needs no wrapper class. This is the shape Material tokenised, a vertical bar in the gutter, 4×48dp inside a 24dp hit target.
<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
<button class="drag-handle"> placed directly
between the panes of a list-detail or supporting
pane layout becomes a working splitter
with cursor: col-resize. The panes page describes it.
In a layout of your own, you build the splitter yourself.
On a bottom sheet
A bottom sheet 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. It
then takes the sheet's shape instead of the vertical one, because
Material gives the sheet's grabber its own size and colour
(32×4dp at 40% on-surface-variant). The same class
works in both places.
<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. In a sheet, the handle button dismisses it.
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>
Tokens
Set these on the handle itself, or in a rule of your own that picks out the handles you want to change. The component declares them on the handle, so a value set on an ancestor element does not inherit. The handle's own default shadows it.
| 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
sets the colour instead.