Beta
Scrub Field <mv-scrub-field>
Zahlenfeld wie im Inspector: Zieh sein Label, Icon oder seinen Wert seitwärts, um zu scrubben, mit optionalem Pointer Lock, geschwindigkeitsabhängiger Beschleunigung, einem Badge, das die Änderung zählt, und einer Schiene, die zeigt, wo der Wert in seinem Bereich liegt. Es bleibt ein echtes Textfeld: Tipp eine Zahl oder einen Ausdruck ein („12*2“, „+=4“), ändere den Wert schrittweise mit den Pfeiltasten, und eine Einheit erscheint als Suffix.
Kategorie Formulare Typ Web Component (<mv-scrub-field>) Status Beta Keywords scrub, number, inspector, spinbutton, pointer-lock, expression, unit, form-associated, editor
When to use
A design-tool inspector needs compact numeric fields for size, spacing or opacity that can be dragged Users type expressions like 12*2 or +=4 and see a unit suffix such as px or ms Many numeric properties must fit in a dense side panel
Avoid when
The form targets general users who expect visible - and + buttons → use Number Field instead Several linked values like X/Y or width/height are edited together → use Vector Field instead It is a shopping quantity with stock limits → use Quantity instead
Hol dir Scrub Field. In jedem Plan enthalten: Das Pro pack liefert alle 317 Komponenten mit der CLI und dem unten verwendeten MCP-Server.
Pläne ansehen
Installation
node scripts/add.mjs scrub-field --out ./src/marvelous
KI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["scrub-field"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Kopierte Dateien (inklusive Abhängigkeiten): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/form.js, core/motion.js, components/scrub-field/scrub-field.js, components/scrub-field/scrub-field.css.
Verwendung
Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:
<div id="scrub-demo">
<style>
#scrub-demo { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: center; justify-content: center; width: 100%; }
#scrub-demo .sfd-stage { display: grid; place-items: center; width: 16rem; height: 16rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-xl); background: repeating-linear-gradient(0deg, transparent 0 15px, color-mix(in oklch, var(--mv-fg) 5%, transparent) 15px 16px), repeating-linear-gradient(90deg, transparent 0 15px, color-mix(in oklch, var(--mv-fg) 5%, transparent) 15px 16px), var(--mv-bg); overflow: hidden; }
#scrub-demo .sfd-layer { display: grid; align-content: end; width: 8.5rem; height: 8.5rem; padding: .8rem; color: #fff; font: 600 .8rem/1.25 var(--mv-font-sans); background: linear-gradient(150deg, oklch(0.74 0.14 205), oklch(0.56 0.19 262)); }
#scrub-demo .sfd-layer small { font: 500 .65rem var(--mv-font-mono); opacity: .85; }
#scrub-demo .sfd-panel { width: 16.5rem; padding: .75rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-lg); background: var(--mv-surface); box-shadow: var(--mv-shadow-sm); }
#scrub-demo .sfd-head { display: flex; justify-content: space-between; align-items: baseline; margin: 0 0 .65rem; font: 600 .7rem var(--mv-font-sans); letter-spacing: .06em; text-transform: uppercase; color: var(--mv-fg-subtle); }
#scrub-demo .sfd-head span { text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--mv-fg-muted); }
#scrub-demo .sfd-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .375rem; }
#scrub-demo .sfd-grid mv-scrub-field { width: 100%; }
#scrub-demo .sfd-wide { grid-column: 1 / -1; }
#scrub-demo .sfd-hint { margin: .7rem 0 0; font-size: .7rem; line-height: 1.55; color: var(--mv-fg-muted); }
#scrub-demo .sfd-hint kbd { padding: 0 .25rem; border: 1px solid var(--mv-border); border-radius: 3px; background: var(--mv-bg-muted); font: 500 .65rem var(--mv-font-mono); }
</style>
<div class="sfd-stage"><div class="sfd-layer" id="scrub-demo-layer">Frame<small id="scrub-demo-out"></small></div></div>
<div class="sfd-panel" role="group" aria-label="Inspector">
<p class="sfd-head">Appearance <span>Frame 12</span></p>
<div class="sfd-grid">
<mv-scrub-field id="scrub-demo-radius" label="Radius" unit="px" value="24" min="0" max="72"></mv-scrub-field>
<mv-scrub-field id="scrub-demo-opacity" label="Opacity" unit="%" value="100" min="0" max="100"></mv-scrub-field>
<mv-scrub-field id="scrub-demo-rotate" aria-label="Rotation" unit="°" value="-8" min="-180" max="180" wrap wheel>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 12a8 8 0 1 1-2.34-5.66"/><path d="M20 4v4h-4"/></svg>
</mv-scrub-field>
<mv-scrub-field id="scrub-demo-scale" aria-label="Scale" unit="×" value="1" min="0.25" max="2" step="0.05" precision="2">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h6v6M10 20H4v-6M20 4l-6 6M4 20l6-6"/></svg>
</mv-scrub-field>
<mv-scrub-field id="scrub-demo-blur" class="sfd-wide" label="Shadow blur" unit="px" value="28" min="0" max="96"></mv-scrub-field>
<mv-scrub-field id="scrub-demo-duration" class="sfd-wide" label="Duration (locked)" unit="ms" value="240" step="10" readonly></mv-scrub-field>
</div>
<p class="sfd-hint">Drag a label or a value sideways (<kbd>Shift</kbd> ×10, <kbd>Alt</kbd> ×0.1, <kbd>Esc</kbd> cancels). Click to type: <kbd>12*2</kbd>, <kbd>+=4</kbd>, <kbd>/2</kbd>.</p>
</div>
</div>
<script type="module">
const $ = (id) => document.getElementById(`scrub-demo-${id}`);
const layer = $("layer");
const apply = () => {
const r = $("radius").value, o = $("opacity").value, rot = $("rotate").value, s = $("scale").value, b = $("blur").value;
layer.style.borderRadius = `${r}px`;
layer.style.opacity = String(o / 100);
layer.style.transform = `rotate(${rot}deg) scale(${s})`;
layer.style.boxShadow = `0 ${b / 3}px ${b}px -${b / 4}px oklch(0.4 0.16 262 / .5)`;
$("out").textContent = `${r}px · ${rot}° · ${s}×`;
};
$("radius").closest("#scrub-demo").addEventListener("mv-input", apply);
customElements.whenDefined("mv-scrub-field").then(() => requestAnimationFrame(apply));
</script>
API
Attributes
Name Typ Default Description valuenumber 0 (or min)Initial value, restored on form reset. labelstring Short label shown in the handle (a <label> linked to the field). Without a label: a child icon (<svg>) and aria-label on the host. unitstring Displayed suffix: px, %, °, ms, ×… Ignored when typing (“12px” = 12). min / maxnumber Bounds (clamped, or modulo with wrap). stepnumber 1Step for dragging and arrow keys: Shift ×10, Alt ×0.1; PageUp/PageDown ×10. precisionnumber step decimals + 1Number of decimals kept and displayed. sensitivitynumber 2Drag pixels per step (before acceleration). wrapboolean The value loops between min and max (angles). wheelboolean A horizontal trackpad swipe scrubs the value; vertical scrolling stays free. no-lockboolean Disables pointer lock while dragging. Without lock the system cursor stays visible (ew-resize) and travel stops at the screen edge. no-accelboolean Disables acceleration based on gesture speed. localestring document langDisplayed decimal separator (input accepts both “,” and “.”). name / disabled / readonly / requiredStandard form field behavior.
Properties
Name Typ Description valuenumber Current value. formattedstring Value formatted with its unit (“12px”). editingboolean True while an uncommitted edit is in progress.
Methods
Name Description stepUp(n) / stepDown(n)Increments or decrements by n steps. evaluate(text, current)Module export: evaluates the safe expression (no eval) and returns null if it can’t be fully parsed. scrubGesture(el, options)Module export: the horizontal drag gesture (threshold, optional pointer lock with a virtual cursor, Escape to cancel) for other controls.
Events
Name Description mv-inputOn every change (drag, arrows, committed input). detail: { value, formatted }. mv-changeOn commit: drag release, Enter, arrow key, blur. mv-scrubstart / mv-scrubendStart and end of a gesture (a single undo step); detail.cancelled after Escape.
Content structure
Name Description child <svg>Icon placed in the handle (aria-hidden).
CSS classes
Name Description mv-scrub-field-handle / -label / -input / -unit / -rail / -deltaGenerated parts (the rail appears when both min and max are set). mv-scrub-field-cursorVirtual cursor appended to <body> only while the pointer is locked.
CSS variables
Name Default Description --mv-scrub-field-width6.5remWidth. --mv-scrub-field-heightvar(--mv-control-sm)Height. --mv-scrub-field-bgField background. --mv-scrub-field-radiusvar(--mv-radius-sm)Radius.
Accessibility
The value is a real text input with role="spinbutton", aria-valuenow/min/max and an aria-valuetext that includes the unit. It is named by its label (<label for>), aria-label or an external <label>. Keyboard: ↑/↓ (Shift ×10, Alt ×0.1), PageUp/PageDown, Home/End when not typing, Enter to commit, Escape to cancel an edit or a drag. An invalid expression is flagged with aria-invalid. The system cursor is never hidden unless the browser grants pointer lock, when a virtual cursor replaces it. Dragging the value itself is for mouse and pen; on touch, drag the handle and tap the value to type. Motion (step nudge, error shake) is skipped under reduced motion, and forced-colors mode uses system colors for the frame, rail, badge and cursor.