MobTranslate styleguide

Number field

Edit a real quantity through direct input, named step actions and locale-aware formatting.

Choose a numeric quantity

Use NumberField when numeric ordering and incrementing make sense. Use text Input for phone numbers, postal codes and identifiers that may include leading zeros or nonnumeric syntax. Use Slider when approximate adjustment along a bounded scale is useful; keep an exact-entry alternative.

The component owns a numeric value or null and an editable formatted string. A formatted comma or decimal mark is not the raw submitted quantity. Empty is not zero. Currency/unit presentation requires appropriate instructions and a task-specific numeric policy.

Anatomy and specifications

ExportActual contract
NumberFielddiv state root; caller controls min/max/step/locale/name and raw number|null
NumberFieldGroup2px boundary, radius-md, overflow clipped; focused descendant applies token ring
NumberFieldInputNative visible text input, 44px high, min-width 0, centered text-base; HTMLInputElement ref
Increment / DecrementNative non-submit buttons, 44×44px, default Increase value / Decrease value names, caller may provide task-specific labels; decorative SVG
Hidden inputNative payload value/name; inputRef targets it, not the visible editing input
NumberFieldScrubAreaInstalled unstyled pointer-scrubbing part. direction (horizontal/vertical), pixelSensitivity and teleportDistance follow the pinned declaration. Keep direct input and named step controls as alternatives; this task does not require scrubbing.
NumberFieldScrubAreaCursorOptional cursor part within the scrub composition; native props, class/style/render composition and ref are delegated to the pinned primitive. It does not supply a keyboard replacement or meaningful accessible label by itself.

Working typed, stepped and formatted value

Optional quantity. Empty is null, distinct from zero. Type or use buttons and arrow keys; wheel scrubbing is off.

Raw value: 2.5. No committed local adjustment.

No local numeric payload inspected.

tsx
'use client';
import { useState } from 'react';
import { Button, Field, FieldDescription, FieldLabel, Form, NumberField, NumberFieldDecrement, NumberFieldGroup, NumberFieldIncrement, NumberFieldInput } from '@mobtranslate/ui';

export function NumberInputExample() {
  const [value, setValue] = useState<number | null>(2.5);
  const [disabled, setDisabled] = useState(false);
  const [readOnly, setReadOnly] = useState(false);
  const [locale, setLocale] = useState('en-AU');
  const [committed, setCommitted] = useState('No committed local adjustment.');
  const [result, setResult] = useState('No local numeric payload inspected.');
  return <div data-number-lab className="space-y-4">
    <label>Number format locale <select data-number-locale className="mt-input" value={locale} onChange={event => setLocale(event.target.value)}><option>en-AU</option><option>de-DE</option></select></label>
    <Form onSubmit={event => { event.preventDefault(); setResult(`Native numeric payload: ${String(new FormData(event.currentTarget).get('sampleAmount') ?? 'omitted')}. No data was sent.`); }}>
      <Field name="sampleAmount"><FieldLabel>Local sample amount (0–10 units)</FieldLabel>
        <NumberField value={value} onValueChange={setValue} onValueCommitted={next => setCommitted(`Committed numeric value: ${next === null ? 'empty' : next}.`)} min={0} max={10} step={0.5} largeStep={2} smallStep={0.1} locale={locale} format={{maximumFractionDigits:1}} disabled={disabled} readOnly={readOnly} className={state => state.value === null ? 'local-number-empty' : undefined}>
          <NumberFieldGroup><NumberFieldDecrement aria-label="Decrease local sample amount" /><NumberFieldInput data-number-input /><NumberFieldIncrement aria-label="Increase local sample amount" /></NumberFieldGroup>
        </NumberField><FieldDescription>Optional quantity. Empty is null, distinct from zero. Type or use buttons and arrow keys; wheel scrubbing is off.</FieldDescription>
      </Field><Button type="submit">Inspect numeric payload</Button>
    </Form>
    <div className="flex flex-wrap gap-3"><Button type="button" variant="outline" onClick={() => setReadOnly(current => !current)}>{readOnly ? 'Allow numeric editing' : 'Make number read-only'}</Button><Button type="button" variant="outline" onClick={() => setDisabled(current => !current)}>{disabled ? 'Enable local number' : 'Disable local number'}</Button></div>
    <p data-number-value>Raw value: {value === null ? 'empty' : value}. {committed}</p><p role="status">{result}</p>
  </div>;
}

Root API

PropInstalled contract
value / defaultValueControlled number|null / initial number; empty controlled value is null.
onValueChange / onValueCommittedNumber|null and event details. Commit means an editing boundary, not server persistence.
min / max / stepCaller-supplied bounds; default step 1. Direct typing and blur clamping must be checked as well as step buttons.
largeStep / smallStep / snapOnStepDefaults 10 / 0.1 / false. Shift/meta modifiers choose alternative increments in installed behavior.
locale / formatIntl locales and NumberFormat options. Default runtime locale; stabilize explicit locale for predictable server/client output.
name / required / disabled / readOnlyNative payload and task states; controls integrate with Field. Empty required values need an error.
allowWheelScrubfalse default; opt-in wheel adjustment while focused/hovered. Prevent accidental quantity changes in scrolling forms.
id / inputRefid belongs to visible input; inputRef is the hidden native input. Use NumberFieldInput ref for editing focus.
className / style / render / native propsInstalled Base UI contracts. State callbacks are preserved by styled wrappers. Custom elements must retain semantics, handlers and refs.

Keyboard, formatting and state

  • Tab reaches the editable input and available buttons. Arrow keys adjust; direct typing permits correction; accessible decrement/increment buttons provide a single-pointer alternative to scrubbing.
  • Choose safe precision, bounds and steps for the task. Large/small increments and locale parsing need checks in the actual browser; decimal formatting alone does not establish decimal arithmetic precision.
  • The example offers en-AU/de-DE formats for the same raw quantity, disabled/read-only alternatives and real native FormData inspection. Locale choice is illustrative, not a language-authority claim.
  • At bounds, unavailable step actions should expose their actual disabled state. Read-only and disabled must reject edits, not merely dim the root.

Failures and recovery

  • Do not parse formatted values with a hand-maintained punctuation replacement table. Use the installed parser and validate raw numbers on the server.
  • Preserve temporary editable text while correcting input; do not turn an empty field into zero without the task’s policy. Reject nonfinite/out-of-domain values authoritatively.
  • Scrubbing and wheel input must not be the only adjustment methods. The optional unstyled scrub exports need a separate integration review if used.
  • Commit callbacks are not save receipts. A numeric display does not measure language proficiency, review quality or authority.

Verification and release scope

  • Verify the actual label/control connection, description/error IDs, invalid state, focus order and corrected input with pointer and keyboard. Read the submitted payload rather than inferring it from visible text.
  • Test blank, populated, invalid, disabled, read-only and pending/recovery states. Check both themes, long labels, user text spacing, 320px reflow, reduced motion, forced colors and zoom.
  • Run the named input-family browser checks and source/type/build checks. Inspect mobile keyboards, autofill, password managers and a named screen-reader/browser pair separately. DOM tests do not prove their behavior.
  • Client validation supports correction; it does not establish authorization, consent, source authority, language quality, identity, security or a successful server write.