Scrub Field <mv-scrub-field>
インスペクター用の数値フィールド:ラベル、アイコン、値を横にドラッグしてスクラブでき、任意のポインターロック、速度に応じた加速、変化量を数えるバッジ、範囲内での値の位置を示すレールを備えます。
一時停止 再生
仕組み
インスペクター用の数値フィールド:ラベル、アイコン、値を横にドラッグしてスクラブでき、任意のポインターロック、速度に応じた加速、変化量を数えるバッジ、範囲内での値の位置を示すレールを備えます。本物のテキストフィールドであることに変わりはなく、数値や式(「12*2」「+=4」)を入力でき、矢印キーでステップし、単位はサフィックスとして表示されます。
カテゴリー フォーム タイプ Web Component(<mv-scrub-field>) ステータス 安定版 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
インストール
node scripts/add.mjs scrub-field --out ./src/marvelous
Marvelous UI の MCP サーバーを使う AI エージェント:install_components({ slugs: ["scrub-field"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。
コピーされるファイル(依存関係を含む):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。
使い方
基本となるマークアップです。ここから属性、data-*、CSS 変数でカスタマイズしてください:
<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); }
@media (forced-colors: active) { #scrub-demo .sfd-layer { border: 2px solid CanvasText; } }
</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 タイプ 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 タイプ 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.