The right place for a message
Put each message as close as possible to what it's about, and make it last as long as the problem does.
Enter a full email address, like sara@northwind.io
The problem
Every message ends up in a toast because it's easy to call. But a toast saying “Email is invalid” disappears before people find the field, and a page-wide banner about one card makes everyone read it. Messages in the wrong place are either missed or ignored.
When to use it
Use it for
- Validation errors, warnings, confirmations, outages and announcements.
- Any time a team is about to add another toast or banner.
Not for
- Questions that need an answer before work continues: use a Dialog.
- Loading: use a Skeleton or the loading state of the button.
Anatomy
- FieldHelper text and errorMessage under an Input. About one field.
- SectionAn Alert inside a card, a form or a table. About that section.
- Page or appA Banner across the top. About the whole product: an outage, a trial ending.
- MomentA Toast. Confirms an action that just happened, then goes away. Never the only record of an error.
- AbsenceAn EmptyState where the content would be. Explains why there's nothing.
How to build it
- Ask what the message is about: a field, a section, the whole page, or an action that just happened.
- Put it there: errorMessage on the field, an Alert in the section, a Banner on the page, a Toast for the action.
- Match how long it stays to how long the problem lasts. Errors stay until fixed; confirmations can go.
- Use one tone per message: info, success, warning or danger. Danger only when something failed or will be lost.
- Give every persistent message a way forward: a fix, a retry or a link.
- Limit Banners to one at a time, and let people dismiss the ones that are only news.
app.tsx
import { Alert, Banner, Button, Input, useToast } from "@nexera-ui/react";
export function LeaveRequest({ exportDelayed, balances, onRetry }: Props) {
const toasts = useToast();
return (
<>
{/* About the whole app, until it's fixed */}
{exportDelayed && <Banner tone="warning" message="Payroll export is delayed. We're on it." />}
{/* About one section */}
{balances.error && (
<Alert
tone="danger"
title="Couldn't load leave balances"
description="Your requests are saved."
actions={<Button size="sm" variant="secondary" onPress={onRetry}>Try again</Button>}
/>
)}
{/* About one field */}
<Input label="Work email" isInvalid errorMessage="Enter a full email address" />
{/* About an action that just happened */}
<Button
onPress={() => {
toasts.add({ title: "Request sent to Omar", tone: "success" });
}}
>
Send request
</Button>
</>
);
}Accessibility
- Alert, Banner and Toast are live regions: they're read out when they appear, without moving focus.
- Danger messages are assertive and interrupt; everything else is polite. Don't make news assertive.
- Field errors are linked to the field with aria-describedby, so they're read when it's focused.
- Toasts pause while hovered or focused, and stay at least 5 seconds; never put the only copy of an error in one.
Do and don't
DoShow a field error under the field, until it's fixed.
Don'tShow “Email is invalid” in a toast that disappears.
DoUse a Banner only for things that affect the whole product.
Don'tPut a banner on the page for a problem in one card.
DoConfirm finished actions with a short Toast.
Don'tInterrupt with a “Success!” dialog that needs closing.