MobTranslate styleguide

Switch

Name a binary feature setting consistently and distinguish requested changes from retained outcomes.

A binary setting

Use Switch for an on/off feature setting. Use Checkbox for inclusion in a form and Toggle for a pressed action such as emphasis. Keep the feature name stable; the checked state communicates on/off.

A real remote setting may need pending, failure and recovery. The example preserves the previous setting until a controlled local outcome and prevents a duplicate change while pending. It schedules no reminders.

Anatomy and specifications

PartCurrent specification
Control / refDefault span switch plus hidden input; public HTMLElement ref
Track44×24px, fully rounded, 2px readable boundary
Thumb20px; logical inline positioning; retained checked state changes position/fill
Label target44px minimum height and width, stable --text-base text
PendingConsumer-owned readOnly guard and persistent explanation; not an automatic request
MotionLogical-position transition; removed under reduced motion

Immediate settings and reference handle behavior

A switch should request a change immediately without a separate Save step. Show local example details below demonstrates an actual immediately effective display setting. The reminders specimen separately exercises an asynchronous outcome boundary: changing the switch starts a request immediately, keeps the retained setting until completion and exposes failure/retry. Its explicit Apply outcome control simulates completion for the guide; it is not a production Save requirement or an actual reminder scheduler.

Use a connected single-choice group for opposing views such as list/map, checkboxes for related saved selections and a button for a call to action. Keep a concise adjacent label describing the feature when on; do not squeeze label text inside the track or substitute ambiguous moon/edit imagery for a binary state. An optional reference handle icon represents on/off, not the feature name.

Reference pointLocal implementation
MeasurementsNative reference track 52×32dp, 2dp outline and 48dp target; handle 16dp off, 24dp on/with icon, 28dp pressed, icon 16dp and 40dp state layer. Local track is nominally 44×24 CSS px with a 20px stable thumb at a 16px root and a minimum 44px label target.
Motion and inputReference handle size can grow on hover/press/drag. This package moves a fixed-size thumb logically and supports installed pointer/keyboard activation; it does not expose a handle-icon/size-morph or drag-to-toggle contract. Reduced motion removes the positional transition.
Focus and namingThe reference describes focus on the handle; the local focusable root exposes switch semantics and a visible ring around the control. Tab reaches that control; Space toggles the mutable setting. Any Enter support must be verified against the installed primitive.
Density and labelsMaterial recommends 48×48 CSS px targets and reversible opt-in density. The local 44px minimum is an explicit site choice, with a foreground label that stays stable across off/on/pending states.

Working requested, retained and failed settings

Local reminders are off.

The stable label names the feature. The checked state describes the retained setting, not the requested change.

tsx
'use client';
import { useRef, useState } from 'react';
import { Button, RadioGroup, RadioItem, Switch } from '@mobtranslate/ui';

export function RadioSelectionExample() {
  const [value, setValue] = useState('compact');
  const [readOnly, setReadOnly] = useState(false);
  const [result, setResult] = useState('No local radio form submitted.');
  const [required,setRequired]=useState(false);
  const [invalid,setInvalid]=useState(false);
  const first=useRef<HTMLElement>(null);
  const effective=value||'compact';
  return <div data-radio-lab className="space-y-4">
    <Button type="button" variant="outline" aria-pressed={readOnly} onClick={() => setReadOnly(current => !current)}>Make local radio group read-only</Button>
    <div className="flex flex-wrap gap-3"><Button type="button" variant="outline" aria-pressed={required} onClick={()=>{setRequired(current=>!current);setValue('');setInvalid(false);setResult('No new local radio form submitted.');}}>Require an explicit radio choice</Button><Button data-radio-restore type="button" variant="outline" onClick={()=>{setRequired(false);setValue('compact');setReadOnly(false);setInvalid(false);}}>Restore initial radio sample</Button></div>
    <form noValidate className="space-y-3" onSubmit={event => { event.preventDefault(); if(required&&!value){setInvalid(true);first.current?.focus();return;}setInvalid(false);setResult(`Local FormData density=${new FormData(event.currentTarget).get('density')}.`); }}>
      <fieldset><legend className="font-semibold">Local reading density</legend>
        <RadioGroup name="density" value={value} readOnly={readOnly} required={required} aria-invalid={invalid||undefined} aria-describedby={invalid?'radio-required-error':undefined} onValueChange={next => { if (typeof next === 'string') {setValue(next);setInvalid(false);} }} aria-label="Local reading density">
          <RadioItem ref={first} value="compact" label="Compact reading" /><RadioItem value="comfortable" label="Comfortable reading" /><RadioItem value="unavailable" label="Unavailable reading option" disabled />
        </RadioGroup>
        {invalid&&<p id="radio-required-error" role="alert">Choose a reading density before submitting.</p>}
      </fieldset>
      <Button type="submit">Submit radio sample</Button>
    </form>
    <p dir="auto" data-density-sample style={{lineHeight:effective === 'compact' ? 1.5 : 1.75, paddingBlock:effective === 'compact' ? 8 : 16}}>This English-only reading sample changes its local spacing without changing the text or any source record.</p>
    <p role="status" dir="auto">Selected local density: {value||'(none)'}. {value?'':'The preview retains compact baseline spacing until a choice is made. '}{result}</p>
  </div>;
}

export function SwitchSelectionExample() {
  const [checked, setChecked] = useState(false);
  const [details,setDetails]=useState(false);
  const [pending, setPending] = useState(false);
  const [requested, setRequested] = useState(false);
  const [result, setResult] = useState('Local reminders are off.');
  const control = useRef<HTMLElement>(null);
  return <div data-switch-lab className="space-y-4">
    <Switch ref={control} label="Local example reminders" checked={checked} readOnly={pending} aria-describedby="local-switch-state" onCheckedChange={next => { if (!pending) { setRequested(next); setPending(true); setResult('Local change pending; current setting is retained until an outcome.'); } }} />
    <p id="local-switch-state" role="status">{result}</p>
    {pending && <div className="flex flex-wrap gap-3"><Button type="button" onClick={() => { setChecked(requested); setPending(false); control.current?.focus(); setResult(`Local reminders are ${requested ? 'on' : 'off'}. No reminder was scheduled.`); }}>Apply local switch change</Button><Button type="button" variant="secondary" onClick={() => { setPending(false); control.current?.focus(); setResult('Simulated change failed. Previous local setting remains; retry is available.'); }}>Fail local switch change</Button></div>}
    <Switch label="Disabled on sample" checked disabled />
    <Switch label="Show local example details" checked={details} onCheckedChange={setDetails} aria-controls="local-switch-details" />
    <div id="local-switch-details" hidden={!details}>These English-only local details appear immediately. No apply action or request is required for this display setting.</div>
    <p dir="auto">The stable label names the feature. The checked state describes the retained setting, not the requested change.</p>
  </div>;
}

API reference

PropContract
labelOptional visible string and default accessible name; provide a meaningful name when omitted.
checked / defaultCheckedControlled boolean / initial uncontrolled boolean; default unchecked.
onCheckedChange(boolean, eventDetails); details.cancel() can reject the change. Use this rather than native onChange.
disabled / readOnly / requiredfalse defaults; unavailable, immutable and required are different states.
name / uncheckedValueForm field name; unchecked is omitted unless an explicit uncheckedValue is supplied.
inputRefRef to hidden HTMLInputElement. The public control ref is HTMLElement, a span by default.
aria-label / aria-labelledby / aria-describedby / aria-invalidConnect a stable name, concise explanation and current error.
className / style / render / native propsInstalled primitive props; styled wrappers retain string-or-state class callbacks. A custom render must preserve semantics, events and refs.
childrenWrapper owns the indicator/thumb. Supply visible text through label and supporting content nearby.

Behavior and keyboard

  • Space changes a mutable focused switch; verify any optional Enter behavior in the installed primitive/browser.
  • Do not change the label from Enable reminders to Disable reminders; use a stable feature name and actual checked state.
  • Pending requested state and retained state are different values. Commit the actual successful result; failure keeps the prior value and a retry path.
  • After outcome controls disappear, return focus to the enabled switch. Disabled is distinct from read-only pending state.
  • If used in a form, inspect the actual hidden input name and uncheckedValue behavior rather than submitting only a visual state.

Failure cases

  • Optimistically announcing a scheduled reminder when no scheduling occurred.
  • A white/dark thumb that is invisible against its track or a track lacking a meaningful boundary.
  • A stale checked value changed again while the first request is pending.
  • Disappearing outcome controls leaving focus on the document body.

Verification and release scope

  • Check actual names, roles, state and focus with pointer and keyboard. Inspect the retained value and submitted FormData when relevant.
  • Exercise empty, selected, mixed, disabled, read-only, invalid and pending/recovery states that apply to the control. Styling alone does not prove enforcement.
  • Check both themes, long labels, 320px reflow, meaningful graphic/text contrast, reduced motion, forced colors and the actual target device.
  • Use a named screen-reader/browser pair to verify spoken state. DOM assertions and local examples do not prove announcements or backend persistence.
  • Selection never establishes language quality, source authority, consent, qualified review or permission. Keep that evidence explicit in the product task.