alouette
alouette-feedback
Tell the user what happened, in place. Message is the semantic banner — InfoMessage, ConfirmationMessage, WarningMessage and ErrorMessage are its ready-made meanings — optionally dismissible, and flattened when it sits inside an already-raised surface. ConnectionState is the banner pinned at the top of the screen reporting the network. LinearProgress and CircularProgress show how far an operation with a known percentage has got; their Indeterminate twins show that one is running when no percentage exists. The skeleton utility is the pulsing placeholder of content still loading. EmptyState stands in for a list or a section with nothing to show. A failure coming out of a button or a form is already rendered by that component, so reach for these for status the screen itself has to state. Load when showing inline status, alerts, dismissible notices, connection status, a progress bar or a loading indicator.
This skill builds on alouette-theming. Read it first for the accent model.
alouette — Feedback messages
Message is an accent-themed banner with an icon and optional dismiss button. Prefer the presets InfoMessage / ConfirmationMessage / WarningMessage / ErrorMessage, which set the accent and icon for you.
Setup
import { InfoMessage } from "alouette";
<InfoMessage>Your changes were saved.</InfoMessage>;Core Patterns
Presets
import { InfoMessage, ConfirmationMessage, WarningMessage, ErrorMessage } from "alouette";
<InfoMessage>Heads up.</InfoMessage> {/* accent="info" */}
<ConfirmationMessage>Saved.</ConfirmationMessage> {/* accent="success" */}
<WarningMessage>Careful.</WarningMessage> {/* accent="warning" */}
<ErrorMessage>Payment failed.</ErrorMessage> {/* accent="danger" */}Dismissible message
onDismiss and dismissIconAriaLabel must be provided together:
<WarningMessage onDismiss={hide} dismissIconAriaLabel="Dismiss">
Low disk space.
</WarningMessage>Elevation — variant
variant is "surface" (default) or "flat". surface carries shadow-m: the banner is its own raised layer above the screen background, which is what you want almost everywhere.
flat only drops the shadow. Reach for it only when the message is already inside a raised surface — a Modal/AlertDialog panel, a surface card, a PressableListItem row — where a second elevation reads as a card stacked on a card. Even there, prefer laying the message out on the screen background (its own surface banner above or below the card) when the layout allows it; flat is the allowance for when it cannot. It is the message's own variant, not the surface-flat utility (alouette-layout): a banner is not a surface — its ground is bg-highlight-accent and its sizes carry a gap.
<ErrorMessage>Payment failed.</ErrorMessage> {/* on the page */}
<Box className="surface">
<Text>Billing</Text>
<ErrorMessage variant="flat">Payment failed.</ErrorMessage> {/* inside a card */}
</Box>AlertDialog already renders its errorToMessage failure flat, and ActionButton exposes errorMessageVariant for the same reason (see alouette-actions).
Custom Message (other accents / icons)
The four presets cover info / success / warning / danger. For a different accent or a custom icon, use the base Message with an explicit accent and icon. size is "sm" | "md" | "lg".
import { Message } from "alouette";
import { XCircleRegularIcon } from "alouette-icons/phosphor-icons/XCircle";
<Message accent="brand" icon={<XCircleRegularIcon />} size="lg">
Custom banner.
</Message>;Connection status banner
ConnectionState is a thin banner pinned to the top of its nearest positioned ancestor. It stays hidden while connected and slides down to reveal a pill when connecting or disconnected. It sets its own accent (success when connected, danger otherwise), so no accent prop.
import { ConnectionState } from "alouette";
<ConnectionState state={status}>
{status === "connected" ? "Connected" : "Reconnecting…"}
</ConnectionState>;state is "connected" | "connecting" | "disconnected" | null (null renders nothing). children is the required pill label. On reconnection it turns green and holds briefly before sliding out. forceVisible keeps it on-screen even when connected (demos); forceHidden forces it off-screen regardless of state.
The banner is absolutely positioned, so its parent must establish a positioning context and clip the off-screen pill — wrap it in relative overflow-hidden.
Progress indicators
LinearProgress (a thin bar pinned to the top of its positioned ancestor) and CircularProgress (a ring) show a known completion percentage. Both take progress (0-100), accent (defaults to the nearest accent scope's, brand outside one), size ("xs" | "sm" | "md" | "lg"), and hidden (fades out without unmounting). The fill animates on progress change.
import { LinearProgress, CircularProgress } from "alouette";
<CircularProgress progress={uploaded} accent="brand" size="md" />
<Box className="relative overflow-hidden">
<LinearProgress progress={uploaded} hidden={uploaded >= 100} />
</Box>For an unknown percentage (reconnecting, a page transition, a background fetch), use IndeterminateLinearProgress / IndeterminateCircularProgress. They take loading instead of progress and hidden: while true the fill creeps toward 100% without reaching it; once false it completes, then fades out. Keep them mounted and toggle loading — unmounting on completion cuts the finish animation off.
import { IndeterminateLinearProgress } from "alouette";
<Box className="relative overflow-hidden">
<IndeterminateLinearProgress loading={isFetching} size="sm" />
</Box>;A pending action needs neither: a Button with state="loading" (or ActionButton, which derives it from the promise) already renders the indeterminate ring (see alouette-actions).
Source: packages/alouette/src/ui/feedback/LinearProgress.tsx; ui/feedback/useSimulatedProgress.ts
Loading placeholders
Content whose shape is known but whose data is not there yet (a first fetch) renders skeleton blocks in its place: animate-pulse rounded-sm bg-lowered, sized with w-* / h-* to the content they stand in for. A bar is mute, so the region carries role="status" and an aria-label. The pulse stops under reduced motion.
<View role="status" aria-label="Loading pull requests" className="gap-xs">
<View className="skeleton h-[54px]" />
<View className="skeleton h-[54px]" />
</View>A placeholder sits where the content will: on the screen ground for rows that are raised on their own, inside the surface for a card's lines. Once data has been shown, a refetch keeps it and uses an IndeterminateLinearProgress instead of swapping back to skeletons.
Source: packages/alouette/scripts/build-css.ts; ui/feedback/Skeleton.stories.tsx
Empty state
A list or a section that loaded and has nothing to show renders an EmptyState where the content would have been, on the screen ground: a muted 48px icon, a title, and optional muted prose as children saying why it is empty or what would fill it. It is not a Message: nothing went wrong, so it carries no accent.
import { EmptyState } from "alouette";
import { ConfettiRegularIcon } from "alouette-icons/phosphor-icons/Confetti";
<EmptyState icon={<ConfettiRegularIcon />} title="Nothing to review">
No pull request is waiting for you.
</EmptyState>;A whole screen with a single purpose (sign-in, splash) is ScreenCenterLayout (alouette-layout), not an EmptyState.
Source: packages/alouette/src/ui/feedback/EmptyState.tsx
Common Mistakes
MEDIUM onDismiss without dismissIconAriaLabel
Wrong:
<InfoMessage onDismiss={hide}>Saved</InfoMessage>Correct:
<InfoMessage onDismiss={hide} dismissIconAriaLabel="Dismiss">
Saved
</InfoMessage>Message types onDismiss and dismissIconAriaLabel as a required pair; providing one without the other is a type error and leaves the dismiss button without an accessible label.
Source: packages/alouette/src/ui/feedback/Message.tsx
MEDIUM Rendering the base Message without accent/icon
Wrong:
<Message>Heads up</Message>Correct:
<WarningMessage>Heads up</WarningMessage>The base Message requires both accent and icon. For the common cases use a preset, which sets them.
Source: packages/alouette/src/ui/feedback/Message.tsx
MEDIUM Reaching for variant="flat" outside a surface
Wrong:
<View>
<ErrorMessage variant="flat">Payment failed.</ErrorMessage>
</View>Correct:
<View>
<ErrorMessage>Payment failed.</ErrorMessage>
</View>flat is not "the quiet look" — it is the fix for a banner nested in a raised surface. On the screen background it loses the elevation that separates the banner from the page. Default to surface; use flat only inside a Modal / AlertDialog panel or a surface card, and prefer restructuring so the message can sit on the page instead.
Source: packages/alouette/src/ui/feedback/Message.tsx
MEDIUM Hand-building a colored banner instead of Message
Wrong:
<Box className="bg-yellow-100">
<Text>Warning</Text>
</Box>Correct:
<WarningMessage>Warning</WarningMessage>A manual banner loses accent theming, the leading icon, dark-mode support and the dismiss a11y that Message provides.
Source: packages/alouette/src/ui/feedback/Message.tsx
MEDIUM ConnectionState in a non-positioned, non-clipping parent
Wrong:
<Box>
<ConnectionState state={status}>Reconnecting…</ConnectionState>
</Box>Correct:
<Box className="relative overflow-hidden">
<ConnectionState state={status}>Reconnecting…</ConnectionState>
</Box>The banner is absolute inset-x-0 top-0 and slides in from off-screen. Without a positioned (relative) ancestor it anchors to the wrong element, and without overflow-hidden the hidden/pre-slide pill leaks above the container.
Source: packages/alouette/src/ui/feedback/ConnectionState.tsx
LOW Passing accent to ConnectionState
ConnectionState derives its accent from state (success when connected, danger otherwise) — there is no accent prop. To theme it, change state.
Source: packages/alouette/src/ui/feedback/ConnectionState.tsx
MEDIUM Using LinearProgress/CircularProgress for an unknown-duration wait
Wrong:
<CircularProgress progress={0} /> {/* as a spinner */}
{isLoading && <CircularProgress progress={50} />}Correct:
<IndeterminateCircularProgress loading={isLoading} />LinearProgress / CircularProgress render the progress value you pass, so a fixed value reads as a stalled operation. The Indeterminate twin owns the creep, the completion and the fade-out from a single loading flag. When the wait belongs to a button press, use Button state="loading" / ActionButton instead.
Source: packages/alouette/src/ui/feedback/CircularProgress.tsx; ui/actions/Button.tsx
See also: alouette-animation/SKILL.md — render messages in PresenceList for animated add/remove; alouette-data/SKILL.md — Badge, for an inline status pill rather than a banner.