Power Off Slide
Some actions should cost a gesture. A confirm dialog asks the reader to read a sentence and click a button they have clicked a thousand times, so the reading is skipped and the click is muscle memory. A handle that has to travel across the track is a different kind of cost: it takes a moment, and it cannot be done by reflex.
The track is a fixed 224 px and the handle covers 48 of them, so the thumb travels 168 px - enough that the pointer has to be held and aimed. Almost the whole rail counts as arrival, so the gesture does not fail on the last few pixels, and the thumb springs back if it is released early.
When it lands, the track is replaced by a centered "Shutting down…" message.
The track then returns after duration, so the control is usable again without
a remount.
Usage
Bind power-off and let the surrounding page decide what shutting down means.
The control owns the gesture and the announcement, not the side effect.
Drag the handle, or focus it and press Enter.
<template>
<div class="flex w-full flex-col items-center gap-4">
<MPowerOffSlide @power-off="handlePowerOff" />
<p class="text-xs text-muted">
{{ count === 0
? 'Drag the handle, or focus it and press Enter.'
: `Powered off ${count} ${count === 1 ? 'time' : 'times'}.` }}
</p>
</div>
</template>
<script setup lang="ts">
const count = ref(0)
function handlePowerOff() {
count.value += 1
}
</script>
Customize
Customize
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | 'Slide to power off' | Printed on the track, and used as the handle's accessible name. Keep it an instruction rather than a state - the handle is a verb. |
duration | number | 2000 | How long the shutting-down state is held before the handle returns, in milliseconds. Long enough to read the label, short enough that the control does not feel stuck. |
disabled | boolean | false | Disables the button and the drag. The track keeps its shape instead of disappearing, so a layout that depends on it does not reflow. |
class | string | '' | Merged onto the wrapper. |
Events
| Event | Payload | Description |
|---|---|---|
powerOff | [] | The gesture completed - by drag past the threshold, by click, or by Enter or Space. Emitted once per completion; the handle ignores further input until it resets. |
UI
This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.powerOffSlide in app.config.ts.
| Prop | Type | Default | Description |
|---|---|---|---|
ui | object | - | Per-slot classes merged over the theme defaults. |
| Slot | Props | Description |
|---|---|---|
root | - | The centring wrapper. |
track | - | The fixed 224 × 56 rail. It is also what the handle is allowed to travel inside. |
label | - | The resting instruction on the track. pointer-events-none keeps it from
swallowing the start of a drag, and the pl-8 offset keeps it clear of the
resting handle. It carries the sheen that reads as power draining. |
shutdown | - | The centered message that replaces the track once the gesture completes. It keeps the track's 224 × 56 box, so the surrounding layout does not jump. |
handle | - | The thumb. touch-none and cursor-grab belong here rather than on the
wrapper, so a drag anywhere else does not fight the page scroll. |
icon | - | The power glyph, text-error by default. |
Accessibility
- The handle is a real
<button type="button">, so it is in the tab order and announced as a button. Itsaria-labelis the track label rather than the icon. - Enter and Space complete the action, so the destructive path is never drag-only. A gesture that can only be performed by dragging would fail anyone navigating with a keyboard.
- Clicking the handle completes it as well. The drag is the expressive path, not the only one.
- While the track is shut down, the handle leaves the tab order with the track;
the control returns after
durationrather than accepting a second gesture. - A visually hidden
role="status"announces "Shutting down…" when it starts. The visible message is not a live region, so it is heard once rather than twice. disableddisables the button itself, not just the styling, so it is skipped by keyboard navigation and by assistive technology.- Under
prefers-reduced-motion: reducedragging is turned off entirely and the thumb jumps to the end; the track still works by click or by key. The sheen stops instead of looping.
Performance
- The drag has no momentum and no elasticity, so there is no inertial simulation to settle and no overshoot past the end of the track.
- Arrival and reset are two calls on the same motion value, and the reset timer is cleared before the arrival animation starts, so there are never two animations writing to one value.
- Both timers are cleared on unmount. A fast route change cannot leave a pending "powered off" state behind.
- The sheen is a scoped CSS animation rather than a Motion loop, so it runs on the compositor and stops when the rule stops matching.
- Nothing is laid out per frame. The handle moves by
x, the track swaps for the shutdown message once, and the track is a fixed box.