Toast
v0.0.10
mix mishka.ui.gen.headless toast
Overview of the Phoenix and LiveView Headless Toast Component
The Mishka Chelekom Headless Toast ships zero opinionated styling - only the semantic markup and a polite aria-live viewport that stacks toasts, newest in front with older ones peeking behind. Hovering or focusing the viewport expands the stack into a full list and pauses auto-dismiss. Create toasts either by rendering :toast slots up front (static or LiveView-streamed), or by pairing a :trigger button with a :template - the engine clones a new toast on every click. Every part - trigger, viewport, toast, content, and close - is addressable through a *_class attribute, so you style it entirely with your own Tailwind classes while the stacking behavior keeps working underneath.
Base UI Toast example (Phoenix headless)
Base UI Toast example (Phoenix headless)
<.toast
id="toast-hero"
trigger_class="flex h-8 items-center justify-center gap-2 rounded-none border border-neutral-950 bg-white px-3 py-0 font-[inherit] text-sm leading-none whitespace-nowrap font-normal text-neutral-950 select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-white dark:bg-neutral-950 dark:text-white dark:hover:bg-neutral-800 dark:active:bg-neutral-700 disabled:border-neutral-500 disabled:text-neutral-500 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white"
viewport_class="fixed top-auto right-[1rem] bottom-[1rem] z-[1] mx-auto w-[calc(100vw-2rem)] sm:right-[2rem] sm:bottom-[2rem] sm:w-[22.5rem]"
toast_class="[--gap:0.75rem] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] absolute right-0 bottom-0 left-auto z-[calc(1000-var(--toast-index))] mr-0 w-full origin-bottom [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] border border-neutral-950 bg-white text-neutral-950 shadow-[0.25rem_0.25rem_0] shadow-black/12 select-none dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-[ending-style]:opacity-0 data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-[limited]:opacity-0 data-[starting-style]:[transform:translateY(150%)] h-[var(--height)] data-[expanded]:h-[var(--toast-height)] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s]"
content_class="flex h-full items-center gap-4 p-3 overflow-hidden transition-opacity duration-[250ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-[behind]:opacity-0 data-[expanded]:opacity-100"
close_class="flex h-8 shrink-0 items-center justify-center gap-2 rounded-none border border-neutral-950 bg-white px-3 py-0 font-[inherit] text-sm leading-none whitespace-nowrap font-normal text-neutral-950 hover:not-data-[disabled]:bg-neutral-100 active:not-data-[disabled]:bg-neutral-200 dark:border-white dark:bg-neutral-950 dark:text-white dark:hover:not-data-[disabled]:bg-neutral-800 dark:active:not-data-[disabled]:bg-neutral-700 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white"
>
<:trigger>Create toast</:trigger>
<:close>Dismiss</:close>
<:template>
<div class="flex min-w-0 flex-1 flex-col gap-1">
<div class="text-sm font-bold">Toast <span data-toast-count></span> created</div>
<div class="text-sm">This is a toast notification.</div>
</div>
</:template>
</.toast>Position
Position
<.toast
id="toast-position"
trigger_class="flex h-8 items-center justify-center gap-2 rounded-none border border-neutral-950 bg-white px-3 py-0 font-[inherit] text-sm leading-none whitespace-nowrap font-normal text-neutral-950 select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-white dark:bg-neutral-950 dark:text-white dark:hover:bg-neutral-800 dark:active:bg-neutral-700 disabled:border-neutral-500 disabled:text-neutral-500 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white"
viewport_class="fixed top-[1rem] right-0 bottom-auto left-0 z-[1] mx-auto w-[calc(100vw-2rem)] max-w-[22.5rem]"
toast_class="[--gap:0.75rem] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)+(var(--toast-index)*var(--gap))+var(--toast-swipe-movement-y))] absolute top-0 right-0 left-0 z-[calc(1000-var(--toast-index))] mx-auto w-full origin-top [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--peek))+(var(--shrink)*var(--height))))_scale(var(--scale))] border border-neutral-950 bg-white text-neutral-950 shadow-[0.25rem_0.25rem_0] shadow-black/12 select-none dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none after:absolute after:bottom-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-[ending-style]:opacity-0 data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-[limited]:opacity-0 data-[starting-style]:[transform:translateY(-150%)] h-[var(--height)] data-[expanded]:h-[var(--toast-height)] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s]"
content_class="flex h-full items-center gap-4 p-3 overflow-hidden transition-opacity duration-[250ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-[behind]:opacity-0 data-[expanded]:opacity-100"
close_class="flex h-8 shrink-0 items-center justify-center gap-2 rounded-none border border-neutral-950 bg-white px-3 py-0 font-[inherit] text-sm leading-none whitespace-nowrap font-normal text-neutral-950 hover:not-data-[disabled]:bg-neutral-100 active:not-data-[disabled]:bg-neutral-200 dark:border-white dark:bg-neutral-950 dark:text-white dark:hover:not-data-[disabled]:bg-neutral-800 dark:active:not-data-[disabled]:bg-neutral-700 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white"
>
<:trigger>Create toast</:trigger>
<:close>Dismiss</:close>
<:template>
<div class="flex min-w-0 flex-1 flex-col gap-1">
<div class="text-sm font-bold">Toast <span data-toast-count></span> created</div>
<div class="text-sm">This is a toast notification.</div>
</div>
</:template>
</.toast>Anchored (stacked manager)
Anchored (stacked manager)
<.toast
id="toast-anchored"
trigger_class="flex h-8 items-center justify-center gap-2 rounded-none border border-neutral-950 bg-white px-3 py-0 font-[inherit] text-sm leading-none whitespace-nowrap font-normal text-neutral-950 select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-white dark:bg-neutral-950 dark:text-white dark:hover:bg-neutral-800 dark:active:bg-neutral-700 disabled:border-neutral-500 disabled:text-neutral-500 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white"
viewport_class="fixed top-auto right-[1rem] bottom-[1rem] z-[1] mx-auto w-[calc(100vw-2rem)] sm:right-[2rem] sm:bottom-[2rem] sm:w-[22.5rem]"
toast_class="[--gap:0.75rem] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] absolute right-0 bottom-0 left-auto z-[calc(1000-var(--toast-index))] mr-0 w-full origin-bottom [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] border border-neutral-950 bg-white text-neutral-950 shadow-[0.25rem_0.25rem_0] shadow-black/12 select-none dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-[''] data-[ending-style]:opacity-0 data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] data-[limited]:opacity-0 data-[starting-style]:[transform:translateY(150%)] h-[var(--height)] data-[expanded]:h-[var(--toast-height)] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s]"
content_class="flex h-full items-center gap-4 p-3 overflow-hidden transition-opacity duration-[250ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-[behind]:opacity-0 data-[expanded]:opacity-100"
close_class="flex h-8 shrink-0 items-center justify-center gap-2 rounded-none border border-neutral-950 bg-white px-3 py-0 font-[inherit] text-sm leading-none whitespace-nowrap font-normal text-neutral-950 hover:not-data-[disabled]:bg-neutral-100 active:not-data-[disabled]:bg-neutral-200 dark:border-white dark:bg-neutral-950 dark:text-white dark:hover:not-data-[disabled]:bg-neutral-800 dark:active:not-data-[disabled]:bg-neutral-700 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white"
>
<:trigger>Stacked toast</:trigger>
<:close>Dismiss</:close>
<:template>
<div class="flex min-w-0 flex-1 flex-col gap-1">
<div class="text-sm">Copied</div>
</div>
</:template>
</.toast>Single toast - only the latest is shown (limit)
With a limit of 1, the toaster keeps only the newest toast visible - older ones are marked data-limited and faded to opacity-0 by the toast_class. So clicking Create toast again replaces the current toast instead of stacking a new copy. Raise the limit to allow a small stack.
Single toast - only the latest is shown (limit)
<.toast
id="toast-single"
limit={1}
trigger_class="flex h-8 items-center justify-center gap-2 rounded-none border border-neutral-950 bg-white px-3 py-0 font-[inherit] text-sm leading-none whitespace-nowrap font-normal text-neutral-950 select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-white dark:bg-neutral-950 dark:text-white dark:hover:bg-neutral-800 dark:active:bg-neutral-700 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white"
viewport_class="fixed top-auto right-[1rem] bottom-[1rem] z-[1] mx-auto w-[calc(100vw-2rem)] sm:right-[2rem] sm:bottom-[2rem] sm:w-[22.5rem]"
toast_class="[--gap:0.75rem] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] absolute right-0 bottom-0 left-auto z-[calc(1000-var(--toast-index))] mr-0 w-full origin-bottom border border-neutral-950 bg-white text-neutral-950 shadow-[0.25rem_0.25rem_0] shadow-black/12 select-none dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none data-[ending-style]:opacity-0 data-[limited]:opacity-0 data-[starting-style]:[transform:translateY(150%)] h-[var(--height)] data-[expanded]:h-[var(--toast-height)] [transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s]"
content_class="flex h-full items-center gap-4 p-3 overflow-hidden"
close_class="flex h-8 shrink-0 items-center justify-center gap-2 rounded-none border border-neutral-950 bg-white px-3 py-0 font-[inherit] text-sm leading-none whitespace-nowrap font-normal text-neutral-950 hover:bg-neutral-100 active:bg-neutral-200 dark:border-white dark:bg-neutral-950 dark:text-white dark:hover:bg-neutral-800 dark:active:bg-neutral-700"
>
<:trigger>Create toast</:trigger>
<:close>Dismiss</:close>
<:template>
<div class="flex min-w-0 flex-1 flex-col gap-1">
<div class="text-sm font-bold">Toast <span data-toast-count></span> (latest only)</div>
<div class="text-sm">Click again and it replaces this one instead of stacking.</div>
</div>
</:template>
</.toast>Parts & data attributes
Each part carries a stable data-part and a structural chelekom-toast__* class you can target, plus paired-presence state for the stacking/expand/dismiss transitions.
| Part | data-part | State / ARIA | Purpose |
|---|---|---|---|
| Root | root | data-limit | Carries the ToastRegion hook that owns the stacking engine. |
| Trigger | trigger | - | Creates a new toast from the template on click. |
| Viewport | viewport | aria-live="polite", aria-atomic="false", data-expanded | The live region and stacking container; expands on hover/focus and pauses auto-dismiss. |
| Template | template | - | Cloned per new toast; fill a data-toast-count element for a running number. |
| Toast | toast | data-duration, data-open, data-closed, data-expanded, data-limited, data-starting-style, data-ending-style | One notification (an li); carries the --toast-index/--toast-offset-y/--toast-height stacking vars. |
| Content | content | data-behind | Wraps the toast body and close button; fades when behind the front toast. |
| Close | close | aria-label | Dismiss button for the toast. |
Accessibility
Follows the WAI-ARIA aria-live region guidance: the viewport is a polite live region, so new toasts are announced without interrupting the user. Hovering or focusing the viewport expands the stack into a full list and pauses auto-dismiss timers, and each toast's close button is reachable by keyboard.
Attributes
| Name | Type | Default | Values | Required | Description |
|---|---|---|---|---|---|
id
|
string
| — | — | Yes | Unique id (anchors the live region) |
limit
|
integer
|
3
| — | No | Max toasts shown before older ones are marked `data-limited` |
duration
|
integer
|
5000
| — | No | Auto-dismiss (ms) for toasts created from the template (`0` disables) |
dedup_key
|
string
| — | — | No | Stable key: re-triggering refreshes the matching toast instead of stacking a duplicate |
class
|
any
| — | — | No | Extra classes for the root |
trigger_class
|
any
| — | — | No | Extra classes for the trigger button part |
viewport_class
|
any
| — | — | No | Extra classes for the viewport part (the live region) |
toast_class
|
any
| — | — | No | Extra classes for every toast part (the `<li>`) |
content_class
|
any
| — | — | No | Extra classes for every toast content part |
close_class
|
any
| — | — | No | Extra classes for every close button part |
close_label
|
string
|
Dismiss
| — | No | Accessible label for the close button |
rest
|
global
| — | — | No | — |
Slots
| Name | Required | Description |
|---|---|---|
trigger
| No | A button that creates a toast from the template on click |
close
| No | Custom content for the close button (defaults to a × glyph; e.g. a Base UI 'Dismiss' label) |
template
| No | Content for a dynamically-created toast (use `data-toast-count` for a running number) |
toast
| No | A static toast, rendered up front |
Attributes of the toast slot
| Name | Type | Default | Values | Required | Description |
|---|---|---|---|---|---|
duration
|
integer
| — | — | No | Auto-dismiss delay in ms (default 5000; 0 disables) |
Options
Each option takes one or more of its values, separated by commas.
| Option | Values |
|---|---|
--type
|
toast
|
--module
| A module name of your own |
What it generates
mix mishka.ui.gen.headless toast