MobTranslate styleguide

Date and time input

Choose calendar dates and wall-clock windows with explicit bounds, identity and recovery. Keep civil values distinct from instants.

Choose the temporal model first

TaskValue and interface
Calendar dateA birthday, collection date or date-only filter is a calendar value. Use Input type=date and retain its normalized date string. Do not introduce the viewer’s zone by parsing it into an instant.
Time of dayA daily opening window or repeated wall-clock time is a civil time. Use Input type=time; state precision, limits and whether a window crosses midnight.
Local date and timeInput type=datetime-local combines civil date/time without a zone. Use when the task needs that combination and provide zone/disambiguation separately if scheduling an instant.
InstantA unique point on the timeline needs an offset/zone-aware domain conversion. Resolve nonexistent/repeated local times through an explicit policy and show the result before confirming.
Date rangeExpose labelled beginning and end, specify inclusivity, validate ordering and retain both drafts when correction is needed.
Visual calendar or clockNative picker chrome depends on browser/OS. A custom calendar/clock requires a separate tested interaction model; this package does not export a Material calendar grid, clock dial or date-range popup.

Web equivalent and anatomy

Material’s date and time picker families map here to native web input compositions, using the existing styled Input. The example includes actual range and window filtering, error focus, pending outcomes and empty recovery. It does not impersonate a Material picker or add an unimplemented package API.

PartSpecification
Persistent labelSeparate named first/last dates and earliest/latest times. Do not rely on locale placeholders or an unlabeled calendar glyph.
Editable controlNative type, required/name, min/max/step, value/onChange and HTMLInputElement ref. Minimum 44px height, full available width with min-width:0; browser owns internal segments and picker chrome.
Supporting textState calendar bounds, increment, zone meaning, interval endpoints and midnight policy before input. Connect it with aria-describedby.
ValidationVisible linked error with aria-invalid on its field; move focus to the first invalid control. Keep the loaded result and useful values intact.
Applied resultShow normalized dates, times and named zone alongside actual records. Separate draft, requested and applied state.
ActionsExplicit Apply, Clear, external Restore and surviving completion/failure/cancellation. Actions wrap within narrow containers with at least 44px targets.
DirectionLogical layout and bdi isolate displayed temporal values. Native segment order follows browser/locale; never reverse date strings manually.

Material date entry, calendar views and confirmation

The reference distinguishes docked calendar entry, modal calendar and modal input, with single-date, range and year views. Near dates may benefit from a calendar; distant dates such as a birthday need efficient typed or month/year entry. Compact native full-screen and larger docked arrangements are reference choices, not browser chrome supplied by this package.

The captured usage text says tapping the field opens a docked calendar, while its accessibility chapter makes the calendar icon the exclusive calendar entry. Preserve independently useful manual input and a deliberate optional launcher rather than making every text-entry focus open a custom modal. This lab uses native date controls and does not add a custom calendar launcher or claim its OS popup appearance.

Reference decisionLocal web contract and application choice
Date identity and selectionDistinguish today, selected day, disabled dates, and range endpoints/interior. Local applied dates are normalized civil strings; input drafts, requested windows and actual filtered records have separate owners. Browser/OS owns any calendar highlight, year/month menu, locale order and picker dismissal.
Entry and formattingNative inputs provide their browser-supported editing format and serialize normalized values. The reference encourages flexible separators and formatting after completion, without masks that rewrite during typing. This example does not advertise an arbitrary locale-text parser; a custom text adapter must declare accepted formats and preserve partially entered drafts.
Anatomy, targets and contrastNative reference targets are 48dp with date text contrast of at least 4.5:1. Its month/year/day menus and full-screen measurements are not local CSS presets. The local labelled fields have 44px minimum height, explicit Gregorian bounds and instructions; test actual internal native segments and OS picker targets separately.
Keyboard and namingA custom calendar needs fully named dates, weekday headers, a grid, selected/today/disabled state and tested month/year navigation. The source keyboard table duplicates Enter and gives Home/End the same description; use the actual installed interaction rather than copying that defect. Native input editing keys stay native here, with persistent labels and connected help.
Clear and shortcutsKeep optional clear and shortcut affordances only when they have real effects. The local Clear empties all drafts, preserves loaded results and focuses the first date; Restore is independent. No unimplemented month/year shortcut is advertised.
Adaptation and dismissalNative range views can use vertical month browsing, and compact full-screen adds explicit close/save; the reference also warns against simply scaling fixed picker geometry across breakpoints. Web native controls remain bounded fields and delegate picker internals to the platform. A custom modal would need real confirm/cancel, retained draft, background/focus policy and viewport tests.

Material time formats, dial alternatives and precision

The reference has dial and input variants, with 12-hour period selection or 24-hour time without a period control. It recommends manual entry from every dial view, and input-only presentation when a dial cannot fit. Native format preference normally comes from settings outside the picker. This web fixture uses native time inputs; it supplies no clock face, custom AM/PM radio or native orientation transition.

Reference decisionLocal web contract and application choice
Anatomy and native dimensionsThe reference separates hour/minute fields, period selector, dial, mode switch and confirm/cancel. Examples include 256dp dial,48dp handle,24dp padding,80dp dial-selector height and 72dp input-selector height. These are native design references; local input dimensions and precision are the actual contract above.
Constrained viewportSwitch native orientation or fall back to text rather than compressing dial targets or forcing a scrolling clock face. This lab keeps independently labelled full-width fields and wrapping actions. Native picker layout and mobile keyboards require target-device checks; a DOM screenshot cannot certify them.
Precision and purposeThe usage chapter discourages millisecond-level selection while accessibility mentions finer precision in some cases. The local policy is explicitly quarter-hour civil windows, not stopwatch timing. Seconds, overnight windows and instant booking need deliberate supported models, validation and downstream precision.
Roles and keyboardReference SDKs differ on radio versus checkbox period semantics and dial roles. Keep actual native web time input semantics and ordinary Tab order; never imitate a dial keyboard table with global arrow handlers. Visible instructions and normalized payload remain available independently of picker chrome.
Commit and time-zone meaningThe reference separates temporary selection from OK/Cancel. Here Apply snapshots actual native form entries; only explicit completion changes records, and failure/cancel retain previous results. A wall-clock value plus zone still needs a maintained DST/disambiguation service before it can become a booked instant.

Working civil-time window

This English-only page-memory dataset holds civil records in a named zone. The applied inclusive date and daily time windows filter real records. Native FormData captures normalized values; the zone is validated/canonicalized by the runtime’s Intl.DateTimeFormat. It identifies civil records, without converting them into instants.

Dates are bounded to 2026–2028 and times to 09:00–17:00 in 15-minute increments. Ordering is validated in addition to native constraints. Failure/cancellation preserve loaded records; only explicit completion applies the request. An empty result has working restore/apply recovery. No remote availability, durable booking, permission or cultural source authority is implied.

Inclusive calendar dates, 2026–2028. Daily inclusive wall-clock window, 09:00–17:00 in 15-minute steps. The zone identifies matching civil records; these values do not identify an instant.

Loaded civil-time records

2026-10-13 through 2026-10-15; 09:00 through 17:00; Australia/Brisbane

  • English-only reading session: 2026-10-13 09:00 Australia/Brisbane
  • English-only note session: 2026-10-14 12:00 Australia/Brisbane
  • English-only example session: 2026-10-15 16:30 Australia/Brisbane

Actual native entries: No native form submission.

Initial civil-time window loaded. No instant conversion or remote request.

tsx
'use client';
import {useRef,useState,type CSSProperties} from 'react';
import {Button,Input} from '@mobtranslate/ui';
type WindowDraft={start:string;end:string;from:string;until:string;zone:string};
const initial:WindowDraft={start:'2026-10-13',end:'2026-10-15',from:'09:00',until:'17:00',zone:'Australia/Brisbane'};
const records=[{id:'reading',label:'English-only reading session',date:'2026-10-13',time:'09:00',zone:'Australia/Brisbane'},{id:'notes',label:'English-only note session',date:'2026-10-14',time:'12:00',zone:'Australia/Brisbane'},{id:'examples',label:'English-only example session',date:'2026-10-15',time:'16:30',zone:'Australia/Brisbane'}];
const action:CSSProperties={minWidth:0,maxWidth:'100%',minHeight:44,height:'auto',whiteSpace:'normal',overflowWrap:'anywhere'};
function canonicalZone(zone:string){return new Intl.DateTimeFormat('en',{timeZone:zone}).resolvedOptions().timeZone;}
export function DateTimeWindowExample(){
  const [draft,setDraft]=useState(initial);
  const [applied,setApplied]=useState(initial);
  const [requested,setRequested]=useState<WindowDraft | null>(null);
  const [disabled,setDisabled]=useState(false);
  const [readOnly,setReadOnly]=useState(false);
  const [rtl,setRtl]=useState(false);
  const [error,setError]=useState<{field:keyof WindowDraft;message:string} | null>(null);
  const [payload,setPayload]=useState('No native form submission.');
  const [status,setStatus]=useState('Initial civil-time window loaded. No instant conversion or remote request.');
  const form=useRef<HTMLFormElement>(null);
  const heading=useRef<HTMLHeadingElement>(null);
  const pending=requested!==null;
  const visible=records.filter(record=>record.date>=applied.start && record.date<=applied.end && record.time>=applied.from && record.time<=applied.until && canonicalZone(record.zone)===applied.zone);
  function change(field:keyof WindowDraft,value:string){setDraft(current=>({...current,[field]:value}));setError(null);}
  function reject(field:keyof WindowDraft,message:string){setError({field,message});(form.current?.elements.namedItem(field) as HTMLInputElement)?.focus();}
  function outcome(result:'complete'|'fail'|'cancel'){
    if(!requested)return;
    if(result==='complete'){setApplied(requested);setStatus('Actual local civil-time records filtered. No durable save or instant conversion.');}
    else setStatus(`Local window ${result==='fail'?'failed':'cancelled'}. Draft values and loaded records retained; submit again to retry.`);
    setRequested(null);heading.current?.focus();
  }
  const fields=[{key:'start' as const,label:'First calendar date',type:'date',min:'2026-01-01',max:'2028-12-31',step:1},{key:'end' as const,label:'Last calendar date',type:'date',min:'2026-01-01',max:'2028-12-31',step:1},{key:'from' as const,label:'Earliest wall-clock time',type:'time',min:'09:00',max:'17:00',step:900},{key:'until' as const,label:'Latest wall-clock time',type:'time',min:'09:00',max:'17:00',step:900}];
  return <div data-date-time-lab className="space-y-4" dir={rtl?'rtl':'ltr'}>
    <div className="flex flex-wrap gap-3"><Button type="button" style={action} variant="outline" disabled={pending} aria-pressed={disabled} onClick={()=>setDisabled(v=>!v)}>Disable civil-time fields</Button><Button type="button" style={action} variant="outline" disabled={pending} aria-pressed={readOnly} onClick={()=>setReadOnly(v=>!v)}>Make civil-time fields read only</Button><Button type="button" style={action} variant="outline" aria-pressed={rtl} onClick={()=>setRtl(v=>!v)}>Use RTL civil-time layout</Button><Button type="button" style={action} variant="outline" disabled={pending} onClick={()=>{setDraft(initial);setDisabled(false);setReadOnly(false);setError(null);}}>Restore initial civil-time draft</Button></div>
    <p id="date-time-help">Inclusive calendar dates, 2026–2028. Daily inclusive wall-clock window, 09:00–17:00 in 15-minute steps. The zone identifies matching civil records; these values do not identify an instant.</p>
    <form ref={form} noValidate className="space-y-4" onSubmit={event=>{
      event.preventDefault();if(pending||disabled)return;
      for(const field of fields){const input=event.currentTarget.elements.namedItem(field.key) as HTMLInputElement;if(!input.value || !input.validity.valid){reject(field.key,`${field.label}: enter a valid value within the stated bounds and increment.`);return;}}
      if(draft.end<draft.start){reject('end','Last calendar date must be on or after the first date.');return;}
      if(draft.until<draft.from){reject('until','Latest wall-clock time must be on or after the earliest time. This daily window does not cross midnight.');return;}
      let zone:string;try{if(!draft.zone.trim())throw new Error('Missing zone');zone=canonicalZone(draft.zone.trim());}catch{reject('zone','Enter a time-zone identifier supported by this runtime, such as Australia/Brisbane.');return;}
      setPayload(JSON.stringify([...new FormData(event.currentTarget).entries()]));setRequested({...draft,zone});setError(null);setStatus('Civil-time window requested; loaded records remain until explicit completion.');heading.current?.focus();
    }}>
      {fields.map(field=><div key={field.key}><label htmlFor={`civil-${field.key}`} className="block font-semibold mb-2">{field.label}</label><Input id={`civil-${field.key}`} name={field.key} type={field.type} required min={field.min} max={field.max} step={field.step} value={draft[field.key]} onChange={event=>change(field.key,event.target.value)} disabled={disabled} readOnly={readOnly||pending} aria-describedby={error?.field===field.key?'date-time-help date-time-error':'date-time-help'} aria-invalid={error?.field===field.key} style={{minWidth:0,maxWidth:'100%',minHeight:44}} /></div>)}
      <div><label htmlFor="civil-zone" className="block font-semibold mb-2">Civil record time zone</label><Input id="civil-zone" name="zone" required value={draft.zone} onChange={event=>change('zone',event.target.value)} disabled={disabled} readOnly={readOnly||pending} aria-describedby={error?.field==='zone'?'date-time-help date-time-error':'date-time-help'} aria-invalid={error?.field==='zone'} style={{minWidth:0,maxWidth:'100%',minHeight:44}} /></div>
      {error && <p role="alert" id="date-time-error">{error.message}</p>}
      <div className="flex flex-wrap gap-3"><Button type="submit" style={action} disabled={disabled||pending}>Apply civil-time window</Button><Button type="button" style={action} variant="outline" disabled={disabled||readOnly||pending} onClick={()=>{setDraft({start:'',end:'',from:'',until:'',zone:''});setError(null);(form.current?.elements.namedItem('start') as HTMLInputElement)?.focus();}}>Clear civil-time draft</Button></div>
    </form>
    <h3 ref={heading} tabIndex={-1} data-date-time-heading className="font-semibold">Loaded civil-time records</h3><p dir="auto" data-date-time-applied style={{overflowWrap:'anywhere'}}><bdi>{applied.start}</bdi> through <bdi>{applied.end}</bdi>; <bdi>{applied.from}</bdi> through <bdi>{applied.until}</bdi>; <bdi>{applied.zone}</bdi></p>
    <ul data-date-time-records className="guidance-list">{visible.map(record=><li key={record.id} data-record-id={record.id}>{record.label}: <bdi>{record.date} {record.time} {record.zone}</bdi></li>)}</ul>
    {!visible.length && <p data-date-time-empty>No local civil records match. Restore the initial draft and apply it to recover.</p>}
    <p data-date-time-payload style={{overflowWrap:'anywhere'}}>Actual native entries: {payload}</p>
    {pending && <div className="flex flex-wrap gap-3"><Button type="button" style={action} onClick={()=>outcome('complete')}>Complete civil-time request</Button><Button type="button" style={action} variant="outline" onClick={()=>outcome('fail')}>Fail civil-time request</Button><Button type="button" style={action} variant="outline" onClick={()=>outcome('cancel')}>Cancel civil-time request</Button></div>}
    <p role="status" data-date-time-status>{status}</p>
  </div>;
}

Input API and form ownership

ContractUse
typedate, time or datetime-local select the native temporal control. Keep the task model explicit.
value / onChangeControlled draft strings; reflect the actual input value. Empty string is a real draft state. Do not switch between controlled and uncontrolled ownership.
defaultValueUncontrolled initial value; later prop changes do not constitute an external reset.
name / FormDataSerialize successful native controls. Disabled fields are omitted; read-only fields remain registered. The example captures entries before entering pending state.
required / min / max / stepNative temporal constraints. The example calls native validity and also validates pair ordering. step is days for date and seconds for time/datetime-local.
readOnly / disabledPending uses read-only fields so values remain accessible and submitted; duplicate Apply is unavailable. An independent Restore can recover deliberately unavailable fields.
state / aria-invalidInput state=error supplies visual styling; aria-invalid and described error convey field semantics. Validation copy must identify how to fix the value.
ref / id / aria-describedbyRef reaches the native input; labels/descriptions/errors stay associated with the real control.
Custom picker boundaryNo showPicker wrapper or custom grid is promised. A product that adds a picker launcher must test support, user activation, read-only/disabled behavior, naming and focus recovery.

Calendar validation and precision

  • Use the browser’s temporal parser/validity for native drafts, including leap days, out-of-range values and step mismatch. Validate incoming server/URL values through the domain model too; native controls can sanitize invalid strings to empty.
  • The fixture compares normalized fixed-width in-range calendar/time strings after native validation. That ordering is specific to these Gregorian values, not a locale-text parser or a general historical-calendar algorithm.
  • State whether endpoints are inclusive. This fixture applies its time window to each date independently; it is not a continuous instant interval from the first morning to the last evening.
  • Reversed date/time pairs produce focused errors instead of silently swapping user intent. The fixture disallows midnight-crossing windows; overnight schedules need an explicit next-day model.
  • Choose meaningful precision. A quarter-hour increment is a fixture policy, not a universal appointment rule. If seconds are meaningful, expose them and preserve that precision through storage.
  • Do not enforce a frozen “today” or minimum age through a build-time constant. When the product needs relative limits, calculate them in its authoritative clock/zone and describe the boundary.

Zones, daylight saving and display

  • A date-only value has no time-zone offset. A civil date/time plus a zone is still not always a unique instant: a transition can repeat or remove local clock times.
  • Before booking an instant, resolve the selected civil values with a maintained zone-aware domain service. Require a deliberate repeated-time choice and explain nonexistent-time correction; never silently shift an appointment.
  • Persist the task’s actual model: date-only strings for calendar facts, civil fields/zone for recurring schedules, and a resolved instant plus relevant zone/intent for a confirmed one-off event.
  • The lab validates runtime-supported zone identifiers with Intl and uses the canonical result for matching. It does not manually maintain an alias or offset map. Unsupported zone input produces a recoverable field error.
  • Intl formatting localizes an existing instant; it does not by itself interpret an arbitrary local date/time as an instant. Never derive that conversion from a formatted string.
  • Native displayed segment order, month names and 12/24-hour presentation may differ from serialized values. Keep labels readable, display the zone near a scheduled result and test the actual target locales/calendars/devices.

Keyboard, accessibility and responsive behavior

  • Use persistent native labels and ordinary document Tab order. Native segments and picker interactions are browser/OS responsibilities; do not replace arrow keys with a global calendar handler.
  • Keep validation instructions available before submit. On error, focus the actual invalid field and retain useful sibling values. Pending and applied statuses live outside unavailable controls.
  • Native picker appearance is not fully styled by these tokens. Test high contrast, zoom, date-segment names and chosen browser/assistive-technology pairs; DOM validity alone does not prove spoken behavior.
  • At narrow widths, each labelled field occupies its own row and actions wrap. Check native intrinsic width rather than assuming width:100% prevents overflow.
  • Show calendar values in isolated text and let locale/browser semantics govern entry. RTL layout does not imply reversing ISO serialization or a time-zone identifier.
  • For a custom grid, separately implement named month/year navigation, roving day focus, selected/today/disabled distinctions, keyboard month/year movement and modal focus restoration. For a custom clock, supply a text/keyboard alternative to dragging. Test those implementations before documenting them as available.

States, recovery and acceptance evidence

StateVerify
Empty / invalidClear really empties drafts; required correction focuses the first field. Impossible dates, boundaries and increments are rejected by actual controls/domain checks.
Reversed range / windowFocus the ending field, explain the ordering and preserve the loaded records.
Unsupported zoneA connected error explains the expected identifier; no partial request or new result.
Read-only / disabledEditing is unavailable; native entries distinguish retained values from omission. External restoration remains reachable.
Pending / failed / cancelledSnapshot actual values, reject duplicates, keep loaded content, offer explicit surviving outcomes and retry.
Completed / empty resultChange actual record content only on completion. Explain zero results and provide working recovery.
Locale / DST / deviceTest leap boundaries, time precision, midnight, repeated/nonexistent times, locale formats, hardware pickers, spoken labels and service-side scheduling policy independently.

Verification and primary references

Run test:date-time plus checked-source/token freshness, lint, types, production build and the full route/fragment crawl. The fixture’s browser checks verify native values, constraints, ordering, zone recovery, request outcomes and reflow; they do not certify OS picker chrome, every locale or an instant-booking service.

  • HTML defines the native date/time states, normalized values and constraints. ECMA-402 defines runtime Intl formatting and time-zone options. Follow the actual installed web behavior alongside these standards.