MobTranslate styleguide

Tooltip

Offer brief noninteractive context without making the tooltip a control’s only name or instruction.

Supplemental context, persistent essentials

Use Tooltip for brief contextual text on hover/focus. The real trigger needs its own accessible name, and essential instructions need persistent placement for touch and other input modes. An icon-only control still needs its own name when hints are disabled.

Do not put links, fields or recovery actions inside a tooltip. Use Popover or visible help for interactive content. Do not advertise a keyboard shortcut that the application has not implemented. The previous Save hint implied an unsupported Ctrl+S command; this example states the actual page-memory effect.

Anatomy and visual contract

PartSpecification
ProviderShared delay group; example sets opening delay=200ms and closeDelay=100ms. Adjacent tooltip behavior uses the installed provider policy.
TriggerDirect installed button/render part with actual name, target and action supplied by caller. Example icon is decorative in a named 44px Button.
Positioner / PopupPortalled supporting text with 8px example offset/collision padding. Shared popup uses inverted foreground/background roles, 14px text, 6px × 12px padding, radius-md and no wide shadow/fade.
BoundsMaximum width min(20rem, viewport−16px), available-height scrolling and wrapping. Long content should become visible guidance rather than a scrolling tooltip task.
ArrowAutomatic installed Arrow; visual hint only, not an accessible label or action.
StateFocus and hover can disclose; Escape dismisses. Hint availability is separate from whether the bookmark action works.

Material plain and rich tooltip boundaries

Plain reference tooltips briefly explain icon actions; rich variants can add a heading, longer explanation and up to two actions, including persistent click/onboarding presentations. On this web stack, plain Tooltip remains noninteractive supplemental description. Interactive rich help belongs in a named Popover or visible content with its own deliberate focus/dismissal model, not links or buttons inside role=tooltip.

Reference decisionLocal web contract and application choice
Native dimensions and timingReference plain geometry includes 24dp height/8dp padding; rich uses 12dp top,8dp bottom and 16dp inline padding. Typical target distances are 4/8dp with 8dp collision increments and 1.5s after leaving. These differ from the actual local popup, delay/provider and continuous collision contracts specified above.
Name and essential contentThe control needs its own meaningful accessible name, especially for touch when no hint opens. Keep required instructions, errors and recovery persistent. The reference’s suggestion to use a dialog for crucial information is not a requirement to interrupt every help task.
Hover, focus and dismissalVerify keyboard-focus and pointer disclosure, Escape without activation, pointer transfer onto the popup and an inspectable dismissal window. The fixture explicitly tests a noninteractive hint; native mobile long-press and unsolicited onboarding are not installed or certified.
Rich content and focusA deliberate rich Popover may contain clear links/actions and retain normal reading order. Match its actual modality/final focus and do not copy the reference’s generic Tooltip role onto interactive content. Keep the trigger and important content unobscured, and do not introduce a focus trap just because a help surface appears.
One hint and readable textA shared provider owns sibling delay/open behavior. A consumer using multiple providers needs to check competing hints rather than assuming a global singleton. Reference plain/rich headings favor single lines; this web popup wraps within actual viewport bounds at enlarged text rather than truncating essential meaning.

Actual bookmark with optional hint

The named icon button toggles an actual English-only page-memory bookmark and reports its result. Its persistent explanation remains available when tooltip hints are disabled. The hint contains only supplemental text, with no fake shortcut or persistence claim.

Toggle the English-only bookmark in page memory. This instruction remains available without a tooltip.

Local bookmark absent.

tsx
'use client';
import {useRef,useState,type CSSProperties} from 'react';
import {Accordion,AccordionItem,AccordionHeader,AccordionTrigger,AccordionPanel,Collapsible,CollapsibleTrigger,CollapsiblePanel,Popover,PopoverTrigger,PopoverPortal,PopoverPositioner,PopoverPopup,PopoverTitle,PopoverDescription,PopoverClose,TooltipProvider,Tooltip,TooltipTrigger,TooltipPortal,TooltipPositioner,TooltipPopup,Button,Input,Textarea} from '@mobtranslate/ui';
const action:CSSProperties={minHeight:44,minWidth:0,maxWidth:'100%',height:'auto',whiteSpace:'normal',overflowWrap:'anywhere'};
export function AccordionTaskExample(){
  const [expanded,setExpanded]=useState<string[]>(['notes']);
  const [multiple,setMultiple]=useState(true);
  const [disabled,setDisabled]=useState(false);
  const [draft,setDraft]=useState('English-only retained reading note.');
  const [loaded,setLoaded]=useState('No note applied.');
  return <div data-accordion-lab className="space-y-4"><div className="flex flex-wrap gap-3"><Button type="button" style={action} variant="outline" aria-pressed={multiple} onClick={()=>{setMultiple(v=>!v);setExpanded(current=>multiple?current.slice(0,1):current);}}>Allow multiple local sections</Button><Button type="button" style={action} variant="outline" aria-pressed={disabled} onClick={()=>setDisabled(v=>!v)}>Disable local section disclosure</Button></div>
    <Accordion value={expanded} onValueChange={setExpanded} multiple={multiple} disabled={disabled} className={state=>state.disabled?'disclosure-unavailable':'disclosure-available'}>
      <AccordionItem value="notes"><AccordionHeader><AccordionTrigger>Local reading notes</AccordionTrigger></AccordionHeader><AccordionPanel><div className="p-4 space-y-3"><label htmlFor="accordion-note" className="block">Retained English-only note</label><Textarea id="accordion-note" value={draft} onChange={event=>setDraft(event.target.value)} /><Button style={action} type="button" onClick={()=>setLoaded(draft)}>Apply local note preview</Button></div></AccordionPanel></AccordionItem>
      <AccordionItem value="help"><AccordionHeader><AccordionTrigger>Local reading instructions</AccordionTrigger></AccordionHeader><AccordionPanel><p className="p-4">Opening this section does not save or submit the retained note. Closing its editor does not discard parent-owned text.</p></AccordionPanel></AccordionItem>
      <AccordionItem value="restricted" disabled><AccordionHeader><AccordionTrigger>Unavailable local section</AccordionTrigger></AccordionHeader><AccordionPanel><p>This unavailable fixture cannot be opened.</p></AccordionPanel></AccordionItem>
    </Accordion><p data-accordion-expanded>Expanded: {expanded.join(', ')||'none'}</p><p data-accordion-loaded role="status">{loaded}</p>
  </div>;
}
export function CollapsibleTaskExample(){
  const [open,setOpen]=useState(false);
  const [disabled,setDisabled]=useState(false);
  const [note,setNote]=useState('English-only optional context.');
  const [payload,setPayload]=useState('No native submission.');
  const trigger=useRef<HTMLButtonElement>(null);
  return <div data-collapsible-lab className="space-y-4"><Button type="button" style={action} variant="outline" aria-pressed={disabled} onClick={()=>setDisabled(v=>!v)}>Disable optional-context disclosure</Button>
    <form className="space-y-4" onSubmit={event=>{event.preventDefault();setPayload(JSON.stringify([...new FormData(event.currentTarget).entries()]));}}><label htmlFor="collapsible-title" className="block">Always visible local title</label><Input id="collapsible-title" name="title" defaultValue="English-only local draft" required /><input type="hidden" name="context" value={note} />
      <Collapsible open={open} onOpenChange={setOpen} disabled={disabled}><CollapsibleTrigger ref={trigger} render={<Button type="button" style={action} variant="outline" />} className={state=>state.open?'context-open':'context-closed'}>Optional local context</CollapsibleTrigger><CollapsiblePanel><div className="p-4 space-y-3"><label htmlFor="collapsible-note" className="block">Optional English-only context</label><Textarea id="collapsible-note" value={note} onChange={event=>setNote(event.target.value)} /><Button type="button" style={action} variant="outline" onClick={()=>{setOpen(false);trigger.current?.focus();}}>Close context and retain text</Button></div></CollapsiblePanel></Collapsible>
      <Button type="submit" style={action}>Submit local context draft</Button></form><p role="status" data-collapsible-payload style={{overflowWrap:'anywhere'}}>{payload}</p>
    <p>The parent-owned context is registered once through a hidden field, even when its optional editor is unmounted. Disclosure availability does not authorize or disable the form.</p>
  </div>;
}
export function PopoverTaskExample(){
  const [open,setOpen]=useState(false);
  const [draft,setDraft]=useState('16');
  const [applied,setApplied]=useState(16);
  const [requested,setRequested]=useState<number | null>(null);
  const [error,setError]=useState('');
  const [status,setStatus]=useState('Initial 16px local reading preview.');
  const [rtl,setRtl]=useState(false);
  const input=useRef<HTMLInputElement>(null);
  const trigger=useRef<HTMLButtonElement>(null);
  function resolve(result:'complete'|'fail'|'cancel'){
    if(requested===null)return;
    if(result==='complete'){setApplied(requested);setStatus('Actual local reading size applied. No durable setting saved.');setOpen(false);}
    else setStatus(`Local reading request ${result==='fail'?'failed':'cancelled'}. Draft and loaded preview remain; retry is available.`);
    setRequested(null);
  }
  return <div data-popover-lab className="space-y-4" dir={rtl?'rtl':'ltr'}><Button type="button" style={action} variant="outline" aria-pressed={rtl} onClick={()=>setRtl(v=>!v)}>Use RTL reading settings</Button>
    <Popover open={open} onOpenChange={next=>{setOpen(next);if(next){setDraft(String(applied));setError('');}else if(requested!==null){setRequested(null);setStatus('Dismissed local request cancelled. Loaded reading size remains.');}}}>
      <PopoverTrigger id="popover-reading-trigger" ref={trigger} render={<Button type="button" style={action} variant="outline" />}>Local reading settings</PopoverTrigger>
      <PopoverPortal><PopoverPositioner sideOffset={8} collisionPadding={8}><PopoverPopup dir={rtl?'rtl':'ltr'} initialFocus={input} finalFocus={interaction=>interaction==='keyboard'?trigger.current:true} className={state=>state.open?'reading-popup-open':'reading-popup-closed'}>
        <PopoverTitle>Local reading size</PopoverTitle><PopoverDescription>Choose 12–24 whole pixels. Applying changes this page-memory preview only.</PopoverDescription>
        <form className="mt-3 space-y-3" noValidate onSubmit={event=>{event.preventDefault();if(requested!==null)return;if(!input.current?.value || !input.current.validity.valid){setError('Enter a whole reading size from 12 through 24.');input.current?.focus();return;}setRequested(input.current.valueAsNumber);setError('');setStatus('Local reading size requested; preview remains until completion.');}}>
          <label htmlFor="popover-reading-size" className="block">Reading size in pixels</label><Input ref={input} id="popover-reading-size" type="number" name="size" required min={12} max={24} step={1} value={draft} onChange={event=>{setDraft(event.target.value);setError('');}} readOnly={requested!==null} aria-invalid={!!error} aria-describedby={error?'popover-reading-error':undefined} />
          {error && <p id="popover-reading-error" role="alert">{error}</p>}<div className="flex flex-wrap gap-3"><Button type="submit" style={action} disabled={requested!==null}>Request local reading size</Button><PopoverClose render={<Button type="button" style={action} variant="outline" />}>Close reading settings</PopoverClose></div>
        </form>
        {requested!==null && <div className="mt-3 flex flex-wrap gap-3"><Button type="button" style={action} onClick={()=>resolve('complete')}>Complete reading size</Button><Button type="button" style={action} variant="outline" onClick={()=>resolve('fail')}>Fail reading size</Button><Button type="button" style={action} variant="outline" onClick={()=>resolve('cancel')}>Cancel reading request</Button></div>}
        <p>{status}</p>
      </PopoverPopup></PopoverPositioner></PopoverPortal>
    </Popover><p data-popover-preview style={{fontSize:applied,overflowWrap:'anywhere'}}>Actual English-only reading sample at {applied}px.</p><p role="status" data-popover-status>{status}</p>
  </div>;
}
export function TooltipTaskExample(){
  const [saved,setSaved]=useState(false);
  const [tips,setTips]=useState(true);
  return <div data-tooltip-lab className="space-y-4"><p id="tooltip-persistent-help">Toggle the English-only bookmark in page memory. This instruction remains available without a tooltip.</p><Button type="button" style={action} variant="outline" aria-pressed={tips} onClick={()=>setTips(v=>!v)}>Enable local bookmark hint</Button>
    <TooltipProvider delay={200} closeDelay={100}><Tooltip disabled={!tips}><TooltipTrigger id="tooltip-bookmark-trigger" aria-describedby="tooltip-persistent-help" render={<Button type="button" size="icon" variant="outline" aria-label="Toggle local bookmark" aria-pressed={saved} onClick={()=>setSaved(v=>!v)} />}><svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill={saved?'currentColor':'none'} stroke="currentColor" strokeWidth="2"><path d="M6 3h12v18l-6-4-6 4z" /></svg></TooltipTrigger><TooltipPortal><TooltipPositioner sideOffset={8} collisionPadding={8}><TooltipPopup className={state=>state.open?'bookmark-hint-open':'bookmark-hint-closed'}>Keeps a bookmark for this English-only local task.</TooltipPopup></TooltipPositioner></TooltipPortal></Tooltip></TooltipProvider><p role="status" data-tooltip-result>{saved?'Local bookmark present.':'Local bookmark absent.'}</p>
  </div>;
}

Exported API and delay ownership

Export / propInstalled contract
TooltipProviderDirect Provider: delay, closeDelay, timeout (default 400ms grouping window) and children.
TooltipDirect Root: open/defaultOpen=false, onOpenChange(open, details), onOpenChangeComplete and disabled. disableHoverablePopup=false retains hoverable supporting content.
trackCursorAxisInstalled none | x | y | both positioning policy, default none. Cursor tracking must not make the content impossible to inspect.
triggerId / defaultTriggerId / handle / childrenInstalled external trigger/payload association. Controlled openness needs the correct trigger identity; advanced compositions require their own acceptance tests.
actionsRefInstalled custom unmount ownership. Complete cleanup if taking over the popup lifecycle.
TooltipTriggerDirect button/render part, native action/ref, payload/handle and optional per-trigger delay (default 600ms) / closeDelay (default 0ms); provider can supply shared delay.
TooltipPortal / TooltipPositionerDirect portal and styled positioning div/ref with installed side/align/offset/collision options.
TooltipPopupStyled div/ref with state class callback and automatic Arrow. Its content is supplemental description, not an interactive menu or focus destination.

Pointer, keyboard and accessibility

  • Verify both hover and keyboard focus disclosure. Escape dismisses without moving focus into the tooltip or performing the trigger action.
  • Allow people to move the pointer onto supplemental content without dismissing it immediately. Hoverability, dismissal and persistence need actual input tests, not just open DOM.
  • Keep the trigger name and essential visible instruction independent of a tooltip-generated description. Supply id and additional aria-describedby on TooltipTrigger. A render child ID can differ from the primitive’s registered identity, and a child description overrides the composed link in this pin. Verify composed aria-describedby relationships in the actual rendered tree. The APG tooltip pattern is a draft; its role/description and interaction guidance is a reference, not a blanket certification.
  • Do not require hover to discover errors, authorization or meaning. Touch users need the named action and persistent alternative; desktop mouse emulation is not hardware touch proof.
  • A disabled trigger may not receive focus. Explain unavailable actions visibly; enabling a wrapper tooltip is not a substitute for accessible recovery.

Verification and limits

Run test:disclosure, checked-source/token freshness, lint, UI/styleguide types, production build and the route/fragment crawl. Browser checks establish actual DOM, keyboard and task behavior for these fixtures; they do not certify every device, spoken announcement or consuming application.

  • Check the actual bookmark result, focus/hover disclosure, Escape, pointer transfer, hint-disabled action, accessible naming/description, both-theme text contrast and narrow bounds. This pinned dependency originally omitted role/ID wiring; the reproducible CJS/ESM patch restores tooltip relationships and composes an existing described-by value.
  • Test several provider siblings, custom delay, anchor removal, cursor tracking and spoken announcements in their actual consumer. The fixture establishes one noninteractive hint, not certification of every advanced mode.