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.

KategorieFormulare
TypWeb Component (<mv-scrub-field>)
StatusBeta
Keywordsscrub, 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

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

NameTypDefaultDescription
valuenumber0 (or min)Initial value, restored on form reset.
labelstringShort label shown in the handle (a <label> linked to the field). Without a label: a child icon (<svg>) and aria-label on the host.
unitstringDisplayed suffix: px, %, °, ms, ×… Ignored when typing (“12px” = 12).
min / maxnumberBounds (clamped, or modulo with wrap).
stepnumber1Step for dragging and arrow keys: Shift ×10, Alt ×0.1; PageUp/PageDown ×10.
precisionnumberstep decimals + 1Number of decimals kept and displayed.
sensitivitynumber2Drag pixels per step (before acceleration).
wrapbooleanThe value loops between min and max (angles).
wheelbooleanA horizontal trackpad swipe scrubs the value; vertical scrolling stays free.
no-lockbooleanDisables pointer lock while dragging. Without lock the system cursor stays visible (ew-resize) and travel stops at the screen edge.
no-accelbooleanDisables acceleration based on gesture speed.
localestringdocument langDisplayed decimal separator (input accepts both “,” and “.”).
name / disabled / readonly / requiredStandard form field behavior.

Properties

NameTypDescription
valuenumberCurrent value.
formattedstringValue formatted with its unit (“12px”).
editingbooleanTrue while an uncommitted edit is in progress.

Methods

NameDescription
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

NameDescription
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

NameDescription
child <svg>Icon placed in the handle (aria-hidden).

CSS classes

NameDescription
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

NameDefaultDescription
--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.

Diese Seite wurde mit KI übersetzt. Übersetzungsfehler melden