MobTranslate styleguide

Action Groups and FAB Menus

Compose related commands, a split primary action and a FAB menu without confusing command execution with selection.

Choose the interaction model

PatternUse and ownership
Button groupRelated independent commands in a named group. Ordinary Tab order; visual adjacency does not add arrow navigation.
Split buttonA visible primary command beside an explicit alternative menu. Explain whether alternatives execute immediately or only select the next default.
FAB menuOne prominent creation launcher opens a small set of related actions. It is a menu trigger, not an extra command run before the menu opens.
Selection / toolbarUse ToggleGroup for committed segments, Radio for one-of-many settings and Toolbar for a tested command keyboard model. Do not mix their roles by styling alone.

Anatomy and dimensions

PartAuthored web pattern
GroupNative role=group with a name, wrapping settings commands and 44px minimum targets. It implies neither mutually exclusive selection nor roving focus.
Split surfaceFlex container, 2px muted-foreground boundary and radius-md. Primary flexes and its label wraps. The 44 CSS px-wide chevron trigger keeps an inline-start divider and the accessible name More actions, leaving room for the primary label at enlarged text.
MenuInstalled Root/Trigger/Portal/Positioner/Popup/Item. Actual trigger emits expanded/menu semantics; 8px offset/collision padding, explicit portal direction and a container within the main page region, outside the clipped split surface.
FABExported icon-only medium FAB with a persistent adjacent creation label and decorative plus. In-flow, trailing alignment within a padded workspace; no fixed overlay covers content.
PendingUnknown-total CircularProgress plus task-specific status. A spinner is not a count, an expressive spring solver or proof of a remote save.

Reference families and web adaptation

The captured Material expressive reference distinguishes standard groups, where pressed neighbors change width, from connected selection groups. It recommends connected groups in place of its baseline segmented-button design. Local command groups preserve ordinary Tab order and stable geometry; use Toolbar for an arrow-navigation command composite, ToggleGroup for installed single/multiple pressed selection, or Radio for a required one-of-many value. Styling adjacent buttons does not add those semantics.

The reference encourages single-line group arrangements and concise labels. This web composition wraps related settings and grows targets when text enlarges; the split pair keeps two independent controls in one bounded surface. It does not implement expressive neighbor resizing, round/square selection morphs or the reference’s five-size/three-width configuration system.

Pattern pointImplemented choice
Split trailing controlA chevron with the accessible name More actions turns 180° when aria-expanded is true. Its fixed CSS-pixel width preserves primary-label space at 320px and 200% text. Native menu state, named menu and collision-aware placement remain authoritative; opening is not a selected document value.
Menu spacing8 CSS px offset/collision padding in this composition. Native reference dp measurements are not silently treated as web pixels.
FAB menu launcherAn icon FAB opens related choices; a persistent adjacent label explains its purpose. The extended FAB is reserved for a directly labelled action, not this menu launcher.
Web versus Android menuThe web reference uses a FAB plus a menu. The installed Menu provides item focus, arrows, Escape and return focus. There is no Android close-button replacement or container-transform animation claim.
Selection and availabilityThe draft settings button uses aria-pressed and a retained value. Menu alternatives execute once and update the next default; unavailable remote publication cannot become a local success.

Working commands and retained draft outcomes

Enter a title and create actual local drafts through the primary action, split menu or FAB menu. Alternatives execute exactly once and become the next primary action. Hold creation to inspect pending, failure, cancellation and explicit completion. Existing drafts and the title remain through recovery.

The source uses only exported primitives and a typed page-memory model. No new SplitButton, ButtonGroup or FABMenu package export is implied. These are compiled web compositions with their own documented task policy.

These explicit local commands create retained page-memory drafts. An alternative menu command runs once and becomes the next primary action. No source or server publication occurs.

The FAB menu offers the same creation tasks in this bounded workspace. It is placed in flow so it cannot cover the reading task.

Create local draft options

No local draft created. 0 local drafts exist.

    The two command groups use ordinary Tab navigation. They are not a selection segment or a toolbar with arrow navigation. The menu supplies its installed keyboard/dismissal contract.

    tsx
    'use client';
    import {useRef,useState,type CSSProperties} from 'react';
    import {Button,Input,CircularProgress,FloatingActionButton,Menu,MenuTrigger,MenuPortal,MenuPositioner,MenuPopup,MenuItem,DirectionProvider} from '@mobtranslate/ui';
    const action:CSSProperties={minHeight:44,height:'auto',minWidth:0,maxWidth:'100%',whiteSpace:'normal',overflowWrap:'anywhere'};
    type Kind='reading'|'comparison';
    type Draft={id:number;kind:Kind;title:string};
    export function ActionGroupsExample(){
      const portalContainer=useRef<HTMLDivElement>(null);
      const [title,setTitle]=useState('English-only local draft');
      const [kind,setKind]=useState<Kind>('reading');
      const [drafts,setDrafts]=useState<Draft[]>([]);
      const [hold,setHold]=useState(false);
      const [pending,setPending]=useState<Draft|null>(null);
      const [message,setMessage]=useState('No local draft created.');
      const [error,setError]=useState('');
      const [rtl,setRtl]=useState(false);
      const input=useRef<HTMLInputElement>(null);
      const arrival=useRef<HTMLParagraphElement>(null);
      const nextId=useRef(0);
      const menuFocus=useRef<'trigger'|'input'|'status'>('trigger');
      function request(nextKind:Kind){
        if(pending)return;
        if(!title.trim()){menuFocus.current='input';setError('Enter a local draft title.');input.current?.focus();return;}
        setError('');setKind(nextKind);
        const snapshot={id:++nextId.current,kind:nextKind,title:title.trim()};
        if(hold){menuFocus.current='status';setPending(snapshot);setMessage(`Local ${nextKind} creation pending for “${snapshot.title}”.`);arrival.current?.focus();}
        else{setDrafts(previous=>[...previous,snapshot]);setMessage(`Created local ${nextKind} draft “${snapshot.title}”.`);}
      }
      function resolve(outcome:'complete'|'fail'|'cancel'){
        if(!pending)return;
        if(outcome==='complete'){setDrafts(previous=>[...previous,pending]);setMessage(`Created local ${pending.kind} draft “${pending.title}”.`);}
        else setMessage(`Local draft creation ${outcome==='fail'?'failed; correct the title or retry the chosen action':'cancelled'}. Existing drafts and title remain.`);
        setPending(null);arrival.current?.focus();
      }
      function options(fab:boolean){return <Menu disabled={!!pending} onOpenChange={next=>{if(next)menuFocus.current='trigger';}}>
        {fab&&<span id="action-fab-label" lang="en" dir="ltr">Create local draft options</span>}
        <MenuTrigger id={fab?'action-fab-trigger':'action-split-trigger'} render={fab?<FloatingActionButton size="md" disabled={!!pending} aria-label="Create local draft options" aria-labelledby="action-fab-label"><svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 12h14M12 5v14" fill="none" stroke="currentColor" strokeWidth="2" /></svg></FloatingActionButton>:<Button type="button" variant="ghost" aria-label="More actions" disabled={!!pending} style={{...action,width:44,flexShrink:0,paddingInline:0,borderInlineStart:'2px solid var(--color-muted-foreground)'}}><svg data-split-chevron width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" focusable="false" style={{flexShrink:0}}><path d="m6 9 6 6 6-6" fill="none" stroke="currentColor" strokeWidth="2" /></svg></Button>} />
        <MenuPortal container={portalContainer}><MenuPositioner sideOffset={8} collisionPadding={8}><MenuPopup finalFocus={()=>menuFocus.current==='input'?input.current:menuFocus.current==='status'?arrival.current:true} dir={rtl?'rtl':'ltr'} aria-label={fab?'Local FAB creation options':'Alternative local creation actions'}>
          <MenuItem onClick={()=>request('reading')}>Add reading draft{fab?' from FAB':''}</MenuItem>
          <MenuItem onClick={()=>request('comparison')}>Add comparison draft{fab?' from FAB':''}</MenuItem>
          <MenuItem disabled>Publish remotely unavailable</MenuItem>
        </MenuPopup></MenuPositioner></MenuPortal>
      </Menu>;}
      return <DirectionProvider direction={rtl?'rtl':'ltr'}><div ref={portalContainer} data-action-groups-lab lang="en" dir={rtl?'rtl':'ltr'} className="space-y-4">
        <p dir="auto">These explicit local commands create retained page-memory drafts. An alternative menu command runs once and becomes the next primary action. No source or server publication occurs.</p>
        <label htmlFor="action-group-title" className="block">Local draft title</label><Input ref={input} id="action-group-title" value={title} readOnly={!!pending} aria-invalid={!!error} aria-describedby={error?'action-group-error':undefined} onChange={event=>setTitle(event.target.value)} />
        {error&&<p id="action-group-error" role="alert" dir="auto">{error}</p>}
        <div role="group" aria-label="Local draft creation" className="material-split">
          <Button id="action-primary" type="button" variant="ghost" disabled={!!pending} style={{...action,flex:1}} onClick={()=>request(kind)}>Add {kind} draft</Button>{options(false)}
        </div>
        <div role="group" aria-label="Local draft settings" className="flex flex-wrap gap-3"><Button style={action} type="button" variant="outline" disabled={!!pending} aria-pressed={hold} onClick={()=>setHold(value=>!value)}>Hold local draft creation</Button><Button style={action} type="button" variant="outline" aria-pressed={rtl} onClick={()=>setRtl(value=>!value)}>Use RTL action groups</Button></div>
        <div className="material-fab-space"><p dir="auto">The FAB menu offers the same creation tasks in this bounded workspace. It is placed in flow so it cannot cover the reading task.</p><div className="flex flex-wrap items-center justify-end gap-3">{options(true)}</div></div>
        {pending&&<div className="flex flex-wrap gap-3"><CircularProgress value={null} aria-label="Local draft creation progress" /><Button style={action} type="button" onClick={()=>resolve('complete')}>Complete local draft creation</Button><Button style={action} type="button" variant="outline" onClick={()=>resolve('fail')}>Fail local draft creation</Button><Button style={action} type="button" variant="outline" onClick={()=>resolve('cancel')}>Cancel local draft creation</Button></div>}
        <p ref={arrival} tabIndex={-1} role="status" dir="auto" data-action-groups-status>{message} {drafts.length} local drafts exist.</p>
        <ol data-action-drafts className="guidance-list">{drafts.map(draft=><li key={draft.id} dir="auto">{draft.kind}: {draft.title}</li>)}</ol>
        <p dir="auto">The two command groups use ordinary Tab navigation. They are not a selection segment or a toolbar with arrow navigation. The menu supplies its installed keyboard/dismissal contract.</p>
      </div></DirectionProvider>;
    }
    

    Command and selection state

    • Keep the default command separate from the created records. Choosing an alternative does not retroactively change earlier drafts.
    • The primary action repeats the current default with the current title. The title is validated before a request snapshot exists; whitespace-only titles focus the linked invalid field.
    • While pending, title editing and both creation launchers are unavailable. Completion appends only the held snapshot; failure/cancel append nothing. Duplicate creation cannot be requested through disabled controls.
    • After failure, correct the retained title or retry the chosen action. The status receives focus after the pending outcome controls disappear. No server publication is inferred.

    Keyboard, dismissal and touch

    • Tab reaches the primary command and menu launcher as distinct controls. Enter/Space activate the native command contract; installed Menu supplies its arrow/typeahead/Escape behavior.
    • Escape from a menu dismisses without creating a draft and returns to its surviving launcher. Selecting a command closes the menu; verify focus when a pending launcher becomes disabled.
    • A FAB menu must remain available through a usable native button, not hover alone. Disabled remote publication remains visibly unavailable and never becomes a local success.
    • Keep a real, visible command label. The plus is decorative; the launcher name describes opening options rather than claiming that it creates a draft immediately.

    Adaptive placement and direction

    • Let long labels wrap before shrinking target size or hiding essential meaning. The split pair remains one bounded surface, while the two buttons remain independent controls.
    • Keep a fixed FAB clear of reading content, scrollbars, keyboards, safe areas and other overlays if a product chooses fixed placement. This verified example keeps it in flow.
    • Use logical divider/alignment plus installed DirectionProvider and explicit portal dir. English-only status uses automatic text direction inside RTL geometry.
    • Do not position the menu by a static screen coordinate. Verify actual collision behavior and all items at narrow widths, zoom and changed labels.

    API and composition contract

    ConcernActual contract
    Primary / groupsButton type=button, disabled and native handler; group name/structure owned by composition. See complete Button/Toolbar/ToggleGroup APIs.
    Menu launcherMenuTrigger render={<Button …/>} or render={<FloatingActionButton …/>}; preserve primitive props/ref/handlers on the single rendered target.
    Menu commandsMenuItem onClick executes the task; disabled item is unavailable. Root disabled gates opening during pending.
    FAB optionsFloatingActionButton size=md with an action name and adjacent visible purpose; direct exported Button behavior. Layout owns placement, safe areas and obstruction.
    LoadingCircularProgress value=null represents unknown total; surrounding text and actual result model distinguish completion/failure/cancel.

    Failure cases and verification

    • Avoid two nested buttons, a primary action that also opens the menu, unlabeled icon-only launchers, or a selection checkmark that suggests an action did not run.
    • Run test:material-patterns for actual records, chosen default, validation, pending duplicate rejection, completion/failure/cancel, dismissal/focus, both themes and 320px RTL bounds.
    • Test remote authorization, partial failures, real persistence and spoken assistive technology independently when consuming the pattern. The local workflow does not certify those systems.