---
title: "ScrubField"
description: "A compact numeric field whose label is the drag handle."
category: Form
status: beta
since: 0.3.9
tags: [form, number, drag, inspector]
syncStatus: reviewed
verified: true
---

## Usage

### Basic

:::TuffDemoWrapper{demo="ScrubFieldScrubFieldDemo" code-lang="vue" description="An integer, a percentage, and a half step."}
---
code: |
  <script setup lang="ts">
  import { ref } from 'vue'

  const width = ref(324)
  const opacity = ref(100)
  const gap = ref(1.5)
  </script>

  <template>
    <TxScrubField v-model="width" label="W" :min="40" :max="999" :active="width !== 324" />
    <TxScrubField v-model="opacity" label="Opacity" :min="0" :max="100" suffix="%" />
    <TxScrubField v-model="gap" label="Gap" :min="0" :max="8" :step="0.5" />
  </template>
---
:::

### Best Practices

- Keep labels short (`W`, `H`, `Radius`); the label is the drag handle.
- Let the host decide `active`, usually "value differs from the preset", compared against data rather than a constant.
- Use `clampOn="blur"` in forms people type into; keep the default `'input'` in inspectors with a live preview.
- Use `TxSlider` to pick a value by position on a track, and `TxNumberInput` for a form field with stepper buttons.

## API Reference

### Props

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `modelValue` | `number` | — | The current value (controlled). |
| `label` | `string` | — | Short label that doubles as the drag handle. |
| `min` / `max` | `number` | — | Bounds; dragging, arrow keys, and typing all clamp to them. |
| `step` | `number` | `1` | Step size; a fractional step keeps its decimals. |
| `suffix` | `string` | — | Unit after the value, such as `%`. |
| `active` | `boolean` | `false` | Tints the field as changed; the host decides what counts as changed. |
| `disabled` | `boolean` | `false` | Disables dragging, arrow keys, and typing. |
| `pixelsPerStep` | `number` | `2` | Pointer travel that advances one step. |
| `shiftMultiplier` | `number` | `10` | Arrow-key multiplier while Shift is held. |
| `clampOn` | `'input' \| 'blur'` | `'input'` | When typed text is clamped: on every keystroke or on blur. |
| `ariaLabel` | `string` | — | Accessible name for the handle; falls back to `label`. |
| `valueLabel` | `string` | `'{label} value'` | Accessible name for the input. |

### Events

| Event | Payload | Description |
|------|------|-------------|
| `update:modelValue` | `(value: number)` | Fires when the value changes; never repeats an identical value. |
| `change` | `(value: number)` | Fires together with `update:modelValue`. |
| `scrubStart` / `scrubEnd` | — | Start and end of a drag, cancellation included. |

### Exposed Methods

| Method | Description |
|------|-------------|
| `focus()` | Focuses the drag handle. |
| `focusInput()` | Focuses the number input. |

## Overview

- Drag: move sideways on the label; every `pixelsPerStep` pixels advance one `step`, measured from the gesture's start, with no track. Escape during a drag restores the starting value.
- Keyboard: ↑ → increase, ↓ ← decrease, Shift multiplies by `shiftMultiplier`, and Home / End jump to the bounds.
- Typing: the value is a native `<input>`; `clampOn="blur"` keeps the typed text until blur. Escape reverts the text.
- The handle is a `role="slider"` with `aria-valuenow` / `aria-valuemin` / `aria-valuemax` / `aria-orientation`, plus `aria-valuetext` when `suffix` is set; the input is a second control with its own accessible name.
- The handle sets `touch-action: pan-y`, leaving vertical gestures to page scroll; `pointercancel` and `lostpointercapture` both end a drag.
- Under reduced motion, the only state transition is switched off.

## Technologies

- Travel is quantised, not the result: `round(distance / pixelsPerStep) * step`, so fractional steps work and an off-grid starting value is not snapped.
- Source: `packages/tuffex/packages/components/src/scrub-field/`.
- Adapted from Beautiful UI (https://www.beautifului.dev), © 2026 Shane Levine, MIT.

<TuffDocSourceLink />
