App Bars
Keep location and contextual actions visible within a workspace while preserving global navigation, reading clearance and retained task state.
Location and action hierarchy
An app bar identifies the current workspace and presents a small number of relevant actions. It is different from global site navigation, a command toolbar or a decorative heading. Back must return to a meaningful previous context; overflow contains secondary commands.
The checked web pattern uses a native header inside a bounded scrolling workspace. It is not a new AppBar export, a native mobile status bar or an implementation of every Material collapsing/scroll behavior.
Anatomy and measured specifications
| Part | Actual geometry and state |
|---|---|
| Workspace | Native overflow region with a 360px minimum and at least 8rem beyond the measured header height, a readable 2px boundary, radius-lg and explicit name/focus. Enlarged headers cannot consume its entire content viewport. |
| Sticky header | position:sticky; top:0, local z-index:1, 12px padding/gap, opaque background and a 2px lower boundary. It wraps to actual available width rather than asserting a fixed height. |
| Leading / title | Back is a native button available in detail context; title identifies the real selected collection, uses text-lg and a 12rem flex basis, taking a separate row when the leading control would squeeze it. |
| Trailing actions | Save snapshots the selected collection’s note. Workspace options is an installed Menu trigger with checked density/scope settings. All action targets remain at least 44px. |
| Reading clearance | ResizeObserver measures actual header height, writes local scroll padding and disconnects on unmount. Detail arrival updates the measurement before focusing/scrolling its real heading. |
Material app-bar variants and scroll decisions
The expressive reference offers search, small, medium flexible and large flexible app bars. Flexible variants replace the older medium/large forms, support subtitles and wrapped headings, and can compress into small bars on scroll; the former center-aligned variant becomes a text-alignment configuration. This local composition is one measured, wrapped sticky header, not a native size-variant, search-view or compress-transition API.
Use one or two essential page actions, with Back/Menu as a clear leading navigation control; use Toolbar for many actions. The reference advises avoiding an overflow menu where possible but also allows trailing actions to collapse into overflow at narrow widths. This example keeps Save as its page action and uses a named menu for two workspace settings. It does not claim those two pieces of advice are identical.
| Reference point | Authored web choice |
|---|---|
| Title, subtitle and media | A real current-collection title updates with content. Flexible reference headings can wrap; a local wrapped header grows to preserve all text at 200%. No subtitle, logo/avatar or filled/wide icon-button configuration is implemented here. A logo must not masquerade as a menu opener. |
| Search app bar | Use a named Search entry point that opens an actual search view when search dominates the product. The reference caps trailing actions by available space and gives its search container native 312dp/50% guidance. This composition has no search-field shortcut; the separate Search pattern implements actual HTTP search and recovery. |
| Scroll fill and access | The header begins on the background surface and changes to muted when this actual workspace scrollTop is positive. It stays sticky within that region, retains an opaque boundary and does not hide actions or add a scroll shadow. There is no screen-reader detection, transparent floating-button or hide-on-scroll behavior. |
| Geometry, direction and focus | Use the full local workspace width and logical edges. Native fixed heights and shape/typography roles are reference decisions, not copied CSS numbers. ResizeObserver reserves the actual wrapped height before scrolling arrival; keyboard Tab reaches ordinary controls, with named actions and visible rings. |
| Color and contrast | The reference uses surface/surface-container and permits some search-label comparisons at 3:1. This site still requires 4.5:1 ordinary text and 3:1 meaningful graphics against actual adjacent surfaces. Foreground text stays readable across the scroll-fill change. |
| Menu ownership | The installed settings popup portals outside the scrolling workspace into this example’s main-page container. It retains web menu focus/dismissal behavior and explicit direction without clipping behind the sticky header or leaving content outside landmarks. |
Working collection context and note snapshots
Open any of three English-only local collections, edit its separate retained note and snapshot it through the actual app-bar action. Back restores the originating collection control. Open another collection and verify independent drafts. Overflow changes real row density and visible scope guidance.
No local note snapshot saved.
'use client';
import {useEffect,useRef,useState,type CSSProperties} from 'react';
import {Button,Input,Menu,MenuTrigger,MenuPortal,MenuPositioner,MenuPopup,MenuCheckboxItem,MenuCheckboxItemIndicator,DirectionProvider} from '@mobtranslate/ui';
const records=[{id:'reading',title:'Local reading collection'},{id:'comparison',title:'Local comparison collection'},{id:'drafts',title:'Local draft collection'}];
const action:CSSProperties={minHeight:44,height:'auto',minWidth:0,maxWidth:'100%',paddingInline:12,whiteSpace:'normal',overflowWrap:'anywhere'};
export function AppBarExample(){
const [opened,setOpened]=useState<string|null>(null);
const [notes,setNotes]=useState<Record<string,string>>({});
const [result,setResult]=useState('No local note snapshot saved.');
const [compact,setCompact]=useState(false);
const [scope,setScope]=useState(true);
const [rtl,setRtl]=useState(false);
const [scrolled,setScrolled]=useState(false);
const portalContainer=useRef<HTMLDivElement>(null);
const bar=useRef<HTMLElement>(null);
const workspace=useRef<HTMLDivElement>(null);
const heading=useRef<HTMLHeadingElement>(null);
const opener=useRef<HTMLElement|null>(null);
const current=records.find(record=>record.id===opened);
useEffect(()=>{const node=bar.current;if(!node)return;const observer=new ResizeObserver(()=>workspace.current?.style.setProperty('--local-appbar-height',`${Math.ceil(node.getBoundingClientRect().height)}px`));observer.observe(node);return()=>observer.disconnect();},[]);
useEffect(()=>{if(bar.current)workspace.current?.style.setProperty('--local-appbar-height',`${Math.ceil(bar.current.getBoundingClientRect().height)}px`);if(opened){heading.current?.focus({preventScroll:true});heading.current?.scrollIntoView({block:'nearest'});}else {opener.current?.focus({preventScroll:true});opener.current?.scrollIntoView({block:'nearest'});}},[opened]);
return <DirectionProvider direction={rtl?'rtl':'ltr'}><div ref={portalContainer} data-app-bar-lab lang="en" className="space-y-4">
<Button type="button" variant="outline" style={action} aria-pressed={rtl} onClick={()=>setRtl(value=>!value)}>Use RTL app bar workspace</Button>
<div ref={workspace} data-app-bar-workspace onScroll={event=>setScrolled(event.currentTarget.scrollTop>0)} dir={rtl?'rtl':'ltr'} role="region" aria-label="Local app bar workspace" tabIndex={0} className="material-appbar-workspace">
<header ref={bar} data-local-app-bar data-scrolled={scrolled||undefined} className="material-appbar">
<Button type="button" variant="outline" style={action} disabled={!current} onClick={()=>setOpened(null)}>Back to collections</Button>
<h3 className="material-appbar-title" dir="auto">{current?.title??'Local collections'}</h3>
<Button type="button" variant="outline" style={action} disabled={!current} onClick={()=>setResult(`Actual local snapshot for ${current?.title}: ${notes[opened??'']||'(empty note)'}.`)}>Save local note</Button>
<Menu><MenuTrigger id="appbar-options" render={<Button type="button" variant="outline" style={action}>Workspace options</Button>} />
<MenuPortal container={portalContainer}><MenuPositioner sideOffset={8} collisionPadding={8}><MenuPopup dir={rtl?'rtl':'ltr'} aria-label="Local workspace options"><MenuCheckboxItem checked={compact} onCheckedChange={setCompact}><MenuCheckboxItemIndicator aria-hidden="true">✓</MenuCheckboxItemIndicator>Compact collection rows</MenuCheckboxItem><MenuCheckboxItem checked={scope} onCheckedChange={setScope}><MenuCheckboxItemIndicator aria-hidden="true">✓</MenuCheckboxItemIndicator>Show workspace scope</MenuCheckboxItem></MenuPopup></MenuPositioner></MenuPortal>
</Menu>
</header>
<div className="material-appbar-content">
{scope&&<p dir="auto">Three English-only local collections. This workspace and its app bar scroll independently from the documentation header.</p>}
<div hidden={!!current}><ul data-appbar-list className="space-y-3">{records.map(record=><li key={record.id}><Button style={{...action,width:'100%',textAlign:'start',paddingBlock:compact?8:16}} type="button" variant="outline" onClick={event=>{opener.current=event.currentTarget;setOpened(record.id);}}>Open {record.title}</Button></li>)}</ul></div>
<div hidden={!current}><h4 ref={heading} tabIndex={-1} data-appbar-heading dir="auto" className="font-semibold">{current?.title} note</h4><label htmlFor="appbar-note" className="block">Retained local collection note</label><Input id="appbar-note" value={notes[opened??'']??''} onChange={event=>setNotes(previous=>({...previous,[opened??'']:event.target.value}))} /><p dir="auto">Saving snapshots this collection’s actual note. Back retains every collection’s separate draft and restores the originating control.</p></div>
{Array.from({length:6},(_,index)=><p className="mt-4" dir="auto" key={index}>Local workspace reading context {index+1}. Use the named region to scroll; the app bar stays within its own workspace and does not replace global site navigation.</p>)}
</div>
</div>
<p role="status" dir="auto" data-appbar-result>{result}</p>
</div></DirectionProvider>;
}
Navigation, focus and scroll lifetime
- Opening detail changes the actual location/title and focuses a real detail heading after arrival. Back returns to the visible originating control without erasing other collection drafts.
- Keep global and local navigation scopes distinct. A local Back action here does not promise browser history/URL routing; use a real destination link/router contract when the product requires it.
- Sticky position is bounded by its scrolling ancestor. Reserve clearance from the measured wrapped height, including zoom/locale changes; a fixed guessed offset can hide arrival focus.
- The workspace remains native-scrollable. The bar does not steal focus simply because someone scrolls, and its overflow menu follows the installed focus/dismissal model.
Contextual actions and outcomes
- Save is unavailable without a selected collection. It snapshots that ID’s current note, with explicit empty-note representation. It does not save a hidden neighboring field or imply server persistence.
- Per-record drafts and last successful result have separate ownership. Back or a density change retains both; opening a different record does not relabel the prior result as its own.
- When a real save is pending, define duplicate rejection, availability, failure/cancel and stale-response policy. Do not announce a queued request as an applied result.
- Keep source review, permission and publication outcomes separate from an app-bar Save label. This fixture uses only page-memory English notes.
Adaptation, variants and semantics
- The example demonstrates an opaque wrapped sticky top bar. Products can choose static, compact or larger title arrangements, but must measure their actual clearance and preserve action names/targets.
- Avoid hiding Back or a required save action only to preserve a single row. Move secondary actions into a named overflow menu and let long titles wrap.
- Use logical layout and explicit portal direction; text language and workspace direction remain independent. A title is not a button unless it has a real navigation action.
- The header is not given an invented toolbar keyboard model. Its ordinary native controls retain Tab/Enter/Space behavior and the menu supplies arrow navigation.
API and implementation
| Owner | Native/exported contract |
|---|---|
| Header / region | HTML header and named div region, CSS sticky/overflow. No package AppBar component or undocumented fixed-size API. |
| Back / Save | Exported Button with native type/disabled/handler. Record state, history scope and actual save outcome remain application-owned. |
| Overflow | Menu Root/Trigger/Portal/Positioner/Popup plus MenuCheckboxItem/Indicator. Reflect checked state in the real density/scope model. |
| Measurement / focus | ResizeObserver cleanup; local refs for header, scrolling ancestor, arrival heading and surviving originating control. Use observed geometry, not a viewport-global magic offset. |
Failure cases and acceptance
- Reject transparent overlays hiding content, global sticky bars inside unrelated scroll containers, Back controls that erase drafts and title changes without an actual destination.
- Run test:material-patterns for real note snapshots, independent retained notes, Back/arrival focus, checked overflow effects, measured sticky position, 320px wrapping and both themes/directions.
- Test actual browser history, onscreen keyboards, safe areas, hardware scrolling and spoken assistive technology in the intended consuming product.