MobTranslate styleguide

Dialog

Focus a short task over the page while preserving input, a clear exit and an honest task outcome.

Choose the right surface

Use a modal dialog for a bounded task that needs focused attention. Prefer an inline form or dedicated page for long, multi-step work, and AlertDialog for a brief consequential decision. A toast is supplementary feedback, not a modal task surface.

Dialog maps the Material dialog family to installed web primitives. It does not implement native full-screen transitions or operating-system back behavior. At narrow or short viewports, the current popup bounds its height and scrolls; choose a dedicated page when this no longer supports the task.

Anatomy and specifications

PartCurrent CSS
BackdropFixed full viewport, 50% black, --z-modal-backdrop
PopupCentered fixed surface; 24px padding; 2px border; --radius-lg; --shadow-xl
Sizessm 24rem / md 32rem / lg 42rem / xl 56rem maximum widths; md default
ViewportWidth viewport minus 2rem; max-height 100dvh minus 2rem; vertical scrolling
Title / description--text-xl weight 700 / --text-base muted
MotionToken-duration scale/fade; explicit animation:none under reduced motion
ActionsCaller-owned controls that wrap and remain reachable at short heights

Material variants, action hierarchy and web adaptation

The reference distinguishes basic dialogs for critical bounded tasks and compact full-screen dialogs for more involved input, with larger windows returning to a basic surface. Its native basic geometry includes 280–560dp width, 28dp corners, 24dp padding, 16dp title/body separation and 8dp action spacing; full-screen uses a 56dp header and square corners. The installed web sizes and bounded scrolling surface are specified separately above. They do not implement a native full-screen variant, FAB transform, predictive back or a platform window.

A native full-screen flow may ask before discarding unsaved changes and can show a basic child dialog; that is not permission to stack arbitrary web modals. The optional local discard policy instead replaces the form with a clear two-action decision inside the same named dialog, retaining its draft and safe initial focus. Keep editing returns to the field; Discard leaves the saved collection unchanged. Clean dismissal never needs the extra decision.

Reference decisionLocal implementation and consuming-product responsibility
Purpose, title and priorityReserve interruption for a task or consequential decision, and use inline feedback for routine status. Use a specific title and explicit action names, rather than an ambiguous question or generic confirmation. Native optional titles do not remove the web requirement for an accessible name. Long names remain available rather than silently truncating at a native line limit.
Actions and alignmentNative guidance normally allows acknowledgement alone or confirm plus dismiss, with confirm at the logical trailing edge; stacked native actions use a different order. The local form puts Cancel before Save with Save at the logical trailing edge. At narrow widths it wraps while retaining that reading/focus order, rather than reversing the stacked DOM order. The discard decision exposes exactly Keep editing and Discard edits while hiding the editing controls. Simulated pending outcome controls are test instrumentation, not a recommendation for a five-action production dialog.
Validation versus pendingThe native basic recommendation disables confirmation until a choice, while its full-screen chapter discourages disabled confirmation. This form keeps Save discoverable, validates on submit and focuses the actual error. Pending keeps an explicit cancellation/outcome path and prevents duplicate application. Field errors are inline and task failures retain input; a network error does not mandate opening another modal here.
Scrolling and visibilityNative long-content guidance pins title and actions. The shared bounded popup scrolls as one semantic surface so enlarged titles/actions cannot consume the entire short viewport. Reach the last action with keyboard and scroll, keep background scroll policy consistent with modality and prefer a dedicated page for extensive work. Do not advertise a sticky header/footer contract that the popup does not supply.
Role and initial focusThe reference calls basic web dialogs alert dialogs and initially focuses the first interactive element. On this installed web stack, editing uses role=dialog and a labelled field; a consequential confirmation uses AlertDialog with least-destructive initial focus. Long structured information may instead focus a readable heading. Preserve the installed semantics, trap/dismissal policy and final focus rather than applying one native table universally.

Working edit, validation and recovery

Submit valid input to enter a controlled local pending state. Choose simulated success, failure or cancellation. Failure retains input and offers retry; success alone updates the retained local collection. Escape and outside dismissal are rejected while pending, and cancellation remains available. Enable Confirm discarding local edits before opening to inspect a retained draft decision on an attempted close; the default small-edit policy still discards uncommitted edits on close.

Current local collection: Local collection

tsx
'use client';
import { useLayoutEffect, useRef, useState } from 'react';
import { Button, Dialog, DialogBackdrop, DialogClose, DialogDescription, DialogPopup, DialogPortal, DialogTitle, DialogTrigger, Input } from '@mobtranslate/ui';

export function DialogContainmentExample() {
  const [open, setOpen] = useState(false);
  const [draft, setDraft] = useState('Local collection');
  const [saved, setSaved] = useState('Local collection');
  const [phase, setPhase] = useState<'editing'|'pending'|'failed'>('editing');
  const [error, setError] = useState('');
  const [size, setSize] = useState<'sm'|'md'|'lg'|'xl'>('md');
  const [mode, setMode] = useState<'modal'|'non-modal'|'trap-focus'>('modal');
  const [protect,setProtect]=useState(false);
  const [discard,setDiscard]=useState(false);
  const keepEditing=useRef<HTMLElement>(null);
  const input = useRef<HTMLInputElement>(null);
  const save = useRef<HTMLElement>(null);
  const restoreEditing=useRef(false);
  // Focus the newly visible control during commit, before the popup’s removed-focus fallback.
  useLayoutEffect(()=>{if(discard)keepEditing.current?.focus();else if(restoreEditing.current){restoreEditing.current=false;input.current?.focus();}},[discard]);
  return <div data-dialog-lab className="space-y-4">
    <label>Dialog size <select data-dialog-size className="mt-input" value={size} onChange={event => setSize(event.target.value as typeof size)}>{['sm','md','lg','xl'].map(item => <option key={item}>{item}</option>)}</select></label>
    <label>Dialog interaction mode <select data-dialog-mode className="mt-input" value={mode} onChange={event => setMode(event.target.value as typeof mode)}>{['modal','non-modal','trap-focus'].map(item => <option key={item}>{item}</option>)}</select></label>
    <label className="flex items-center gap-3" style={{minHeight:44}}><input data-dialog-protect type="checkbox" checked={protect} onChange={event=>setProtect(event.target.checked)} style={{width:24,height:24,flexShrink:0}} />Confirm discarding local edits</label>
    <Dialog modal={mode === 'modal' ? true : mode === 'non-modal' ? false : 'trap-focus'} open={open} onOpenChange={(next, details) => {
      if (!next && phase === 'pending') { details.cancel(); return; }
      if(!next&&protect&&draft!==saved){details.cancel();if(discard){restoreEditing.current=true;setDiscard(false);}else setDiscard(true);return;}
      setOpen(next);
      if (next) { setDraft(saved); setPhase('editing'); setError(''); setDiscard(false); }
    }}>
      <DialogTrigger render={<Button type="button" />}>Edit local collection</DialogTrigger>
      <DialogPortal>{mode === 'modal' && <DialogBackdrop />}<DialogPopup size={size} initialFocus={input} className={state => state.open ? 'local-dialog-open' : ''}>
        <DialogTitle>Edit local collection</DialogTitle><DialogDescription>Changes apply to this example only after you confirm a simulated outcome.</DialogDescription>
        <form hidden={discard} className="mt-form mt-4" onSubmit={event => {
          event.preventDefault();
          if (phase === 'pending') return;
          if (!draft.trim()) { setError('Enter a collection name.'); input.current?.focus(); return; }
          setError(''); setPhase('pending');
        }}>
          <label>Collection name<Input ref={input} value={draft} readOnly={phase === 'pending'} aria-invalid={!!error || undefined} aria-describedby={error ? 'local-dialog-error' : undefined} onChange={event => { setDraft(event.target.value); setError(''); }} /></label>
          {error && <p id="local-dialog-error" role="alert">{error}</p>}
          <p role="status">{phase === 'pending' ? 'Local save pending. Choose success or failure below.' : phase === 'failed' ? 'Simulated failure. Your text remains available for retry.' : 'Editing local example; no request will be sent.'}</p>
          <div className="flex flex-wrap justify-end gap-3"><DialogClose disabled={phase === 'pending'} render={<Button type="button" variant="outline" />}>Cancel collection edit</DialogClose><Button ref={save} type="submit" aria-disabled={phase === 'pending' || undefined}>Save local collection</Button></div>
          {phase === 'pending' && <div className="flex flex-wrap gap-3"><Button type="button" onClick={() => { setSaved(draft.trim()); setPhase('editing'); setOpen(false); }}>Simulate save success</Button><Button type="button" variant="secondary" onClick={() => { setPhase('failed'); save.current?.focus(); }}>Simulate save failure</Button><Button type="button" variant="ghost" onClick={() => { setPhase('editing'); save.current?.focus(); }}>Cancel pending local save</Button></div>}
        </form>
        {discard&&<div data-dialog-discard className="mt-4 space-y-4"><p role="status">Discard the unsaved local name “{draft}”? The current collection remains “{saved}”.</p><div className="flex flex-wrap gap-3"><Button ref={keepEditing} type="button" style={{paddingInline:12}} onClick={()=>{restoreEditing.current=true;setDiscard(false);}}>Keep editing</Button><Button type="button" style={{paddingInline:12}} variant="outline" onClick={()=>{setDraft(saved);setDiscard(false);setOpen(false);}}>Discard edits</Button></div></div>}
      </DialogPopup></DialogPortal>
    </Dialog>
    <p role="status">Current local collection: {saved}</p>
  </div>;
}

Root API

Root propInstalled contract
open / defaultOpenControlled boolean / initial uncontrolled boolean; default false.
onOpenChange(open, eventDetails); details.reason describes cause; details.cancel() can reject a change.
onOpenChangeCompleteCalled after open/close animations finish; distinct from a submitted task succeeding.
modalDialog/Drawer: true default; false or trap-focus alternatives. AlertDialog is always modal.
disablePointerDismissalDialog/Drawer: false default. AlertDialog does not expose this prop and prevents outside-pointer dismissal.
actionsRefImperative close/unmount lifecycle; do not hold invisible inert content indefinitely.
handle / triggerId / defaultTriggerIdInstalled primitive trigger association/payload mechanisms; imperative triggers need a meaningful final focus destination.
childrenParts or installed payload render function. Root does not render its own DOM node.

Parts API

Exports: Dialog, DialogTrigger, DialogClose, DialogPortal, DialogBackdrop, DialogPopup, DialogTitle and DialogDescription. DialogPopup adds size=sm|md|lg|xl, md default. Other part contracts come from the pinned @base-ui-components/react 1.0.0-rc.0.

Part / propContract
TriggerInstalled button/render props, disabled, nativeButton, trigger payload/association; button ref. Use render={<Button type="button" />} to compose one control.
CloseInstalled button/render props and button ref; closes the surface, not an application request.
PortalMounts the surface outside local layout; preserve theme variables through its actual container. keepMounted retains the subtree, not an open state.
BackdropDiv/render props, className string or state callback, style and div ref; visible dimming belongs to modality.
PopupDiv/render props and div ref; initialFocus and finalFocus accept boolean, element ref or interaction-type callback.
TitleInstalled heading/render props and heading ref; supplies the visible dialog name.
DescriptionInstalled paragraph/render props and paragraph ref; supplies a concise purpose/message.
className / render / native propsStyled parts retain primitive state callbacks. Custom render must preserve forwarded props, semantics and refs.

Focus, keyboard and dismissal

  • Choose initial focus deliberately: a labelled field for a small edit, a focusable heading for long structured content, or the least destructive action for a consequential choice.
  • Modal Tab/Shift+Tab remain inside; Escape closes when application policy permits. Always include a visible named close or cancel action.
  • Return to the invoking control, or a meaningful enabled alternative when that control is removed or disabled by the result.
  • Do not make long semantic content one flattened description. Use a concise description and retain headings, lists and paragraphs for normal reading.
  • A pending operation can prevent accidental dismissal only when a real cancellation/recovery path remains. Closing the surface is not cancelling a network request.
  • Non-modal use needs an explicit workflow and keyboard route between the surface and page. Do not claim modal blocking for modal=false or pointer blocking for trap-focus. Styled popup wrappers expose aria-modal only for fully modal roots; omit a dimming backdrop for non-modal/focus-only layouts.

Task states and failures

  • Editing: explicit labels, current draft and cancel. Invalid: focus the invalid field and connect its concise error; retain other input.
  • Pending: prevent duplicate submission and retain a real cancellation/outcome path. Do not silently drop a response or claim that closing aborts the operation.
  • Failed: retain the draft, explain the failure and return to an enabled retry action. Complete: apply the actual result before closing and restoring focus.
  • Opening without a name/description, losing theme variables in the portal, or scrolling the page instead of the popup are implementation failures.
  • Programmatically closing after success is different from a close-button press; both must leave an enabled meaningful focus destination.

Verification and release scope

  • Verify the actual title, description relationships, focus destination, keyboard operation and task result in the consuming application.
  • Exercise narrow and short viewports, both themes, long labels, text spacing, reduced motion and forced colors. Reach the last action by scrolling and keyboard.
  • For overlays, check background interaction, scroll locking and each permitted dismissal path. A role or aria-modal attribute alone does not prove modal behavior.
  • Use a named screen reader and browser for spoken output. Local examples and automated DOM checks do not prove announcements or a persisted backend result.
  • Retain source/review/permission evidence. A container or confirmation cannot grant authority or establish that a request succeeded.