# Scrub field

A number you can type or drag, the way a pro tool’s value field works: an engraved ruler under the numerals slides beneath a fixed index as the value moves.

Kit part · Free · https://tactileui.dev/parts/scrub

- Element: `<tui-scrub> + <input>`
- Needs: `kit.css` and `kit.mjs` ([how to install the kit](https://tactileui.dev/docs/installation#the-kit))
- Categories: Forms, Inputs, Numbers, Sliders & faders
- Finishes: Aluminium, Graphite, Oak, Walnut, Porcelain, Black, Daylight, Night, Paper, Phosphor, E-paper, AMOLED
- Volumes: Tactile, Quiet

## Variants

### Plain · agent temperature

```html
<div style="width:230px;text-align:left"><label class="tui-field"><span class="tui-label">Temperature</span><tui-scrub min="0" max="2" step="0.1" precision="2"><input name="v-temp" value="0.70"></tui-scrub></label></div>
```

### With a unit · kW, ms, tokens

```html
<div style="width:230px;display:grid;gap:12px;text-align:left"><label class="tui-field"><span class="tui-label">Export limit</span><tui-scrub min="0" max="10" step="0.01" unit="kW"><input name="v-kw" value="3.68"></tui-scrub></label><label class="tui-field"><span class="tui-label">Max tokens</span><tui-scrub class="tui-scrub--compact" min="256" max="32000" step="256" unit="tokens"><input name="v-tok" value="4096"></tui-scrub></label></div>
```

### Compact · in a table cell

```html
<div class="tui-table-wrap" style="width:250px"><table class="tui-table"><thead><tr><th>Region</th><th class="is-num">Weight</th></tr></thead><tbody><tr><td>eu-west</td><td style="width:112px;padding:6px 0 6px 8px"><tui-scrub class="tui-scrub--compact" min="0" max="100" step="1" unit="%" label="eu-west weight"><input name="v-w-eu-west" value="40"></tui-scrub></td></tr><tr><td>us-east</td><td style="width:112px;padding:6px 0 6px 8px"><tui-scrub class="tui-scrub--compact" min="0" max="100" step="1" unit="%" label="us-east weight"><input name="v-w-us-east" value="45"></tui-scrub></td></tr><tr><td>ap-south</td><td style="width:112px;padding:6px 0 6px 8px"><tui-scrub class="tui-scrub--compact" min="0" max="100" step="1" unit="%" label="ap-south weight"><input name="v-w-ap-south" value="15"></tui-scrub></td></tr></tbody></table></div>
```

### Inspector row · label on the left

```html
<div style="width:260px;display:grid;gap:8px;text-align:left"><label class="tui-field tui-scrub-row"><span class="tui-label">Timeout</span><tui-scrub class="tui-scrub--compact" min="0" max="5000" step="10" unit="ms"><input name="v-to" value="250"></tui-scrub></label><label class="tui-field tui-scrub-row"><span class="tui-label">Retries</span><tui-scrub class="tui-scrub--compact" min="0" max="10" step="1"><input name="v-re" value="3"></tui-scrub></label><label class="tui-field tui-scrub-row"><span class="tui-label">Backoff</span><tui-scrub class="tui-scrub--compact" min="1" max="4" step="0.1" unit="×" unit-text="times"><input name="v-bo" value="1.5"></tui-scrub></label></div>
```

### Detents · half degrees

```html
<div style="width:230px;text-align:left"><label class="tui-field"><span class="tui-label">Setpoint</span><tui-scrub min="5" max="30" step="0.5" precision="1" unit="°C" data-detent><input name="v-sp" value="19.5"></tui-scrub></label></div>
```

### Error · what was typed stays

```html
<div style="width:230px;text-align:left"><label class="tui-field"><span class="tui-label">Temperature</span><tui-scrub min="0" max="2" step="0.1" precision="2"><input name="v-err" value="0.7x"></tui-scrub><span class="tui-hint">The agent’s sampling.</span></label></div>
```

### Disabled · set by policy

```html
<div style="width:230px;text-align:left"><label class="tui-field"><span class="tui-label">Canary weight</span><tui-scrub min="0" max="100" step="1" unit="%"><input name="v-dis" value="100" disabled></tui-scrub><span class="tui-hint">Locked while the incident is open.</span></label></div>
```

## Every state

- Rest
- Hover
- Scrubbing
- Focus
- At a stop
- Error
- Disabled

Hover, pressed and focus are forced with `.is-hover`, `.is-down` and `.is-focus`; checked and disabled are real.

## API

| Name | Kind | Values | What it does |
| - | - | - | - |
|  | element | wraps one | The well: the numerals on top, the ruler along its floor. Builds itself around the input it is given. |
|  | element | name · value | The form control, kept real: its value posts; it becomes a text spinbutton with inputmode="decimal". |
| min · max | attribute | 0 · 100 | The stops. Values are clamped to them; typed values past them are too, with a knock at the stop. |
| step | attribute | 1 | What an arrow key, the wheel and a detent move by. The ruler ticks once per step. |
| precision | attribute | the step’s decimals | Decimals the value keeps. A drag can land between steps down to this; Alt steps a tenth when it allows. |
| unit · unit-text | attribute | e.g. "ms" · "milliseconds" | Printed after the numerals, and spoken in aria-valuetext. unit-text is the spoken form when the printed one reads badly. |
| px-per-step | attribute | 1.5–8, from the range | Drag rate at slow speed. Faster drags gain up to ×4, smoothly. |
| label | attribute | text | An aria-label for the input when there is no printed label, as in a table cell. |
| data-detent | attribute | — | Drags snap to whole steps; each one moves the ruler a notch and taps the phone’s motor where it has one. |
| data-lock | attribute | — | Pointer lock once a drag passes its dead zone, for endless drags. Refused or unsupported, the drag carries on as usual. |
| data-bound | attribute | "min" · "max" | Set while the value rests on a stop. Read it; the part writes it. |
| data-scrubbing · data-error | attribute | — | Set during a drag, and while typed text is not a number. |
| .tui-scrub--compact | class | — | 26px numerals over a ruler of ticks only: for table cells and dense inspectors. |
| .tui-scrub-row | class | on the .tui-field | The label printed on the left, the field on the right: an inspector row. |
| .tui-scrub__grip | class | added to the label | The field’s printed label becomes a drag handle too. Added by the part. |
| .value · .input | property | number · | Read or set the number (setting fires nothing); reach the input. |
| input · change | event | on the | input while the value moves; change when it settles: drag released, key pressed, text taken. |
| Drag | pointer | 3px dead zone | Press the ruler or the label and drag; right is more. Under 3px it is a click, and you are typing. Esc puts the value back. |
| Shift · Alt | modifier | ×10 · ×0.1 | For drags, arrow keys and the wheel alike. |
| ↑ → · ↓ ← | keyboard | ± step | Step up and down. While you are typing, ← and → move the caret instead. |
| PageUp · PageDown · Home · End | keyboard | ±10 steps · min · max | Big steps and the stops. While typing, Home and End move the caret. |
| Enter · Escape | keyboard | take · revert | Take what was typed, or put back the last good number. |
| Wheel | pointer | while focused | Steps the value only when the field has focus, so scrolling the page never changes it. |
| .is-hover · .is-down · .is-focus | class | on | Forced states, for documentation and specimens only. |

## Markup

```html
<label class="tui-field">
  <span class="tui-label">Temperature</span>
  <tui-scrub min="0" max="2" step="0.1" precision="2">
    <input name="temperature" value="0.70">
  </tui-scrub>
  <span class="tui-hint">0 repeats itself; 2 wanders.</span>
</label>

<label class="tui-field">
  <span class="tui-label">Rate limit</span>
  <tui-scrub min="0" max="6000" step="10" unit="/min" unit-text="requests per minute" data-lock>
    <input name="rate" value="600">
  </tui-scrub>
</label>
```

```js
import '/kit/kit.mjs';   // defines <tui-scrub>

const temp = document.querySelector('tui-scrub input[name="temperature"]');
temp.addEventListener('input', () => preview(Number(temp.value)));   // while it moves
temp.addEventListener('change', () => save(Number(temp.value)));     // when it settles

// Or read and set the number on the element; setting fires nothing.
const field = temp.closest('tui-scrub');
field.value = 0.2;
```

The demo on the page:

```html
<form class="tui-panel" data-demo-form aria-label="Run settings" style="width:min(100%,680px);text-align:left">
  <div class="tui-panel__head"><span class="tui-label">Inspector · planner-7</span><span class="tui-status"><span class="tui-lamp" data-on></span>Live</span></div>
  <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,250px),1fr));gap:20px 22px">
    <label class="tui-field"><span class="tui-label">Temperature</span><tui-scrub min="0" max="2" step="0.1" precision="2"><input name="temperature" value="0.70"></tui-scrub><span class="tui-hint">The agent’s sampling. 0 repeats itself; 2 wanders.</span></label>
    <label class="tui-field"><span class="tui-label">Canary weight</span><tui-scrub min="0" max="100" step="1" unit="%" data-detent><input name="canary" value="5"></tui-scrub><span class="tui-hint">Traffic on api@4f2a91c. Snaps to whole percent.</span></label>
    <label class="tui-field"><span class="tui-label">Setpoint</span><tui-scrub min="5" max="30" step="0.5" precision="1" unit="°C" data-detent><input name="setpoint" value="21.5"></tui-scrub><span class="tui-hint">Hall thermostat, from 06:00, in half degrees.</span></label>
    <label class="tui-field"><span class="tui-label">Rate limit</span><tui-scrub min="0" max="6000" step="10" unit="/min" unit-text="requests per minute" data-lock><input name="rate" value="600"></tui-scrub><span class="tui-hint">Per key on /v1/runs. Drags past the screen edge.</span></label>
  </div>
  <p class="tui-hint" data-demo-post style="margin:18px 0 0;padding:10px 0 0;border-top:1px solid var(--tui-rule)">Form data · temperature 0.70 · canary 5 · setpoint 21.5 · rate 600</p>
</form>
```

## Usage notes

Drag a ruler or a label; hold Shift for ×10, Alt for ×0.1. Click to type, Enter to take it. The rate limit locks the pointer for endless drags.

## Accessibility

- The input is a `role="spinbutton"` with `aria-valuenow`, `aria-valuemin`, `aria-valuemax` and an `aria-valuetext` that carries the unit (“600 requests per minute”). Its name is the printed label that wraps it.
- Everything a drag does, the keyboard does: arrows, Page keys, Home and End, typing. Dragging is never required.
- Typed text that is not a number sets `aria-invalid` and a custom validity message, and the hint below says the range; nothing typed is thrown away.
- Only the ruler and the label take `touch-action: none`: on a phone the page scrolls from anywhere else, and a tap on the numerals opens the decimal keypad.
- The ruler and index are decoration for the number above them (`aria-hidden`). Reduced motion stops the ruler gliding and the stop’s knock; the number, the stop and the lit index still show.

## How it’s built

One custom element around a real input. The ruler is a single strip whose ticks are three repeating backgrounds (fine every step, a longer one halfway, a long one at each round-numbered major) each an ink line with a lit line beside it, so the cuts read engraved; the numbers are spans placed along it. The strip moves by `transform` only, under a fixed index: a printed pointer over a hairline that turns amber with a lamp’s glow while you set the value. Past each end a heavy stop and hatching are printed on the strip, so at min or max the stop arrives under the index; a push past it knocks the hairline 1.5px and lets it settle. The window’s ends are masked away, as a tape curls over its drums. Drags use pointer capture, a 3px dead zone and a smoothed speed-to-gain curve capped at ×4; `data-lock` adds pointer lock.

## Rules

Type it, or drag the ruler. Keys move by the step, a drag reaches the precision, and the index lights only while you are setting it.

The full rules are in `scrub.rules.md` in the brain, which ships with the Kit.

## Related parts

- [Field](https://tactileui.dev/parts/field): Text entry in a recess, the label printed above it, and a hint that says how to fix an error.
- [Stepper](https://tactileui.dev/parts/stepper): A small integer with − and + keys either side, in one well.
- [Knob](https://tactileui.dev/parts/knob): A rotary control for continuous or stepped values where space is square. Its knurl and pointer turn; its light does not.
