Drag handles, sliders and the affordance problem
The drag handle is the most optimistically designed element on the web. A designer draws a slim divider between two panels, adds three tasteful dots in the middle, and imagines users happily resizing their workspace like pilots trimming a cockpit. What actually happens, in usability test after usability test, is this: nobody drags it. A few users drag it by accident, can't undo it, and learn to fear it. Keyboard users never discover it exists. And on the next visit, the carefully chosen layout has reset to default anyway.
This is the affordance problem. Draggable surfaces — split panes, before/after comparison sliders, range inputs, resizable sidebars — promise direct manipulation, the most satisfying interaction style we have. But a promise users can't see, can't reach with a keyboard, or can't trust to persist is worse than no promise at all. Here's how we build them so they hold up.
Discoverability: if it looks like wallpaper, it's wallpaper
Fitts's law punishes us here: the target that controls a two-hundred-pixel panel is typically a four-pixel line. Users cannot be expected to hunt a pixel seam. Our rules:
- Fat hit area, thin visual. The visible divider can stay a 1–2px hairline, but the interactive zone needs 24px minimum (44px on touch). Implement with an invisible pseudo-element or a transparent wrapper so the hit area overflows the seam visually. This is the single highest-leverage fix in the entire pattern.
- A grip that reads as a grip. Not three faint dots — a handle with shape, contrast and a hover state that visibly grasps: the grip lifts, the cursor commits, the adjacent panels dim half a stop to show what's at stake. This is a microinteraction that earns its keep: it teaches the model of the interface in 160ms.
- Cursor honesty across the whole seam.
cursor: col-resizeshould appear anywhere on the seam, not just on the grip. Mismatched cursors (resize cursor over a zone that doesn't drag) destroy trust faster than no drag at all. - Prove it early, once. A one-time nudge — the panel breathing 8px wider on first load, or the comparison handle drifting a few degrees — teaches draggability better than any tooltip. One cycle, then never again. Persistence of the "seen it" flag matters as much as the animation.
For before/after image sliders specifically: lead with the handle off-centre (around 40%), never dead on 50%. A centred handle on a symmetric image looks like a watermark; an offset one looks like a control.
Keyboard operability is not optional garnish
Every draggable widget is, semantically, already a solved problem: it's a slider. A resizable split pane is a slider whose value is the panel width; a before/after comparison is a slider whose value is the reveal position. Use the platform:
role="slider"(or better, a real<input type="range">visually restyled) witharia-valuemin,aria-valuemax,aria-valuenowand a humanaria-label— "Reveal comparison", "Adjust editor width".- Arrow keys for fine steps (1%), Shift+Arrow for coarse (10%), Home/End to snap to bounds. Announce the value in terms users understand — "Editor 65 percent", not raw pixels.
- Visible focus that is unmistakably on the handle, not vaguely near it. A resized panel that silently moves when someone arrows through the page is a genuine accessibility failure. Our focus-visible work covers the styling discipline; the short version is that the focus ring must travel with the value.
- Focus order must follow visual order. If dragging the handle past another control would reorder the layout, the DOM order and the visual arrangement must never quietly disagree.
The reason to take this seriously isn't ideological. Keyboard users are the canary: a handle operable by keyboard is a handle whose state transitions are explicit, bounded and testable. The mouse path inherits the discipline.
Touch: where drag handles go to die
On touch, drags compete with the scroll. A vertical before/after slider on a page that scrolls vertically is a trap: half your "drags" move the page, half move the handle, and the user experiences both as unpredictable. Mitigations, in order of preference:
- Choose orientations that don't fight the scroll. Horizontal handles on vertical pages.
- Use
touch-actionprecisely —touch-action: noneon the grip itself so a gesture that begins on the handle is captured, while gestures beginning on the image still scroll. - Make the outside tappable. Tapping a point on a comparison image should glide the handle there (with a short ease, and full
prefers-reduced-motionrespect). Tap-to-position rescues users who never discover dragging at all — in our testing, that's a lot of users.
Persistence: remember, or don't pretend
A user-set size is an act of self-expression. Destroying it on navigation or reload teaches users that customising your interface is pointless, and they stop. Conversely, persisting forever creates its own bug class: someone drags a panel to near-zero on a laptop in March, returns on a tablet in September, and the app appears broken.
Our policy, which has survived several product cycles: persist panel sizes per surface, per breakpoint class, with escape hatches. Store the ratio, not the pixel value, so it survives window resizing. Reset when the viewport crosses a breakpoint class (a 65/35 desktop split is meaningless at 375px — collapse to stacked or tabbed instead). And provide a discreet "reset layout" in whatever settings surface exists. The double-click-to-reset convention on the handle itself is a nice bonus for those who find it.
Where we shipped a configurator with a draggable preview/properties split for Osprey Outdoor, persistence was the single highest-complaint feature until we got the rules above exactly right — which is to say, expect to get it wrong once, and instrument drag-start events so you can see whether anyone drags on purpose.
Engineering notes worth a section
- Pointer Events, one listener.
pointerdown→setPointerCapture→pointermoveuntilpointerup. Pointer capture makes drags survive the cursor leaving the element, the window, and reality generally. Never bindmousemoveto the size of the universe; bind it to the duration of the gesture. - Don't re-render layout per pixel. Update a CSS custom property (
--split: 62%) during the drag and let layout consume it; commit React state on release. Throttle withrequestAnimationFrameat most. A resize that triggers framework re-render per frame will jank on exactly the low-end hardware where your product needs to feel solid. - Clamp with intent. Panels need minimum sizes derived from content (the smallest size at which the panel is still useful), not arbitrary pixels. A panel that can drag to zero is a panel that can be lost.
- Test the drag, don't trust it. Pointer-event tests for capture and clamping; keyboard tests for arrow-key stepping and announcements; and one visual regression at a non-default split, because "works at the default" is how all these bugs begin. Fold it into your regular accessibility audit process, not a special occasion.
The honest alternative
Finally, the senior move: sometimes don't build the drag at all. If analytics or testing show a split is only ever used at two or three positions, replace the continuous handle with discrete layout presets — buttons or a segmented control labelled with what they do ("Focus editor", "Balanced", "Focus preview"). Presets are discoverable, keyboard-native, persist cleanly, and photograph well in docs. Direct manipulation is delightful when it's found and trusted; a labelled button is dependable whether it's found by a mouse, a keyboard, or a screen reader. Pick dependability when in doubt — this is the same philosophy behind how we scope product engagements: the boring, load-bearing choice, made on purpose.
Key takeaways
- Draggable UI fails by invisibility: thin seams, faint grips, no hover story. Fat hit areas over thin visuals, cursor honesty, and a one-time teaching nudge fix most of it.
- Every drag handle is semantically a slider — give it the role, the keyboard steps, the announcements and a focus ring that travels with the value.
- On touch, don't fight the scroll: orient handles against it, use
touch-actionprecisely, and let taps position the handle directly. - Persist user-set sizes as ratios, per breakpoint class, with a reset. Never persist a layout that makes the app look broken.
- When positions cluster at two or three values, ship labelled presets instead of a continuous drag.
FAQ
What's the minimum touch target for a drag handle? 44×44px per WCAG target-size guidance for anything users must hit on touch. The visible affordance can be much smaller — the hit area is what counts.
Should before/after sliders autoplay a wiggle on load? One gentle cycle, once per user, is an effective teaching device. Looping it, repeating it on every page, or running it under prefers-reduced-motion turns teaching into noise.
How do we announce slider values to screen readers? aria-valuenow keeps assistive tech in sync; add aria-valuetext when raw numbers are meaningless ("Editor 65 percent" beats "420"). Announce on release, not per pixel, to avoid speech floods.
Are native <input type="range"> sliders enough? For value entry, yes — restyle the track and thumb and keep all the platform behaviour. For split panes and comparisons, the widget isn't a form input, so use role="slider" on the dividing element with the full keyboard contract.
Focus-visible: designing the ring, not removing it
Keyboard focus styles are a design problem most teams delete instead of solve. How to design focus rings that are visible, beautiful and part of the identity system.
A taxonomy of micro-interactions (and when to say no)
Hover states, cursor follow, scroll reveals and button physics — a working taxonomy with duration curves, quality tests, and reduced-motion fallbacks for every pattern.
Hover states with purpose
Hover is the most abused state in interface design — decoration where it should teach. A taxonomy of hover states that do a job, plus the focus-parity rules.