# Consumer interactions and navigation

Icon dismiss controls, queued feedback, marked sliders, responsive navigation and purposeful interactions.

## Purpose and composition

These examples use the public Faye UI package to deliver the remaining ordinary UI functions from the Material UI comparison. The implementation and APIs belong to Faye UI. Explore the interactive weekend example below with keyboard, pointer, and touch.

## Common dismiss controls

`Dialog.Close`, `Drawer.Close`, `Popover.Close`, `AlertDialog.Close`, `SwipeDrawer.Close`, and `Toast.Close` supply an X icon and a localized accessible name when no content is supplied. Their default frames are square, with a 44px touch target on coarse pointers. Supply `aria-label` for a context-specific name, `children` for text or custom artwork, or `render` for a custom control. Provider part settings, refs, events and class/style overrides continue to apply. `Modal.closeProps` exposes that same configurable control. Footer actions such as “Done” remain explicit text actions.

## Sliders with marks and restricted values

`Slider.Marks` belongs inside `Slider.Control`, alongside the track. Each mark has a numeric `value` and an optional `label`. Marks are decorative; give every thumb an accessible name. `Slider.Root.scale` and `formatValue` transform displayed and announced values while forms submit the raw numeric value. A nonlinear example maps raw exponents to storage sizes.

`Slider.DiscreteRoot` accepts an ascending `allowedValues` array and scalar or range values. Its physical positions follow the numeric domain, including uneven gaps. Arrow keys advance to the next allowed value; Home/End and pointer interaction respect the domain. Forms submit actual values. It supports controlled/uncontrolled values, disabled state, orientation, styles and refs. `minStepsBetweenValues` counts allowed choices between range thumbs. This restricted composition uses non-crossing thumbs; use ordinary `Slider.Root` when push/swap behavior is required. Uncontrolled native form reset restores `defaultValue`, including canceled-reset handling. Controlled resets remain application-owned.

## Consecutive notifications

Call `useToastQueue` inside `Toast.Provider`, then enqueue normal toast options. Only one queued notification enters the provider at a time. The next enters after the previous is removed, including its exit transition. Pending messages have no running timeout. `clear()` discards pending messages and closes the active one; `pendingCount` reports waiting messages. IDs must be unique among queued messages. Use a dedicated provider for an independent queue. `Toaster.dismissOnClickAway` is an explicit optional policy; its default is false. It dismisses one visible notification on a primary outside press without canceling that interaction. Avoid click-away for messages requiring an explicit decision.

## Navigation and layouts

`NavigationDrawer` pushes content on desktop and switches to a modal drawer below 48rem. `mode="mini"` retains `miniNavigation` when collapsed. Supply accessible icon links for the mini rail. Logical `edge` follows RTL. The application owns `open`; closing returns focus to the toggle. Its viewport subscription and portal use the provider document, including an embedded document. Mobile and desktop trigger/close props are configurable separately. Widths can be overridden with `--_navigation-width` and `--_mini-width`. Its responsive threshold is shared by its CSS and media-query subscription.

`CollapsedBreadcrumbs` keeps leading/trailing items and puts hidden ancestors in a keyboard-operated menu. `maxItems`, `beforeCollapse`, `afterCollapse`, separator, accessible labels, profiles and normal navigation props are configurable. Escape returns focus to the expansion control. Hidden items retain actual links; path segments without links are descriptive disabled menu items.

`useScrollTrigger` observes the window or a stable mounted `targetRef`. It reports offset, direction, threshold crossing and a hysteresis-aware trigger. Compose `ScrollAppBar` with `hidden={triggered}` and `elevated={scrolled}`. Focus within reveals the bar. `scrollToTop()` respects operating-system reduced motion and a Faye UI theme's zero-duration motion policy; its optional `reducedMotion` flag also supports custom policies outside a Faye UI theme. Subscriptions and animation frames clean up on unmount.

`FloatingActions` exposes controlled/default open state, hover activation, logical/vertical direction, disabled actions and trigger/positioner/popup props. Its default launcher uses an icon button whose plus rotates into a close mark while open. The short transition respects system and provider reduced-motion preferences; custom trigger artwork supplied through triggerProps.children keeps its own behavior. Horizontal docks preserve readable action widths and scroll when constrained. Keyboard navigation brings the focused action into view, and pointer/touch scrolling remains available. Menu owns keyboard navigation, dismissal and focus restoration. `items[].icon` supplies decorative artwork independently from labels. Direction changes keyboard orientation; radial artwork is not required for these functions.

## Touch, badges and timelines

`TouchTooltip` adds long-press activation to the same tooltip hover/focus/Escape/outside-dismissal model. A normal tap activates the child control; a completed hold previews its description and suppresses the following activation. Movement beyond tolerance, pointer cancellation and leaving the trigger cancel a hold. Delay, release timeout, tolerance, controlled state, disabled state and part props are configurable. Essential instructions should always remain visible elsewhere.

`BadgeIndicator.position` accepts all logical corners; legacy `start`/`end` retain their top-corner meaning. `overlap="circular"` moves the indicator inward along round artwork. Counts, dots, maximums and accessible labels retain their existing behavior.

`Timeline.position` supports start/end, alternate and alternate-reverse sides. `TimelineItem.opposite` adds metadata opposite the entry. Narrow containers reflow to one readable column without changing DOM reading order. Timestamp semantics and custom markers remain available. Timeline is an additional MUI Lab comparison.

## Verification boundary

Functional tests exercise actual interactions and form/focus behavior. Visual review covers theme, density, RTL, narrow layouts, long headings and icon frames. Linux browser qualification does not certify every physical device or assistive-technology combination. Specialist grid/chart/date engines remain separate scope.

## Executable example source

```tsx
'use client';
import { Choice } from '../choice';
import { useRef, useState } from 'react';
import { ArrowUp, Bell, Bookmark, Compass, Heart, MapPin, Plus, Share } from '@lucide/icons';
import { Icon } from '@fayegram-ai/faye-ui/core/icon';
import { Card, CardHeader, CardTitle, CardDescription, CardBody } from '@fayegram-ai/faye-ui/core/surface';
import { Button, IconButton } from '@fayegram-ai/faye-ui/controls/button';
import { TextField } from '@fayegram-ai/faye-ui/controls/text-field';
import { Slider } from '@fayegram-ai/faye-ui/controls/slider';
import { Stack, Inline } from '@fayegram-ai/faye-ui/layout/layout';
import { BadgeIndicator } from '@fayegram-ai/faye-ui/feedback/badge-indicator';
import { Toast } from '@fayegram-ai/faye-ui/feedback/toast';
import { Toaster, useToastQueue } from '@fayegram-ai/faye-ui/patterns/toaster';
import { NavigationDrawer } from '@fayegram-ai/faye-ui/patterns/navigation-drawer';
import { CollapsedBreadcrumbs } from '@fayegram-ai/faye-ui/patterns/collapsed-breadcrumbs';
import { ScrollAppBar } from '@fayegram-ai/faye-ui/patterns/application-navigation';
import { FloatingActions } from '@fayegram-ai/faye-ui/patterns/floating-actions';
import { TouchTooltip } from '@fayegram-ai/faye-ui/overlays/tooltip';
import { Drawer } from '@fayegram-ai/faye-ui/overlays/drawer';
import { Timeline, TimelineItem } from '@fayegram-ai/faye-ui/display/timeline';
import { useScrollTrigger } from '@fayegram-ai/faye-ui/hooks/scroll-trigger';
import { ThemeScope } from '@fayegram-ai/faye-ui/provider/provider';
import { Section, Result } from '../example';
import './functional-completion.css';

function QueuedSaves() {
    const queue = useToastQueue(), [clickAway,setClickAway] = useState(false), [undone,setUndone] = useState(0);
    return <Section title="Consecutive notifications"><Inline wrap><Button onClick={() => ['Coastal walk','Garden picnic','Evening concert'].forEach(title => queue.enqueue({title:`Saved: ${title}`,type:'success',timeout:0,actionProps:{children:'Undo save',onClick:() => setUndone(v => v+1)}}))}><Icon icon={Bookmark}/>Save three plans</Button><Button onClick={queue.clear}>Clear queued saves</Button><Button aria-pressed={clickAway} onClick={() => setClickAway(v => !v)}>Click-away dismissal</Button></Inline><Result><span data-queue-count>{queue.pendingCount} waiting</span> · <span data-queue-undo>{undone} {undone === 1 ? 'save' : 'saves'} undone</span></Result><Toaster dismissOnClickAway={clickAway}/></Section>;
}
const allowed = [5,15,30,60,120];
export default function FunctionalCompletion() {
    const [open,setOpen] = useState(false), [mini,setMini] = useState(false), [duration,setDuration] = useState(15), [range,setRange] = useState<readonly number[]>([15,60]);
    const [navigationEdge,setNavigationEdge] = useState<'start'|'end'>('start'), [timelinePosition,setTimelinePosition] = useState<'start'|'end'|'alternate'|'alternate-reverse'>('alternate');
    const [storyReduced,setStoryReduced] = useState(false);
    const [highlight,setHighlight] = useState(false);
    const [explored,setExplored] = useState(false), [readBadges,setReadBadges] = useState<string[]>([]);
    const [submitted,setSubmitted] = useState('Choose a duration'), [cancelReset,setCancelReset] = useState(false), [tapCount,setTapCount] = useState(0);
    const [dock,setDock] = useState(false), [dockOpen,setDockOpen] = useState(false), [direction,setDirection] = useState<'up'|'down'|'start'|'end'>('up'), [selected,setSelected] = useState('No quick action selected');
    const scrollRef = useRef<HTMLDivElement>(null), scroll = useScrollTrigger({targetRef:scrollRef,threshold:40,hysteresis:8});
    const navigation = <nav aria-label="Weekend destinations" className="faye-ui-weekend-links"><a href="#quiet-plan"><Icon icon={Compass}/>Discover</a><a href="#quiet-plan"><Icon icon={Bookmark}/>Your saved plans</a><a href="#quiet-plan"><Icon icon={Heart}/>Favorites</a></nav>;
    const miniNavigation = <nav aria-label="Compact weekend destinations" className="faye-ui-weekend-mini">{[{label:'Discover',icon:Compass},{label:'Your saved plans',icon:Bookmark},{label:'Favorites',icon:Heart}].map(item => <a key={item.label} href="#quiet-plan" aria-label={item.label} title={item.label}><Icon icon={item.icon}/></a>)}</nav>;
    return <Stack gap="6" data-functional-completion>
        <Section title="A little room for the weekend"><Inline wrap align="end"><Button aria-pressed={mini} onClick={() => setMini(v => !v)}>Mini navigation</Button><Choice label="Navigation edge" value={navigationEdge} onValueChange={value => setNavigationEdge(value as typeof navigationEdge)} options={['start','end'].map(value=>({value,label:value}))}/></Inline><p className="faye-ui-weekend-intro">Explore, save, and make a plan at your own pace.</p>
            <NavigationDrawer open={open} onOpenChange={setOpen} edge={navigationEdge} mode={mini ? 'mini' : 'persistent'} title="Your weekends" toggleLabel="Weekend navigation" closeLabel="Close weekend navigation" navigation={navigation} miniNavigation={miniNavigation}>
                <Stack gap="4"><CollapsedBreadcrumbs label="Weekend path" items={[{label:'Home',href:'#home'},{label:'Discover',href:'#discover'},{label:'Nearby',href:'#nearby'},{label:'Outside',href:'#outside'},{label:'Coastal walk',href:'#quiet-plan'}]}/>
                <Card id="quiet-plan"><CardHeader><CardTitle>Follow the coast</CardTitle><CardDescription>A gentle route, a warm drink, and time to notice the view.</CardDescription></CardHeader><CardBody><Stack gap="4"><TextField label="Give your plan a name" description="A name helps you find this day again." defaultValue="A quiet Saturday" startAdornment={<Icon icon={MapPin}/>}/><Inline wrap><Button variant="solid" tone="accent" onClick={() => setExplored(true)}><Icon icon={Compass}/>Explore this route</Button><TouchTooltip content="Save this plan for later. On touch, hold to preview without saving."><IconButton aria-label="Save for later" onClick={() => setTapCount(v => v+1)}><Icon icon={Bookmark}/></IconButton></TouchTooltip><output data-touch-taps>{tapCount} {tapCount === 1 ? 'plan' : 'plans'} saved</output></Inline>{explored && <Result>Coastal route selected</Result>}</Stack></CardBody></Card>
                </Stack>
            </NavigationDrawer>
        </Section>
        <Section title="Choose the pace"><form data-capability-form onSubmit={event => {event.preventDefault();const form=new FormData(event.currentTarget);setSubmitted(`Duration ${form.get('duration')} · Range ${form.getAll('window').join(',')}`);}} onReset={event => {if (cancelReset) {event.preventDefault();setCancelReset(false);} else {setDuration(15);setRange([15,60]);}}}><Stack gap="5">
            <label>Walking time in minutes</label><Slider.DiscreteRoot name="duration" allowedValues={allowed} value={duration} onValueChange={setDuration} formatValue={v => `${v} minutes`}><Slider.Value/><Slider.Control><Slider.Track><Slider.Indicator/><Slider.Thumb aria-label="Walking time"/></Slider.Track><Slider.Marks marks={[{value:5,label:'5 min'},{value:30,label:'30 min'},{value:120,label:'2 hr'}]}/></Slider.Control></Slider.DiscreteRoot>
            <Slider.DiscreteRoot name="window" allowedValues={allowed} value={range} onValueChange={setRange} minStepsBetweenValues={1} formatValue={v => `${v} minutes`}><Slider.Value/><Slider.Control><Slider.Track><Slider.Indicator/><Slider.Thumb index={0} aria-label="Earliest walk"/><Slider.Thumb index={1} aria-label="Latest walk"/></Slider.Track></Slider.Control></Slider.DiscreteRoot>
            <Slider.DiscreteRoot name="uncontrolled" allowedValues={[1,5,20,100]} defaultValue={5} variants={highlight ? {tone:'accent'} : undefined}><Slider.Control><Slider.Track><Slider.Indicator/><Slider.Thumb aria-label="Uncontrolled choices"/></Slider.Track></Slider.Control></Slider.DiscreteRoot>
            <Slider.Root name="space" min={0} max={10} defaultValue={5} scale={v => 2**v} formatValue={v => `${v} KB`}><Slider.Value/><Slider.Control><Slider.Track><Slider.Indicator/><Slider.Thumb aria-label="Offline storage"/></Slider.Track></Slider.Control></Slider.Root>
            <Inline wrap><Button type="submit">Apply walking time</Button><Button type="reset">Reset walking time</Button><Button aria-pressed={cancelReset} onClick={() => setCancelReset(v => !v)}>Cancel next reset</Button><Button aria-pressed={highlight} onClick={() => setHighlight(v => !v)}>Highlight walking controls</Button></Inline><Result>{submitted}</Result>
        </Stack></form></Section>
        <Toast.Provider><QueuedSaves/></Toast.Provider>
        <Section title="Navigation that follows your reading"><Button aria-pressed={storyReduced} onClick={() => setStoryReduced(v => !v)}>Reduce story motion</Button><ThemeScope motion={storyReduced ? 'reduced' : 'system'}><div ref={scrollRef} className="faye-ui-weekend-scroll" tabIndex={0} aria-label="Scrollable weekend story"><ScrollAppBar hidden={scroll.triggered} elevated={scroll.scrolled}><strong>A slow Saturday</strong><IconButton aria-label="Back to story start" variant="ghost" onClick={() => scroll.scrollToTop()}><Icon icon={ArrowUp}/></IconButton></ScrollAppBar><div className="faye-ui-weekend-story">{['Start with the morning light','Take the path beside the water','Leave room for a longer lunch','Find a place to watch the sunset'].map(title => <section key={title}><h4>{title}</h4><p>Move through the day at your own pace. The navigation returns when you scroll back or focus its controls.</p></section>)}</div></div></ThemeScope><Inline wrap><Button onClick={() => scroll.scrollToTop()}><Icon icon={ArrowUp}/>Back to the beginning</Button><output data-scroll-state>{scroll.triggered ? 'Header resting' : 'Header visible'} · {Math.round(scroll.offset)} px</output></Inline></Section>
        <Section title="Make room from any edge"><Inline wrap>{(['top','bottom','start','end'] as const).map(edge => <Drawer.Root key={edge}><Drawer.Trigger render={<Button/>}>Plan from {edge}</Drawer.Trigger><Drawer.Portal><Drawer.Backdrop/><Drawer.Popup edge={edge}><Drawer.Header><Drawer.Title>Make room for your next journey</Drawer.Title><Drawer.Close aria-label={`Close ${edge} plan`}/><Drawer.Description>Keep the details close, with space to breathe.</Drawer.Description></Drawer.Header><Drawer.Body><TextField label="A note for the day" description="This stays in the example." placeholder="Bring a picnic blanket"/></Drawer.Body><Drawer.Actions><Drawer.Close render={<Button/>}>Done planning</Drawer.Close></Drawer.Actions></Drawer.Popup></Drawer.Portal></Drawer.Root>)}</Inline></Section>
        <Section title="Little signals, thoughtful placement"><Inline wrap>{(['top-start','top-end','bottom-start','bottom-end'] as const).map((position,index) => <Stack key={position} gap="2" align="center"><BadgeIndicator count={readBadges.includes(position) ? 0 : index+1} label={`${index+1} ${index === 0 ? 'update' : 'updates'}`} position={position} overlap="circular"><IconButton aria-label={`Updates ${position}`} aria-pressed={readBadges.includes(position)} onClick={() => setReadBadges(values => values.includes(position) ? values.filter(value => value !== position) : [...values,position])} className="faye-ui-weekend-round"><Icon icon={Bell}/></IconButton></BadgeIndicator><small>{position}</small></Stack>)}</Inline></Section>
        <Section title="A day, in order"><Choice label="Timeline position" value={timelinePosition} onValueChange={value => setTimelinePosition(value as typeof timelinePosition)} options={['end','start','alternate','alternate-reverse'].map(value=>({value,label:value}))}/><Timeline position={timelinePosition} aria-label="Weekend itinerary"><TimelineItem timestamp="09:00" dateTime="09:00" opposite="By the coast" tone="accent">Morning walk</TimelineItem><TimelineItem timestamp="12:30" dateTime="12:30" opposite="In the garden" tone="success">A picnic with friends</TimelineItem><TimelineItem timestamp="18:00" dateTime="18:00" opposite="Near the water">Sunset and a quiet moment</TimelineItem></Timeline></Section>
        <Section title="Quick actions, your way"><Inline wrap align="end"><Button aria-pressed={dock} onClick={() => setDock(v => !v)}>Show quick actions</Button><Button aria-pressed={dockOpen} onClick={() => {setDock(true);setDockOpen(v => !v);}}>Open controlled actions</Button><Choice label="Action direction" value={direction} onValueChange={value => setDirection(value as typeof direction)} options={['up','down','start','end'].map(value=>({value,label:value}))}/></Inline>{dock && <FloatingActions label="Weekend quick actions" open={dockOpen} onOpenChange={setDockOpen} direction={direction} openOnHover items={[{id:'save',label:'Save route',icon:<Icon icon={Bookmark}/>,onSelect:() => setSelected('Route saved')},{id:'share',label:'Share plan',icon:<Icon icon={Share}/>,onSelect:() => setSelected('Share selected')},{id:'new',label:'New plan',icon:<Icon icon={Plus}/>,disabled:true,onSelect:() => setSelected('Unexpected action')}]}/>}<Result>{selected}</Result></Section>
    </Stack>;
}
```
