Skip to content

ExpressiveCSS

Browse documentation

Drag handle

The bar that marks a thing as draggable. It does not do the dragging.

View as Markdown

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.

Editor
Preview
<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-width24px
--md-comp-drag-handle-width4px
--md-comp-drag-handle-height48px
--md-comp-drag-handle-shape9999px
--md-comp-drag-handle-colorvar(--md-sys-color-outline)
--md-comp-drag-handle-pressed-width12px
--md-comp-drag-handle-pressed-height52px
--md-comp-drag-handle-pressed-shape12px
--md-comp-drag-handle-pressed-colorvar(--md-sys-color-on-surface)
--md-comp-drag-handle-state-layer-colorvar(--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.

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow