# Feedback and status

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

## Quick start

After the [application setup](/docs/installation/#component-consumption), copy this component into your application. It imports only public packages; the complete live demo later on this page also uses documentation layout helpers.

```tsx
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](/docs/api-feedback-alert/), [Circular Progress](/docs/api-feedback-circular-progress/), [Progress](/docs/api-feedback-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.

```tsx
'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](/docs/functional-completion/).

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.

## Executable example source

```tsx
'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></>;
}
```
