Skip to content
fayeuiStatic / 0.1.0
LLMs
Components / Static edition

Feedback and status

Alerts, inline and anchored badges, progress, loading and notifications.

Quick start

After the application setup, copy this component into your application. It imports only public packages; the complete live demo later on this page also uses documentation layout helpers.

import { Alert } from '@fayegram-ai/faye-ui/feedback/alert';
import { CircularProgress } from '@fayegram-ai/faye-ui/feedback/circular-progress';
import { LinearProgress } from '@fayegram-ai/faye-ui/feedback/progress';
import { Stack } from '@fayegram-ai/faye-ui/layout/layout';

export function UploadFeedback() {
  return <Stack gap="4">
    <Alert tone="warning" title="Connection interrupted">Your draft is retained. Try again when you are connected.</Alert>
    <CircularProgress value={40} label="Upload progress" />
    <LinearProgress value={40} bufferValue={65} mode="buffer" label="Loaded upload progress" />
  </Stack>;
}

API reference: Alert, Circular Progress, Progress.

Usage contract

Color supplements text rather than replacing it. Announcements should happen for meaningful changes, not every static alert on page load. Toast messages are application content. A progress bar reports a task; a meter reports a bounded measurement.

Customization

Use semantic tokens for visual changes, public component parts for structural customization, and explicit props for instance behavior. The surrounding site lets you compare light and dark, different brands and density.

Source and verification

The examples below import actual public package entries. Full source is downloadable. Source coverage does not establish keyboard, hydration or browser compatibility; those require the application test suite.

Progress and placeholders

CircularProgress is server-safe. A finite value uses the min/max range; null (the default) means unknown duration. Values clamp to the range; non-finite values and invalid ranges fail explicitly. Supply a useful label or aria-labelledby. LinearProgress adds determinate, indeterminate, buffer and query modes; the accessible value always describes completed work, while bufferValue describes the loaded portion visually. Use aria-valuetext when a percentage is insufficient. Compound Progress.Label and Progress.Buffer are available for custom anatomy.

Spinner and Skeleton accept explicit static profiles. Skeleton supports text, rectangle and circle shapes, plus pulse, wave or no animation. A circle defaults to equal width and height. Placeholders are hidden from assistive technology; label the surrounding busy region or use a Spinner/status. Reduced motion stops animation while keeping the progress ring, filled track and placeholders visible. Provider motion reduction is honored as well as the platform preference.

Short Badge statuses stay on one line. For intentionally long content choose overflow="wrap"; the entire value remains visible rather than being silently clipped. Let surrounding rows reflow instead of forcing status text into a narrow fixed column.

Notifications and snackbar behavior

Toast is the compound notification API; Toaster is its ready-made viewport. Mount one Toast.Provider around the actions that publish notifications, then mount Toaster inside that provider. The manager is available through Toast.useToastManager(); Toast.createToastManager() supports actions outside React.

'use client';
import { Toast } from '@fayegram-ai/faye-ui/feedback/toast';
import { Toaster } from '@fayegram-ai/faye-ui/patterns/toaster';

function SaveFeedback() {
  const manager = Toast.useToastManager();
  return <button onClick={() => manager.add({
    title: 'Connection interrupted',
    description: 'Your draft is retained. Reconnect and try again.',
    type: 'warning',
    timeout: 0,
  })}>Show warning</button>;
}

export function Notifications() {
  return <Toast.Provider timeout={5000} limit={3}>
    <SaveFeedback />
    <Toaster placement="bottom-end" />
  </Toast.Provider>;
}

type selects neutral, accent, secondary, info, success, warning or danger colors; error maps to danger. Unrecognized types use the toaster's tone, which defaults to neutral. Severity does not automatically make a message urgent: set priority="high" in manager options only when an immediate announcement is appropriate. Ordinary notifications are announced politely.

placement supports top/bottom combined with start/center/end. Start and end follow direction. Use viewportProps for additional attributes, classes or style, including an application-specific inset above a bottom navigation bar or floating action. Portals retain provider theme, direction, density and reduced-motion settings. Low-level public parts remain available for custom anatomy and transitions.

Provider timeout is the default duration; individual messages can override it, and zero keeps a message until dismissed. Hover and keyboard focus pause timers. The provider's limit caps the visible stack; older limited messages are hidden and inert. A visible limit is not a FIFO queue: limited messages retain their own timer/lifecycle. Use useToastQueue for consecutive delivery after exit; it coordinates admission using the provider’s public removal lifecycle. See consumer interactions.

Use actionProps for a native action such as Undo. The manager supports update, close, same-ID replacement and promise loading/success/error transitions. Actions do not implicitly dismiss a message; close its returned ID when that is the intended behavior. Repeated messages and timing policy belong to the application.

Toaster Escape dismissal closes one visible notification before an underlying overlay. Set dismissOnEscape={false} when the application owns that policy. F6 can focus the notification region, and close/action controls remain keyboard accessible. Custom views built directly from Toast.Root retain Base UI's focused-toast Escape behavior. Click-away does not dismiss notifications by default; opt in with dismissOnClickAway for passive messages. Toast.Close supplies a labeled X icon when no content is given.

The default snackbar is a compact inline surface: a semantic icon, message, optional action, and dismiss icon. A brief message is 48px high with a fine pointer and 52px on touch screens; touch action and close targets remain at least 44px. Supporting descriptions are optional. Prefer a brief message for transient feedback; use an inline Alert for instructions that need sustained attention. Long text wraps instead of being truncated or overlapping controls. Longer action groups can flow below the message on narrow screens; keep action labels brief.

Success, warning, danger/error and information notifications have distinct decorative icons. Loading notifications show a spinner. The message remains the accessible announcement; decorative icons do not create duplicate announcements. Pass icon={null} to Toaster for text-only notifications, a React node for custom artwork, or icon={tone => ...} to choose artwork by resolved severity. dismissible={false} removes the close control; timeout and Escape policies remain separately configurable.

For custom anatomy, compose Toast.Content from Toast.Icon, Toast.Message (Title and optional Description), and Toast.Actions (Action and Close). All parts support provider settings and profile classes. Icon inherits the root tone; supplying children replaces its artwork. Direct Title/Description children remain supported for existing custom views. Action controls occupy normal layout space and show hover, pressed, disabled and compact keyboard-focus feedback.

Alert also supplies a semantic icon by default; icon={null} hides it and a React node replaces it. Its action slot accepts a button or action group, aligned beside the content. Titles and supporting text remain optional. Static alerts do not announce on page load unless announce is requested.

Anchored feedback composes Toast.Positioner with Toast.Arrow. The arrow supplies a small themed pointer by default and follows the resolved placement; its artwork remains replaceable through children or render composition.

Swipe dismissal continues in the configured direction from the release position. A short or canceled drag returns to rest. Text selection is suppressed during the gesture so a subsequent swipe cannot become a native text drag. Reduced-motion preferences remove the animated exit. Custom-rendered Action and Close parts retain disabled styling through their aria-disabled state; continue forwarding the public part's props and ref.

Progress motion direction

LinearProgress and Progress.Root accept animationDirection="ltr" or "rtl". Indeterminate and query modes default to a left-to-right sweep, independently of text direction. Determinate values still represent completed work. System and scoped reduced-motion preferences remove the sweep. The progress example lets you compare both directions.

Live examples

Try the controls below, then read or copy the complete example to adapt it for your application.

Messages and compact statuses

Changes are stored on this device.
A new version is available.
Your collection is saved.
You are offline. Changes are safe.
We could not save your changes.
DraftIn reviewReadyNeeds attentionFailed

Inline feedback with an action

You are offline. Your changes are safe.

Progress, meter and loading

Import progress
x
Buffered preview
x
Finding updates
x
Storage usage
x
Loading preview

Notifications

Notifications leave the rest of the page available.

Custom toast anatomy

Anchored notification

Read the complete example
'use client';
import { useRef, useState } from 'react';
import { Bell, RotateCcw } from '@lucide/icons';
import { Icon } from '@fayegram-ai/faye-ui/core/icon';
import { Alert } from '@fayegram-ai/faye-ui/feedback/alert';
import { BadgeIndicator } from '@fayegram-ai/faye-ui/feedback/badge-indicator';
import { Badge } from '@fayegram-ai/faye-ui/feedback/badge';
import { Spinner, Skeleton } from '@fayegram-ai/faye-ui/feedback/loading';
import { Progress, LinearProgress } from '@fayegram-ai/faye-ui/feedback/progress';
import { CircularProgress } from '@fayegram-ai/faye-ui/feedback/circular-progress';
import { Meter } from '@fayegram-ai/faye-ui/feedback/meter';
import { Toast } from '@fayegram-ai/faye-ui/feedback/toast';
import { Toaster, type ToastPlacement } from '@fayegram-ai/faye-ui/patterns/toaster';
import { Button } from '@fayegram-ai/faye-ui/controls/button';
import { Inline, Stack } from '@fayegram-ai/faye-ui/layout/layout';
import { Section } from '../example';
import { Choice } from '../choice';
function ToastAction() {
    const manager = Toast.useToastManager();
    const [type, setType] = useState('success'), [placement, setPlacement] = useState<ToastPlacement>('bottom-end'), [undone, setUndone] = useState(false), [content, setContent] = useState('compact');
    const titles: Record<string,string> = { neutral:'Preferences updated', info:'Updates available', success:'Draft saved', warning:'Working offline', danger:'Save failed' };
    return <Stack gap="3"><Inline wrap align="end">
        <Choice label="Notification severity" value={type} onValueChange={setType} options={['neutral','info','success','warning','danger'].map(value=>({value,label:value}))}/>
        <Choice label="Notification position" value={placement} onValueChange={value=>setPlacement(value as ToastPlacement)} options={['top-start','top-center','top-end','bottom-start','bottom-center','bottom-end'].map(value=>({value,label:value}))}/>
        <Choice label="Notification content" value={content} onValueChange={setContent} options={[{value:'compact',label:'Brief message'},{value:'detail',label:'With supporting detail'},{value:'long',label:'Long message'}]}/>
        <Button onClick={() => { setUndone(false); const id = manager.add({ title: content==='long' ? 'Your changes are saved on this device and will sync when you are connected again.' : titles[type], ...(content==='detail'?{description:'Your draft is retained on this device.'}:{}), type, actionProps: { children: 'Undo', onClick: () => { setUndone(true); manager.close(id); } } }); }}>Show notification</Button>
    </Inline><span role="status">{undone ? 'The preview change was undone.' : 'Notifications leave the rest of the page available.'}</span><Toaster placement={placement}/></Stack>;
}
function ToastCustomAction() { const manager = Toast.useToastManager(); return <Button onClick={() => manager.add({title:'Custom notification',description:'Rendered from public toast parts.',actionProps:{children:'Keep working',onClick:()=>manager.add({title:'Action handled',description:'The custom action keeps the current draft.'})}})}>Show custom notification</Button>; }
function CustomToastView() {
    const { toasts } = Toast.useToastManager();
    return <Toast.Portal><Toast.Viewport>{toasts.map(toast => <Toast.Root key={toast.id} toast={toast}><Toast.Content><Toast.Icon><Icon icon={Bell}/></Toast.Icon><Toast.Message><Toast.Title>{toast.title}</Toast.Title>{toast.description!=null && <Toast.Description>{toast.description}</Toast.Description>}</Toast.Message><Toast.Actions>{toast.actionProps && <Toast.Action/>}<Toast.Close aria-label="Dismiss custom notification"/></Toast.Actions></Toast.Content></Toast.Root>)}</Toast.Viewport></Toast.Portal>;
}
export default function Feedback() {
    const [progress, setProgress] = useState(35), [retried,setRetried] = useState(false), [animationDirection,setAnimationDirection]=useState<'ltr'|'rtl'>('ltr');
    return <><Section title="Messages and compact statuses"><Stack gap="3">{(['neutral', 'info', 'success', 'warning', 'danger'] as const).map(tone => <Alert key={tone} tone={tone}>{{neutral:'Changes are stored on this device.',info:'A new version is available.',success:'Your collection is saved.',warning:'You are offline. Changes are safe.',danger:'We could not save your changes.'}[tone]}</Alert>)}<Inline wrap><BadgeIndicator count={7} label="7 unread notifications"><Button aria-label="Notifications">Inbox</Button></BadgeIndicator><Badge dot>Draft</Badge><Badge tone="accent">In review</Badge><Badge tone="success" dot>Ready</Badge><Badge tone="warning">Needs attention</Badge><Badge tone="danger">Failed</Badge></Inline></Stack></Section><Section title="Inline feedback with an action"><Alert tone={retried?"success":"warning"} action={<Button size="sm" variant="soft" startIcon={<Icon icon={RotateCcw} size="sm"/>} onClick={()=>setRetried(n=>!n)}>{retried?"Reset":"Retry"}</Button>}>{retried?"Connection restored.":"You are offline. Your changes are safe."}</Alert></Section><Section title="Progress, meter and loading"><Progress.Root value={progress} aria-label="Import progress"><Progress.Label>Import progress</Progress.Label><Progress.Track><Progress.Buffer aria-hidden="true" style={{ width: `${Math.min(100, progress + 20)}%` }}/><Progress.Indicator /></Progress.Track><Progress.Value /></Progress.Root><Button onClick={() => setProgress(n => n >= 100 ? 0 : n + 5)}>Advance progress</Button><Inline><CircularProgress label="Circular import progress" value={progress}/><CircularProgress label="Discovering files"/></Inline><LinearProgress label="Buffered preview" mode="buffer" value={progress} bufferValue={Math.min(100, progress + 20)}/><Choice label="Progress direction" value={animationDirection} onValueChange={value=>setAnimationDirection(value as typeof animationDirection)} options={[{value:'ltr',label:'Left to right'},{value:'rtl',label:'Right to left'}]}/><LinearProgress label="Finding updates" mode="query" animationDirection={animationDirection}/><Meter.Root value={72} min={0} max={100} aria-label="Storage used"><Meter.Label>Storage usage</Meter.Label><Meter.Track><Meter.Indicator /></Meter.Track><Meter.Value /></Meter.Root><Inline><Spinner label="Loading preview"/><span>Loading preview</span></Inline><Skeleton height="1rem" width="70%"/><Skeleton shape="text" width="45%"/><Inline><Skeleton shape="circle"/><Skeleton animation="wave" height="3rem"/></Inline></Section><Section title="Notifications"><Toast.Provider timeout={5000} limit={3}><ToastAction /></Toast.Provider></Section><Section title="Custom toast anatomy"><Toast.Provider><ToastCustomAction/><CustomToastView/></Toast.Provider></Section><Section title="Anchored notification"><Toast.Provider timeout={5000} limit={1}><AnchoredNotification/></Toast.Provider></Section></>;
}

function AnchoredNotification() {
    const anchor=useRef<HTMLButtonElement>(null),manager=Toast.useToastManager();
    return <><Button ref={anchor} onClick={()=>manager.add({title:'Place saved locally',type:'success'})}>Save place with anchored feedback</Button><Toast.Portal><Toast.Viewport className="docs-anchored-notifications">{manager.toasts.map(toast=><Toast.Positioner key={toast.id} toast={toast} anchor={anchor.current} side="top" sideOffset={8}><Toast.Root toast={toast}><Toast.Arrow/><Toast.Content><Toast.Icon><Icon icon={Bell}/></Toast.Icon><Toast.Message><Toast.Title>{toast.title}</Toast.Title></Toast.Message><Toast.Actions><Toast.Close aria-label="Dismiss anchored notification"/></Toast.Actions></Toast.Content></Toast.Root></Toast.Positioner>)}</Toast.Viewport></Toast.Portal></>;
}