Toast
Toast notifications are short, non-blocking messages. They appear in a corner of the screen and go away again. Terracotta splits this into three pieces:
ToasterStoreis a framework-agnostic queue. Create it outside your components and push messages into it from anywhere.useToaster(store)subscribes to a store and returns a Solid accessor.<Toaster>and<Toast>are the markup. TheToasteris the live region.
The queue lives outside the component tree, so any code can raise a toast without prop drilling: a fetch handler, a router guard, a worker callback.
import { Toast, Toaster, ToasterStore, useToaster } from 'terracotta/toast';Anatomy
<Toaster> {/* role="status" aria-live="polite"; holds the queue */}
<Toast/> {/* one message; no live role of its own */}
</Toaster>Keep the Toaster mounted, even while the queue is empty. Screen readers only
announce changes inside a live region that already exists. A toast added to it
is then announced.
Examples
The store and the region
Anywhere else in the app:
Auto-dismiss
The store has no timing policy. Add one where you create the toast:
export function notify(data: Notice, ms = 4000): string {
const id = notices.create(data);
setTimeout(() => notices.remove(id), ms);
return id;
}Animating entry and exit
The queue removes an item immediately. To animate a toast out, wrap it in a
Transition:
<For each={queue()}>
{item => (
<Transition
show={true}
enter="toast-enter"
enterFrom="toast-enter-from"
enterTo="toast-enter-to"
>
<Toast class="toast">{item.data.title}</Toast>
</Transition>
)}
</For>.toast-enter { transition: opacity 150ms ease-out, translate 150ms ease-out; }
.toast-enter-from { opacity: 0; translate: 0 0.5rem; }
.toast-enter-to { opacity: 1; translate: none; }For a leave animation, keep the item in the queue until the transition finishes.
Flip a local signal, then call notices.remove(item.id) from afterLeave.
Clearing the queue
<button type="button" onClick={() => notices.clear()}>
Dismiss all
</button>Several independent regions
Each store is its own queue, so one page can have several:
export const systemNotices = new ToasterStore<Notice>();
export const uploadNotices = new ToasterStore<UploadProgress>();<Toaster class="toaster toaster-top">{/* systemNotices */}</Toaster>
<Toaster class="toaster toaster-bottom">{/* uploadNotices */}</Toaster>Reading the queue outside a component
notices.getQueue().length; // non-reactive snapshot
const unsubscribe = notices.subscribe(queue => {
console.log(`${queue.length} notice(s) pending`);
});useToaster is the reactive equivalent. It unsubscribes with the component.
State attributes
Neither component carries dynamic state. A toast is either in the queue or it is not, so only the markers are written.
| Element | Attribute | Present when |
|---|---|---|
Toaster |
tc-toaster |
Always |
Toast |
tc-toast |
Always |
Anything that varies per toast is your own data: severity, position, whether it
has an action. Pass a data-* attribute and select on it, as in the example
above.
Styling
[tc-toaster] {
position: fixed;
inset-block-end: 1rem;
inset-inline-end: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
[tc-toast] {
border-radius: 0.5rem;
background: #ffffff;
padding: 0.75rem 1rem;
box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
}
/* Stack newest on top without touching the markup */
[tc-toaster] { flex-direction: column-reverse; }Reading the state in code
There is no component state. The queue is the state, and you read it with
useToaster(store):
| Member | Type | Description |
|---|---|---|
useToaster(store) |
() => ToastData<T>[] |
Reactive accessor for the queue. |
store.getQueue() |
() => ToastData<T>[] |
Non-reactive snapshot. |
store.subscribe(cb) |
(cb) => () => void |
Manual subscription. Returns an unsubscribe function. |
Keyboard
Neither component handles keys. A toast is not a focus stop, and that is the
point: it must not interrupt what the user is typing. For a dismiss or undo
action, put a real <button> inside the toast. Tab reaches it the
normal way.
If the message must be acted on, it is not a toast. Use
AlertDialog.
API
ToasterStore<T>
A subscribable queue of { id, data } entries. T is whatever shape you want a
toast to carry.
| Member | Type | Description |
|---|---|---|
create(data) |
(data: T) => string |
Appends a toast and returns its generated id. |
remove(id) |
(id: string) => void |
Removes the toast with that id. |
clear() |
() => void |
Empties the queue. |
getQueue() |
() => ToastData<T>[] |
The current queue. Not reactive. |
subscribe(callback) |
(cb: (queue: ToastData<T>[]) => void) => () => void |
Subscribes to changes and returns an unsubscribe function. Each notification receives a fresh array. |
ToastData<T> is { id: string; data: T }.
useToaster(store)
| Signature | Description |
|---|---|
useToaster<T>(toaster: ToasterStore<T>): () => ToastData<T>[] |
Subscribes for the lifetime of the calling component and returns an accessor for the queue. |
<Toaster>
The container that owns the toast context. <Toast> must be rendered inside
one, and throws otherwise. Renders a <div> by default. Does not take a ref.
| Prop | Type | Default | Description |
|---|---|---|---|
as |
ValidConstructor |
'div' |
Element or component to render as. |
children |
JSX.Element |
none | The toasts. Not a render prop. |
| …rest | props of as |
none | Forwarded to the rendered element. |
Rendered attributes
| Attribute | Value |
|---|---|
role |
"status" |
aria-live |
"polite" |
aria-atomic |
"false" |
aria-relevant |
"additions text" |
tc-toaster |
"" |
The region is polite, so a toast is announced without interrupting the user.
aria-atomic="false" reads each new toast on its own, not the whole queue
again. Your own props are applied last, so you can change any of these. For
example, pass aria-live="assertive" for a region of urgent messages.
<Toast>
A single notification. Renders a <div> by default. Does not take a ref.
| Prop | Type | Default | Description |
|---|---|---|---|
as |
ValidConstructor |
'div' |
Element or component to render as. |
children |
JSX.Element |
none | The message. Not a render prop. |
| …rest | props of as |
none | Forwarded to the rendered element. |
Rendered attributes
| Attribute | Value |
|---|---|
tc-toast |
"" |
A toast has no live role. The Toaster around it announces it. For a message
that must interrupt, use Alert instead.