Toast
Generates toast notifications.
Based on Base UI’s toast component.
View source
import { For, createSignal } from "solid-js"
import { Toast } from "@danielfrg/solid-ui/toast"
import styles from "./index.module.css"
export function DemoToastHero() {
return (
<Toast.Provider>
<ToastButton />
<Toast.Portal>
<Toast.Viewport class={styles.Viewport}>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
)
}
function ToastButton() {
const toastManager = Toast.useToastManager()
const [count, setCount] = createSignal(0)
function createToast() {
setCount((prev) => prev + 1)
toastManager.add({
title: `Toast ${count()} created`,
description: "This is a toast notification.",
})
}
return (
<button type="button" class={styles.Button} onClick={createToast}>
Create toast
</button>
)
}
function ToastList() {
const { toasts } = Toast.useToastManager()
return (
<For each={toasts()}>
{(toast) => (
<Toast.Root toast={toast} class={styles.Toast}>
<Toast.Content class={styles.Content}>
<Toast.Title class={styles.Title} />
<Toast.Description class={styles.Description} />
<Toast.Close class={styles.Close} aria-label="Close">
<XIcon class={styles.Icon} />
</Toast.Close>
</Toast.Content>
</Toast.Root>
)}
</For>
)
}
function XIcon(props: { class?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class={props.class}
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
)
}/* ---- Shared trigger button ---- */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
cursor: pointer;
}
@media (hover: hover) {
.Button:hover {
background-color: var(--color-gray-100);
}
}
.Button:active {
background-color: var(--color-gray-100);
}
.Button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.ButtonGroup {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* ---- Bottom-right viewport (default) ---- */
.Viewport {
position: fixed;
z-index: 9999;
width: 250px;
bottom: 1rem;
right: 1rem;
left: auto;
top: auto;
}
@media (min-width: 500px) {
.Viewport {
bottom: 2rem;
right: 2rem;
width: 300px;
}
}
/* ---- Top-center viewport (position demo) ---- */
.ViewportTop {
position: fixed;
z-index: 9999;
width: 100%;
max-width: 300px;
top: 1rem;
right: 0;
left: 0;
margin: 0 auto;
bottom: auto;
}
/* ---- Bottom-right toast (default — stacks upward) ---- */
.Toast {
--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 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y, 0px)
);
position: absolute;
right: 0;
bottom: 0;
box-sizing: border-box;
width: 100%;
padding: 1rem;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
transform-origin: bottom center;
-webkit-user-select: none;
user-select: none;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
/* collapsed stack — scale + peek upward */
transform: translateX(var(--toast-swipe-movement-x, 0px))
translateY(
calc(var(--toast-swipe-movement-y, 0px) - (var(--toast-index) * var(--peek)) - (var(--shrink) * var(--height)))
)
scale(var(--scale));
}
/* expanded stack */
.Toast[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x, 0px)) translateY(var(--offset-y));
height: var(--toast-height);
}
/* enter */
.Toast[data-starting-style] {
transform: translateY(150%);
opacity: 0;
}
/* exit base */
.Toast[data-ending-style] {
transform: translateY(150%);
opacity: 0;
}
/* exit — swipe direction overrides */
.Toast[data-ending-style][data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
}
.Toast[data-ending-style][data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%)) translateY(var(--offset-y));
}
.Toast[data-ending-style][data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%)) translateY(var(--offset-y));
}
.Toast[data-ending-style][data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
}
.Toast[data-limited] {
opacity: 0;
}
/* gap-catcher so pointer can travel between toasts without leaving the hover zone */
.Toast::after {
content: "";
position: absolute;
top: 100%;
left: 0;
width: 100%;
height: calc(var(--gap) + 1px);
}
/* ---- Top-stacking toast (stacks downward) ---- */
.ToastTop {
--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, 0px));
position: absolute;
top: 0;
left: 0;
right: 0;
margin: 0 auto;
box-sizing: border-box;
width: 100%;
padding: 1rem;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
transform-origin: top center;
-webkit-user-select: none;
user-select: none;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
/* collapsed — scale + peek downward */
transform: translateX(var(--toast-swipe-movement-x, 0px))
translateY(
calc(var(--toast-swipe-movement-y, 0px) + (var(--toast-index) * var(--peek)) + (var(--shrink) * var(--height)))
)
scale(var(--scale));
}
.ToastTop[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x, 0px)) translateY(var(--offset-y));
height: var(--toast-height);
}
.ToastTop[data-starting-style] {
transform: translateY(-150%);
opacity: 0;
}
.ToastTop[data-ending-style] {
transform: translateY(-150%);
opacity: 0;
}
.ToastTop[data-ending-style][data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
}
.ToastTop[data-ending-style][data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%)) translateY(var(--offset-y));
}
.ToastTop[data-ending-style][data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%)) translateY(var(--offset-y));
}
.ToastTop[data-ending-style][data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
}
.ToastTop[data-limited] {
opacity: 0;
}
.ToastTop::after {
content: "";
position: absolute;
bottom: 100%;
left: 0;
width: 100%;
height: calc(var(--gap) + 1px);
}
/* ---- Content ---- */
.Content {
overflow: hidden;
transition: opacity 0.25s;
}
.Content[data-behind] {
opacity: 0;
}
.Content[data-expanded] {
opacity: 1;
}
.Title {
font-weight: 500;
font-size: 0.975rem;
line-height: 1.25rem;
margin: 0;
}
.Description {
font-size: 0.925rem;
line-height: 1.25rem;
color: var(--color-gray-600);
margin: 0.125rem 0 0;
}
/* ---- Close button ---- */
.Close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
border: none;
background: transparent;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
cursor: pointer;
color: var(--color-gray-500);
}
.Close:hover {
background-color: var(--color-gray-100);
color: var(--color-gray-700);
}
.Close:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.Icon {
width: 1rem;
height: 1rem;
}
/* ---- Action / Undo button ---- */
.UndoButton {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2rem;
padding: 0 0.75rem;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25rem;
border-radius: 0.25rem;
margin-top: 0.625rem;
background-color: var(--color-gray-900);
color: var(--color-gray-50);
border: none;
cursor: pointer;
}
.UndoButton:hover {
background-color: var(--color-gray-700);
}
.UndoButton:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
/* ---- Promise type indicators ---- */
.Toast[data-type="loading"] .Title::before {
content: "⏳ ";
}
.Toast[data-type="success"] {
border-left: 3px solid #16a34a;
}
.Toast[data-type="error"] {
border-left: 3px solid #dc2626;
}:root {
--color-blue: oklch(45% 50% 264deg);
--color-red: oklch(50% 55% 31deg);
--color-gray-50: oklch(98% 0.25% 264deg);
--color-gray-100: oklch(12% 9.5% 264deg / 5%);
--color-gray-200: oklch(12% 9% 264deg / 7%);
--color-gray-300: oklch(12% 8.5% 264deg / 17%);
--color-gray-400: oklch(12% 8% 264deg / 38%);
--color-gray-500: oklch(12% 7.5% 264deg / 50%);
--color-gray-600: oklch(12% 7% 264deg / 67%);
--color-gray-700: oklch(12% 6% 264deg / 77%);
--color-gray-800: oklch(12% 5% 264deg / 85%);
--color-gray-900: oklch(12% 5% 264deg / 90%);
--color-gray-950: oklch(12% 5% 264deg / 95%);
}
@media (prefers-color-scheme: dark) {
:root {
--color-blue: oklch(69% 50% 264deg);
--color-red: oklch(80% 55% 31deg);
--color-gray-50: oklch(17% 0.25% 264deg);
--color-gray-100: oklch(28% 0.75% 264deg / 65%);
--color-gray-200: oklch(29% 0.75% 264deg / 80%);
--color-gray-300: oklch(35% 0.75% 264deg / 80%);
--color-gray-400: oklch(47% 0.875% 264deg / 80%);
--color-gray-500: oklch(64% 1% 264deg / 80%);
--color-gray-600: oklch(82% 1% 264deg / 80%);
--color-gray-700: oklch(92% 1.125% 264deg / 80%);
--color-gray-800: oklch(93% 0.875% 264deg / 85%);
--color-gray-900: oklch(95% 0.5% 264deg / 90%);
--color-gray-950: oklch(94% 0.375% 264deg / 95%);
}
}Examples
Position
Use a top-aligned Viewport and swipeDirection="up" on each Root for a top-center stack.
View source
import { For, createSignal } from "solid-js"
import { Toast } from "@danielfrg/solid-ui/toast"
import styles from "./index.module.css"
export function DemoToastPosition() {
return (
<Toast.Provider>
<ToastButton />
<Toast.Portal>
<Toast.Viewport class={styles.ViewportTop}>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
)
}
function ToastButton() {
const toastManager = Toast.useToastManager()
const [count, setCount] = createSignal(0)
function createToast() {
setCount((prev) => prev + 1)
toastManager.add({
title: `Toast ${count()} created`,
description: "This is a toast notification.",
})
}
return (
<button type="button" class={styles.Button} onClick={createToast}>
Create toast
</button>
)
}
function ToastList() {
const { toasts } = Toast.useToastManager()
return (
<For each={toasts()}>
{(toast) => (
<Toast.Root toast={toast} swipeDirection="up" class={styles.ToastTop}>
<Toast.Content class={styles.Content}>
<Toast.Title class={styles.Title} />
<Toast.Description class={styles.Description} />
<Toast.Close class={styles.Close} aria-label="Close">
<XIcon class={styles.Icon} />
</Toast.Close>
</Toast.Content>
</Toast.Root>
)}
</For>
)
}
function XIcon(props: { class?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class={props.class}
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
)
}/* ---- Shared trigger button ---- */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
cursor: pointer;
}
@media (hover: hover) {
.Button:hover {
background-color: var(--color-gray-100);
}
}
.Button:active {
background-color: var(--color-gray-100);
}
.Button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.ButtonGroup {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* ---- Bottom-right viewport (default) ---- */
.Viewport {
position: fixed;
z-index: 9999;
width: 250px;
bottom: 1rem;
right: 1rem;
left: auto;
top: auto;
}
@media (min-width: 500px) {
.Viewport {
bottom: 2rem;
right: 2rem;
width: 300px;
}
}
/* ---- Top-center viewport (position demo) ---- */
.ViewportTop {
position: fixed;
z-index: 9999;
width: 100%;
max-width: 300px;
top: 1rem;
right: 0;
left: 0;
margin: 0 auto;
bottom: auto;
}
/* ---- Bottom-right toast (default — stacks upward) ---- */
.Toast {
--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 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y, 0px)
);
position: absolute;
right: 0;
bottom: 0;
box-sizing: border-box;
width: 100%;
padding: 1rem;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
transform-origin: bottom center;
-webkit-user-select: none;
user-select: none;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
/* collapsed stack — scale + peek upward */
transform: translateX(var(--toast-swipe-movement-x, 0px))
translateY(
calc(var(--toast-swipe-movement-y, 0px) - (var(--toast-index) * var(--peek)) - (var(--shrink) * var(--height)))
)
scale(var(--scale));
}
/* expanded stack */
.Toast[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x, 0px)) translateY(var(--offset-y));
height: var(--toast-height);
}
/* enter */
.Toast[data-starting-style] {
transform: translateY(150%);
opacity: 0;
}
/* exit base */
.Toast[data-ending-style] {
transform: translateY(150%);
opacity: 0;
}
/* exit — swipe direction overrides */
.Toast[data-ending-style][data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
}
.Toast[data-ending-style][data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%)) translateY(var(--offset-y));
}
.Toast[data-ending-style][data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%)) translateY(var(--offset-y));
}
.Toast[data-ending-style][data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
}
.Toast[data-limited] {
opacity: 0;
}
/* gap-catcher so pointer can travel between toasts without leaving the hover zone */
.Toast::after {
content: "";
position: absolute;
top: 100%;
left: 0;
width: 100%;
height: calc(var(--gap) + 1px);
}
/* ---- Top-stacking toast (stacks downward) ---- */
.ToastTop {
--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, 0px));
position: absolute;
top: 0;
left: 0;
right: 0;
margin: 0 auto;
box-sizing: border-box;
width: 100%;
padding: 1rem;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
transform-origin: top center;
-webkit-user-select: none;
user-select: none;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
/* collapsed — scale + peek downward */
transform: translateX(var(--toast-swipe-movement-x, 0px))
translateY(
calc(var(--toast-swipe-movement-y, 0px) + (var(--toast-index) * var(--peek)) + (var(--shrink) * var(--height)))
)
scale(var(--scale));
}
.ToastTop[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x, 0px)) translateY(var(--offset-y));
height: var(--toast-height);
}
.ToastTop[data-starting-style] {
transform: translateY(-150%);
opacity: 0;
}
.ToastTop[data-ending-style] {
transform: translateY(-150%);
opacity: 0;
}
.ToastTop[data-ending-style][data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
}
.ToastTop[data-ending-style][data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%)) translateY(var(--offset-y));
}
.ToastTop[data-ending-style][data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%)) translateY(var(--offset-y));
}
.ToastTop[data-ending-style][data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
}
.ToastTop[data-limited] {
opacity: 0;
}
.ToastTop::after {
content: "";
position: absolute;
bottom: 100%;
left: 0;
width: 100%;
height: calc(var(--gap) + 1px);
}
/* ---- Content ---- */
.Content {
overflow: hidden;
transition: opacity 0.25s;
}
.Content[data-behind] {
opacity: 0;
}
.Content[data-expanded] {
opacity: 1;
}
.Title {
font-weight: 500;
font-size: 0.975rem;
line-height: 1.25rem;
margin: 0;
}
.Description {
font-size: 0.925rem;
line-height: 1.25rem;
color: var(--color-gray-600);
margin: 0.125rem 0 0;
}
/* ---- Close button ---- */
.Close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
border: none;
background: transparent;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
cursor: pointer;
color: var(--color-gray-500);
}
.Close:hover {
background-color: var(--color-gray-100);
color: var(--color-gray-700);
}
.Close:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.Icon {
width: 1rem;
height: 1rem;
}
/* ---- Action / Undo button ---- */
.UndoButton {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2rem;
padding: 0 0.75rem;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25rem;
border-radius: 0.25rem;
margin-top: 0.625rem;
background-color: var(--color-gray-900);
color: var(--color-gray-50);
border: none;
cursor: pointer;
}
.UndoButton:hover {
background-color: var(--color-gray-700);
}
.UndoButton:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
/* ---- Promise type indicators ---- */
.Toast[data-type="loading"] .Title::before {
content: "⏳ ";
}
.Toast[data-type="success"] {
border-left: 3px solid #16a34a;
}
.Toast[data-type="error"] {
border-left: 3px solid #dc2626;
}:root {
--color-blue: oklch(45% 50% 264deg);
--color-red: oklch(50% 55% 31deg);
--color-gray-50: oklch(98% 0.25% 264deg);
--color-gray-100: oklch(12% 9.5% 264deg / 5%);
--color-gray-200: oklch(12% 9% 264deg / 7%);
--color-gray-300: oklch(12% 8.5% 264deg / 17%);
--color-gray-400: oklch(12% 8% 264deg / 38%);
--color-gray-500: oklch(12% 7.5% 264deg / 50%);
--color-gray-600: oklch(12% 7% 264deg / 67%);
--color-gray-700: oklch(12% 6% 264deg / 77%);
--color-gray-800: oklch(12% 5% 264deg / 85%);
--color-gray-900: oklch(12% 5% 264deg / 90%);
--color-gray-950: oklch(12% 5% 264deg / 95%);
}
@media (prefers-color-scheme: dark) {
:root {
--color-blue: oklch(69% 50% 264deg);
--color-red: oklch(80% 55% 31deg);
--color-gray-50: oklch(17% 0.25% 264deg);
--color-gray-100: oklch(28% 0.75% 264deg / 65%);
--color-gray-200: oklch(29% 0.75% 264deg / 80%);
--color-gray-300: oklch(35% 0.75% 264deg / 80%);
--color-gray-400: oklch(47% 0.875% 264deg / 80%);
--color-gray-500: oklch(64% 1% 264deg / 80%);
--color-gray-600: oklch(82% 1% 264deg / 80%);
--color-gray-700: oklch(92% 1.125% 264deg / 80%);
--color-gray-800: oklch(93% 0.875% 264deg / 85%);
--color-gray-900: oklch(95% 0.5% 264deg / 90%);
--color-gray-950: oklch(94% 0.375% 264deg / 95%);
}
}Promise
Pass a Promise to toastManager.promise() with loading, success, and error options. The toast
automatically transitions between states as the promise settles.
View source
import { For } from "solid-js"
import { Toast } from "@danielfrg/solid-ui/toast"
import styles from "./index.module.css"
export function DemoToastPromise() {
return (
<Toast.Provider>
<PromiseDemo />
<Toast.Portal>
<Toast.Viewport class={styles.Viewport}>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
)
}
function PromiseDemo() {
const toastManager = Toast.useToastManager()
function runPromise() {
toastManager.promise(
new Promise<string>((resolve, reject) => {
const shouldSucceed = Math.random() > 0.3
setTimeout(() => {
if (shouldSucceed) {
resolve("operation completed")
} else {
reject(new Error("operation failed"))
}
}, 2000)
}),
{
loading: "Loading data...",
success: (data: string) => `Success: ${data}`,
error: (err: unknown) => `Error: ${(err as Error).message}`,
},
)
}
return (
<button type="button" onClick={runPromise} class={styles.Button}>
Run promise
</button>
)
}
function ToastList() {
const { toasts } = Toast.useToastManager()
return (
<For each={toasts()}>
{(toast) => (
<Toast.Root toast={toast} class={styles.Toast}>
<Toast.Content class={styles.Content}>
<Toast.Title class={styles.Title} />
<Toast.Description class={styles.Description} />
<Toast.Close class={styles.Close} aria-label="Close">
<XIcon class={styles.Icon} />
</Toast.Close>
</Toast.Content>
</Toast.Root>
)}
</For>
)
}
function XIcon(props: { class?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class={props.class}
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
)
}/* ---- Shared trigger button ---- */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
cursor: pointer;
}
@media (hover: hover) {
.Button:hover {
background-color: var(--color-gray-100);
}
}
.Button:active {
background-color: var(--color-gray-100);
}
.Button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.ButtonGroup {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* ---- Bottom-right viewport (default) ---- */
.Viewport {
position: fixed;
z-index: 9999;
width: 250px;
bottom: 1rem;
right: 1rem;
left: auto;
top: auto;
}
@media (min-width: 500px) {
.Viewport {
bottom: 2rem;
right: 2rem;
width: 300px;
}
}
/* ---- Top-center viewport (position demo) ---- */
.ViewportTop {
position: fixed;
z-index: 9999;
width: 100%;
max-width: 300px;
top: 1rem;
right: 0;
left: 0;
margin: 0 auto;
bottom: auto;
}
/* ---- Bottom-right toast (default — stacks upward) ---- */
.Toast {
--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 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y, 0px)
);
position: absolute;
right: 0;
bottom: 0;
box-sizing: border-box;
width: 100%;
padding: 1rem;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
transform-origin: bottom center;
-webkit-user-select: none;
user-select: none;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
/* collapsed stack — scale + peek upward */
transform: translateX(var(--toast-swipe-movement-x, 0px))
translateY(
calc(var(--toast-swipe-movement-y, 0px) - (var(--toast-index) * var(--peek)) - (var(--shrink) * var(--height)))
)
scale(var(--scale));
}
/* expanded stack */
.Toast[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x, 0px)) translateY(var(--offset-y));
height: var(--toast-height);
}
/* enter */
.Toast[data-starting-style] {
transform: translateY(150%);
opacity: 0;
}
/* exit base */
.Toast[data-ending-style] {
transform: translateY(150%);
opacity: 0;
}
/* exit — swipe direction overrides */
.Toast[data-ending-style][data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
}
.Toast[data-ending-style][data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%)) translateY(var(--offset-y));
}
.Toast[data-ending-style][data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%)) translateY(var(--offset-y));
}
.Toast[data-ending-style][data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
}
.Toast[data-limited] {
opacity: 0;
}
/* gap-catcher so pointer can travel between toasts without leaving the hover zone */
.Toast::after {
content: "";
position: absolute;
top: 100%;
left: 0;
width: 100%;
height: calc(var(--gap) + 1px);
}
/* ---- Top-stacking toast (stacks downward) ---- */
.ToastTop {
--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, 0px));
position: absolute;
top: 0;
left: 0;
right: 0;
margin: 0 auto;
box-sizing: border-box;
width: 100%;
padding: 1rem;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
transform-origin: top center;
-webkit-user-select: none;
user-select: none;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
/* collapsed — scale + peek downward */
transform: translateX(var(--toast-swipe-movement-x, 0px))
translateY(
calc(var(--toast-swipe-movement-y, 0px) + (var(--toast-index) * var(--peek)) + (var(--shrink) * var(--height)))
)
scale(var(--scale));
}
.ToastTop[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x, 0px)) translateY(var(--offset-y));
height: var(--toast-height);
}
.ToastTop[data-starting-style] {
transform: translateY(-150%);
opacity: 0;
}
.ToastTop[data-ending-style] {
transform: translateY(-150%);
opacity: 0;
}
.ToastTop[data-ending-style][data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
}
.ToastTop[data-ending-style][data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%)) translateY(var(--offset-y));
}
.ToastTop[data-ending-style][data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%)) translateY(var(--offset-y));
}
.ToastTop[data-ending-style][data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
}
.ToastTop[data-limited] {
opacity: 0;
}
.ToastTop::after {
content: "";
position: absolute;
bottom: 100%;
left: 0;
width: 100%;
height: calc(var(--gap) + 1px);
}
/* ---- Content ---- */
.Content {
overflow: hidden;
transition: opacity 0.25s;
}
.Content[data-behind] {
opacity: 0;
}
.Content[data-expanded] {
opacity: 1;
}
.Title {
font-weight: 500;
font-size: 0.975rem;
line-height: 1.25rem;
margin: 0;
}
.Description {
font-size: 0.925rem;
line-height: 1.25rem;
color: var(--color-gray-600);
margin: 0.125rem 0 0;
}
/* ---- Close button ---- */
.Close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
border: none;
background: transparent;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
cursor: pointer;
color: var(--color-gray-500);
}
.Close:hover {
background-color: var(--color-gray-100);
color: var(--color-gray-700);
}
.Close:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.Icon {
width: 1rem;
height: 1rem;
}
/* ---- Action / Undo button ---- */
.UndoButton {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2rem;
padding: 0 0.75rem;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25rem;
border-radius: 0.25rem;
margin-top: 0.625rem;
background-color: var(--color-gray-900);
color: var(--color-gray-50);
border: none;
cursor: pointer;
}
.UndoButton:hover {
background-color: var(--color-gray-700);
}
.UndoButton:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
/* ---- Promise type indicators ---- */
.Toast[data-type="loading"] .Title::before {
content: "⏳ ";
}
.Toast[data-type="success"] {
border-left: 3px solid #16a34a;
}
.Toast[data-type="error"] {
border-left: 3px solid #dc2626;
}:root {
--color-blue: oklch(45% 50% 264deg);
--color-red: oklch(50% 55% 31deg);
--color-gray-50: oklch(98% 0.25% 264deg);
--color-gray-100: oklch(12% 9.5% 264deg / 5%);
--color-gray-200: oklch(12% 9% 264deg / 7%);
--color-gray-300: oklch(12% 8.5% 264deg / 17%);
--color-gray-400: oklch(12% 8% 264deg / 38%);
--color-gray-500: oklch(12% 7.5% 264deg / 50%);
--color-gray-600: oklch(12% 7% 264deg / 67%);
--color-gray-700: oklch(12% 6% 264deg / 77%);
--color-gray-800: oklch(12% 5% 264deg / 85%);
--color-gray-900: oklch(12% 5% 264deg / 90%);
--color-gray-950: oklch(12% 5% 264deg / 95%);
}
@media (prefers-color-scheme: dark) {
:root {
--color-blue: oklch(69% 50% 264deg);
--color-red: oklch(80% 55% 31deg);
--color-gray-50: oklch(17% 0.25% 264deg);
--color-gray-100: oklch(28% 0.75% 264deg / 65%);
--color-gray-200: oklch(29% 0.75% 264deg / 80%);
--color-gray-300: oklch(35% 0.75% 264deg / 80%);
--color-gray-400: oklch(47% 0.875% 264deg / 80%);
--color-gray-500: oklch(64% 1% 264deg / 80%);
--color-gray-600: oklch(82% 1% 264deg / 80%);
--color-gray-700: oklch(92% 1.125% 264deg / 80%);
--color-gray-800: oklch(93% 0.875% 264deg / 85%);
--color-gray-900: oklch(95% 0.5% 264deg / 90%);
--color-gray-950: oklch(94% 0.375% 264deg / 95%);
}
}Undo
Use actionProps when adding a toast to supply an action button. The action can close the current
toast and trigger follow-up behaviour.
View source
import { For } from "solid-js"
import { Toast } from "@danielfrg/solid-ui/toast"
import styles from "./index.module.css"
export function DemoToastUndo() {
return (
<Toast.Provider>
<Form />
<Toast.Portal>
<Toast.Viewport class={styles.Viewport}>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
)
}
function Form() {
const toastManager = Toast.useToastManager()
function action() {
const id = toastManager.add({
title: "Action performed",
description: "You can undo this action.",
type: "success",
actionProps: {
children: "Undo",
onClick() {
toastManager.close(id)
toastManager.add({
title: "Action undone",
})
},
},
})
}
return (
<button type="button" onClick={action} class={styles.Button}>
Perform action
</button>
)
}
function ToastList() {
const { toasts } = Toast.useToastManager()
return (
<For each={toasts()}>
{(toast) => (
<Toast.Root toast={toast} class={styles.Toast}>
<Toast.Content class={styles.Content}>
<Toast.Title class={styles.Title} />
<Toast.Description class={styles.Description} />
<Toast.Action class={styles.UndoButton} />
<Toast.Close class={styles.Close} aria-label="Close">
<XIcon class={styles.Icon} />
</Toast.Close>
</Toast.Content>
</Toast.Root>
)}
</For>
)
}
function XIcon(props: { class?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class={props.class}
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
)
}/* ---- Shared trigger button ---- */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
cursor: pointer;
}
@media (hover: hover) {
.Button:hover {
background-color: var(--color-gray-100);
}
}
.Button:active {
background-color: var(--color-gray-100);
}
.Button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.ButtonGroup {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* ---- Bottom-right viewport (default) ---- */
.Viewport {
position: fixed;
z-index: 9999;
width: 250px;
bottom: 1rem;
right: 1rem;
left: auto;
top: auto;
}
@media (min-width: 500px) {
.Viewport {
bottom: 2rem;
right: 2rem;
width: 300px;
}
}
/* ---- Top-center viewport (position demo) ---- */
.ViewportTop {
position: fixed;
z-index: 9999;
width: 100%;
max-width: 300px;
top: 1rem;
right: 0;
left: 0;
margin: 0 auto;
bottom: auto;
}
/* ---- Bottom-right toast (default — stacks upward) ---- */
.Toast {
--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 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y, 0px)
);
position: absolute;
right: 0;
bottom: 0;
box-sizing: border-box;
width: 100%;
padding: 1rem;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
transform-origin: bottom center;
-webkit-user-select: none;
user-select: none;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
/* collapsed stack — scale + peek upward */
transform: translateX(var(--toast-swipe-movement-x, 0px))
translateY(
calc(var(--toast-swipe-movement-y, 0px) - (var(--toast-index) * var(--peek)) - (var(--shrink) * var(--height)))
)
scale(var(--scale));
}
/* expanded stack */
.Toast[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x, 0px)) translateY(var(--offset-y));
height: var(--toast-height);
}
/* enter */
.Toast[data-starting-style] {
transform: translateY(150%);
opacity: 0;
}
/* exit base */
.Toast[data-ending-style] {
transform: translateY(150%);
opacity: 0;
}
/* exit — swipe direction overrides */
.Toast[data-ending-style][data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
}
.Toast[data-ending-style][data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%)) translateY(var(--offset-y));
}
.Toast[data-ending-style][data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%)) translateY(var(--offset-y));
}
.Toast[data-ending-style][data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
}
.Toast[data-limited] {
opacity: 0;
}
/* gap-catcher so pointer can travel between toasts without leaving the hover zone */
.Toast::after {
content: "";
position: absolute;
top: 100%;
left: 0;
width: 100%;
height: calc(var(--gap) + 1px);
}
/* ---- Top-stacking toast (stacks downward) ---- */
.ToastTop {
--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, 0px));
position: absolute;
top: 0;
left: 0;
right: 0;
margin: 0 auto;
box-sizing: border-box;
width: 100%;
padding: 1rem;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
transform-origin: top center;
-webkit-user-select: none;
user-select: none;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
/* collapsed — scale + peek downward */
transform: translateX(var(--toast-swipe-movement-x, 0px))
translateY(
calc(var(--toast-swipe-movement-y, 0px) + (var(--toast-index) * var(--peek)) + (var(--shrink) * var(--height)))
)
scale(var(--scale));
}
.ToastTop[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x, 0px)) translateY(var(--offset-y));
height: var(--toast-height);
}
.ToastTop[data-starting-style] {
transform: translateY(-150%);
opacity: 0;
}
.ToastTop[data-ending-style] {
transform: translateY(-150%);
opacity: 0;
}
.ToastTop[data-ending-style][data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
}
.ToastTop[data-ending-style][data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%)) translateY(var(--offset-y));
}
.ToastTop[data-ending-style][data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%)) translateY(var(--offset-y));
}
.ToastTop[data-ending-style][data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
}
.ToastTop[data-limited] {
opacity: 0;
}
.ToastTop::after {
content: "";
position: absolute;
bottom: 100%;
left: 0;
width: 100%;
height: calc(var(--gap) + 1px);
}
/* ---- Content ---- */
.Content {
overflow: hidden;
transition: opacity 0.25s;
}
.Content[data-behind] {
opacity: 0;
}
.Content[data-expanded] {
opacity: 1;
}
.Title {
font-weight: 500;
font-size: 0.975rem;
line-height: 1.25rem;
margin: 0;
}
.Description {
font-size: 0.925rem;
line-height: 1.25rem;
color: var(--color-gray-600);
margin: 0.125rem 0 0;
}
/* ---- Close button ---- */
.Close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
border: none;
background: transparent;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
cursor: pointer;
color: var(--color-gray-500);
}
.Close:hover {
background-color: var(--color-gray-100);
color: var(--color-gray-700);
}
.Close:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.Icon {
width: 1rem;
height: 1rem;
}
/* ---- Action / Undo button ---- */
.UndoButton {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2rem;
padding: 0 0.75rem;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25rem;
border-radius: 0.25rem;
margin-top: 0.625rem;
background-color: var(--color-gray-900);
color: var(--color-gray-50);
border: none;
cursor: pointer;
}
.UndoButton:hover {
background-color: var(--color-gray-700);
}
.UndoButton:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
/* ---- Promise type indicators ---- */
.Toast[data-type="loading"] .Title::before {
content: "⏳ ";
}
.Toast[data-type="success"] {
border-left: 3px solid #16a34a;
}
.Toast[data-type="error"] {
border-left: 3px solid #dc2626;
}:root {
--color-blue: oklch(45% 50% 264deg);
--color-red: oklch(50% 55% 31deg);
--color-gray-50: oklch(98% 0.25% 264deg);
--color-gray-100: oklch(12% 9.5% 264deg / 5%);
--color-gray-200: oklch(12% 9% 264deg / 7%);
--color-gray-300: oklch(12% 8.5% 264deg / 17%);
--color-gray-400: oklch(12% 8% 264deg / 38%);
--color-gray-500: oklch(12% 7.5% 264deg / 50%);
--color-gray-600: oklch(12% 7% 264deg / 67%);
--color-gray-700: oklch(12% 6% 264deg / 77%);
--color-gray-800: oklch(12% 5% 264deg / 85%);
--color-gray-900: oklch(12% 5% 264deg / 90%);
--color-gray-950: oklch(12% 5% 264deg / 95%);
}
@media (prefers-color-scheme: dark) {
:root {
--color-blue: oklch(69% 50% 264deg);
--color-red: oklch(80% 55% 31deg);
--color-gray-50: oklch(17% 0.25% 264deg);
--color-gray-100: oklch(28% 0.75% 264deg / 65%);
--color-gray-200: oklch(29% 0.75% 264deg / 80%);
--color-gray-300: oklch(35% 0.75% 264deg / 80%);
--color-gray-400: oklch(47% 0.875% 264deg / 80%);
--color-gray-500: oklch(64% 1% 264deg / 80%);
--color-gray-600: oklch(82% 1% 264deg / 80%);
--color-gray-700: oklch(92% 1.125% 264deg / 80%);
--color-gray-800: oklch(93% 0.875% 264deg / 85%);
--color-gray-900: oklch(95% 0.5% 264deg / 90%);
--color-gray-950: oklch(94% 0.375% 264deg / 95%);
}
}Custom data
Attach arbitrary typed data to a toast via the data option. Use a type guard to narrow the toast
type inside the list renderer.
View source
import { For, Show } from "solid-js"
import { Toast } from "@danielfrg/solid-ui/toast"
import type { ToastObject } from "@danielfrg/solid-ui/toast"
import styles from "./index.module.css"
interface CustomToastData extends Record<string, unknown> {
userId: string
}
function isCustomToast(toast: ToastObject): toast is ToastObject<CustomToastData> {
return typeof (toast.data as CustomToastData | undefined)?.userId === "string"
}
export function DemoToastCustomData() {
return (
<Toast.Provider>
<CustomToastButton />
<Toast.Portal>
<Toast.Viewport class={styles.Viewport}>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
)
}
function CustomToastButton() {
const toastManager = Toast.useToastManager()
function action() {
const data: CustomToastData = { userId: "123" }
toastManager.add({
title: "Toast with custom data",
data,
})
}
return (
<button type="button" onClick={action} class={styles.Button}>
Create custom toast
</button>
)
}
function ToastList() {
const { toasts } = Toast.useToastManager()
return (
<For each={toasts()}>
{(toast) => (
<Toast.Root toast={toast} class={styles.Toast}>
<Toast.Content class={styles.Content}>
<Toast.Title class={styles.Title}>{toast.title}</Toast.Title>
<Show
when={isCustomToast(toast) && (toast as ToastObject<CustomToastData>).data}
fallback={<Toast.Description class={styles.Description} />}
>
{(data) => (
<Toast.Description class={styles.Description}>`data.userId` is {data().userId}</Toast.Description>
)}
</Show>
<Toast.Close class={styles.Close} aria-label="Close">
<XIcon class={styles.Icon} />
</Toast.Close>
</Toast.Content>
</Toast.Root>
)}
</For>
)
}
function XIcon(props: { class?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class={props.class}
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
)
}/* ---- Shared trigger button ---- */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
cursor: pointer;
}
@media (hover: hover) {
.Button:hover {
background-color: var(--color-gray-100);
}
}
.Button:active {
background-color: var(--color-gray-100);
}
.Button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.ButtonGroup {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* ---- Bottom-right viewport (default) ---- */
.Viewport {
position: fixed;
z-index: 9999;
width: 250px;
bottom: 1rem;
right: 1rem;
left: auto;
top: auto;
}
@media (min-width: 500px) {
.Viewport {
bottom: 2rem;
right: 2rem;
width: 300px;
}
}
/* ---- Top-center viewport (position demo) ---- */
.ViewportTop {
position: fixed;
z-index: 9999;
width: 100%;
max-width: 300px;
top: 1rem;
right: 0;
left: 0;
margin: 0 auto;
bottom: auto;
}
/* ---- Bottom-right toast (default — stacks upward) ---- */
.Toast {
--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 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y, 0px)
);
position: absolute;
right: 0;
bottom: 0;
box-sizing: border-box;
width: 100%;
padding: 1rem;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
transform-origin: bottom center;
-webkit-user-select: none;
user-select: none;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
/* collapsed stack — scale + peek upward */
transform: translateX(var(--toast-swipe-movement-x, 0px))
translateY(
calc(var(--toast-swipe-movement-y, 0px) - (var(--toast-index) * var(--peek)) - (var(--shrink) * var(--height)))
)
scale(var(--scale));
}
/* expanded stack */
.Toast[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x, 0px)) translateY(var(--offset-y));
height: var(--toast-height);
}
/* enter */
.Toast[data-starting-style] {
transform: translateY(150%);
opacity: 0;
}
/* exit base */
.Toast[data-ending-style] {
transform: translateY(150%);
opacity: 0;
}
/* exit — swipe direction overrides */
.Toast[data-ending-style][data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
}
.Toast[data-ending-style][data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%)) translateY(var(--offset-y));
}
.Toast[data-ending-style][data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%)) translateY(var(--offset-y));
}
.Toast[data-ending-style][data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
}
.Toast[data-limited] {
opacity: 0;
}
/* gap-catcher so pointer can travel between toasts without leaving the hover zone */
.Toast::after {
content: "";
position: absolute;
top: 100%;
left: 0;
width: 100%;
height: calc(var(--gap) + 1px);
}
/* ---- Top-stacking toast (stacks downward) ---- */
.ToastTop {
--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, 0px));
position: absolute;
top: 0;
left: 0;
right: 0;
margin: 0 auto;
box-sizing: border-box;
width: 100%;
padding: 1rem;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
transform-origin: top center;
-webkit-user-select: none;
user-select: none;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
/* collapsed — scale + peek downward */
transform: translateX(var(--toast-swipe-movement-x, 0px))
translateY(
calc(var(--toast-swipe-movement-y, 0px) + (var(--toast-index) * var(--peek)) + (var(--shrink) * var(--height)))
)
scale(var(--scale));
}
.ToastTop[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x, 0px)) translateY(var(--offset-y));
height: var(--toast-height);
}
.ToastTop[data-starting-style] {
transform: translateY(-150%);
opacity: 0;
}
.ToastTop[data-ending-style] {
transform: translateY(-150%);
opacity: 0;
}
.ToastTop[data-ending-style][data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
}
.ToastTop[data-ending-style][data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%)) translateY(var(--offset-y));
}
.ToastTop[data-ending-style][data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%)) translateY(var(--offset-y));
}
.ToastTop[data-ending-style][data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
}
.ToastTop[data-limited] {
opacity: 0;
}
.ToastTop::after {
content: "";
position: absolute;
bottom: 100%;
left: 0;
width: 100%;
height: calc(var(--gap) + 1px);
}
/* ---- Content ---- */
.Content {
overflow: hidden;
transition: opacity 0.25s;
}
.Content[data-behind] {
opacity: 0;
}
.Content[data-expanded] {
opacity: 1;
}
.Title {
font-weight: 500;
font-size: 0.975rem;
line-height: 1.25rem;
margin: 0;
}
.Description {
font-size: 0.925rem;
line-height: 1.25rem;
color: var(--color-gray-600);
margin: 0.125rem 0 0;
}
/* ---- Close button ---- */
.Close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
border: none;
background: transparent;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
cursor: pointer;
color: var(--color-gray-500);
}
.Close:hover {
background-color: var(--color-gray-100);
color: var(--color-gray-700);
}
.Close:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.Icon {
width: 1rem;
height: 1rem;
}
/* ---- Action / Undo button ---- */
.UndoButton {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2rem;
padding: 0 0.75rem;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25rem;
border-radius: 0.25rem;
margin-top: 0.625rem;
background-color: var(--color-gray-900);
color: var(--color-gray-50);
border: none;
cursor: pointer;
}
.UndoButton:hover {
background-color: var(--color-gray-700);
}
.UndoButton:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
/* ---- Promise type indicators ---- */
.Toast[data-type="loading"] .Title::before {
content: "⏳ ";
}
.Toast[data-type="success"] {
border-left: 3px solid #16a34a;
}
.Toast[data-type="error"] {
border-left: 3px solid #dc2626;
}:root {
--color-blue: oklch(45% 50% 264deg);
--color-red: oklch(50% 55% 31deg);
--color-gray-50: oklch(98% 0.25% 264deg);
--color-gray-100: oklch(12% 9.5% 264deg / 5%);
--color-gray-200: oklch(12% 9% 264deg / 7%);
--color-gray-300: oklch(12% 8.5% 264deg / 17%);
--color-gray-400: oklch(12% 8% 264deg / 38%);
--color-gray-500: oklch(12% 7.5% 264deg / 50%);
--color-gray-600: oklch(12% 7% 264deg / 67%);
--color-gray-700: oklch(12% 6% 264deg / 77%);
--color-gray-800: oklch(12% 5% 264deg / 85%);
--color-gray-900: oklch(12% 5% 264deg / 90%);
--color-gray-950: oklch(12% 5% 264deg / 95%);
}
@media (prefers-color-scheme: dark) {
:root {
--color-blue: oklch(69% 50% 264deg);
--color-red: oklch(80% 55% 31deg);
--color-gray-50: oklch(17% 0.25% 264deg);
--color-gray-100: oklch(28% 0.75% 264deg / 65%);
--color-gray-200: oklch(29% 0.75% 264deg / 80%);
--color-gray-300: oklch(35% 0.75% 264deg / 80%);
--color-gray-400: oklch(47% 0.875% 264deg / 80%);
--color-gray-500: oklch(64% 1% 264deg / 80%);
--color-gray-600: oklch(82% 1% 264deg / 80%);
--color-gray-700: oklch(92% 1.125% 264deg / 80%);
--color-gray-800: oklch(93% 0.875% 264deg / 85%);
--color-gray-900: oklch(95% 0.5% 264deg / 90%);
--color-gray-950: oklch(94% 0.375% 264deg / 95%);
}
}Varying heights
The stack layout uses per-toast height measurements, so toasts of different sizes animate and collapse correctly.
View source
import { For, createSignal } from "solid-js"
import { Toast } from "@danielfrg/solid-ui/toast"
import styles from "./index.module.css"
const TEXTS = [
"Short message.",
"A bit longer message that spans two lines.",
"This is a longer description that intentionally takes more vertical space to demonstrate stacking with varying heights.",
"An even longer description that should span multiple lines so we can verify the clamped collapsed height and smooth expansion animation when hovering or focusing the viewport.",
]
export function DemoToastVaryingHeights() {
return (
<Toast.Provider>
<ToastButton />
<Toast.Portal>
<Toast.Viewport class={styles.Viewport}>
<ToastList />
</Toast.Viewport>
</Toast.Portal>
</Toast.Provider>
)
}
function ToastButton() {
const toastManager = Toast.useToastManager()
const [count, setCount] = createSignal(0)
function createToast() {
setCount((prev) => prev + 1)
const description = TEXTS[Math.floor(Math.random() * TEXTS.length)]
toastManager.add({
title: `Toast ${count()} created`,
description,
})
}
return (
<button type="button" class={styles.Button} onClick={createToast}>
Create varying height toast
</button>
)
}
function ToastList() {
const { toasts } = Toast.useToastManager()
return (
<For each={toasts()}>
{(toast) => (
<Toast.Root toast={toast} class={styles.Toast}>
<Toast.Content class={styles.Content}>
<Toast.Title class={styles.Title} />
<Toast.Description class={styles.Description} />
<Toast.Close class={styles.Close} aria-label="Close">
<XIcon class={styles.Icon} />
</Toast.Close>
</Toast.Content>
</Toast.Root>
)}
</For>
)
}
function XIcon(props: { class?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class={props.class}
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
)
}/* ---- Shared trigger button ---- */
.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;
cursor: pointer;
}
@media (hover: hover) {
.Button:hover {
background-color: var(--color-gray-100);
}
}
.Button:active {
background-color: var(--color-gray-100);
}
.Button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.ButtonGroup {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* ---- Bottom-right viewport (default) ---- */
.Viewport {
position: fixed;
z-index: 9999;
width: 250px;
bottom: 1rem;
right: 1rem;
left: auto;
top: auto;
}
@media (min-width: 500px) {
.Viewport {
bottom: 2rem;
right: 2rem;
width: 300px;
}
}
/* ---- Top-center viewport (position demo) ---- */
.ViewportTop {
position: fixed;
z-index: 9999;
width: 100%;
max-width: 300px;
top: 1rem;
right: 0;
left: 0;
margin: 0 auto;
bottom: auto;
}
/* ---- Bottom-right toast (default — stacks upward) ---- */
.Toast {
--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 + (var(--toast-index) * var(--gap) * -1) + var(--toast-swipe-movement-y, 0px)
);
position: absolute;
right: 0;
bottom: 0;
box-sizing: border-box;
width: 100%;
padding: 1rem;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
transform-origin: bottom center;
-webkit-user-select: none;
user-select: none;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
/* collapsed stack — scale + peek upward */
transform: translateX(var(--toast-swipe-movement-x, 0px))
translateY(
calc(var(--toast-swipe-movement-y, 0px) - (var(--toast-index) * var(--peek)) - (var(--shrink) * var(--height)))
)
scale(var(--scale));
}
/* expanded stack */
.Toast[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x, 0px)) translateY(var(--offset-y));
height: var(--toast-height);
}
/* enter */
.Toast[data-starting-style] {
transform: translateY(150%);
opacity: 0;
}
/* exit base */
.Toast[data-ending-style] {
transform: translateY(150%);
opacity: 0;
}
/* exit — swipe direction overrides */
.Toast[data-ending-style][data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
}
.Toast[data-ending-style][data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%)) translateY(var(--offset-y));
}
.Toast[data-ending-style][data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%)) translateY(var(--offset-y));
}
.Toast[data-ending-style][data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
}
.Toast[data-limited] {
opacity: 0;
}
/* gap-catcher so pointer can travel between toasts without leaving the hover zone */
.Toast::after {
content: "";
position: absolute;
top: 100%;
left: 0;
width: 100%;
height: calc(var(--gap) + 1px);
}
/* ---- Top-stacking toast (stacks downward) ---- */
.ToastTop {
--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, 0px));
position: absolute;
top: 0;
left: 0;
right: 0;
margin: 0 auto;
box-sizing: border-box;
width: 100%;
padding: 1rem;
background: var(--color-gray-50);
color: var(--color-gray-900);
border: 1px solid var(--color-gray-200);
border-radius: 0.5rem;
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
background-clip: padding-box;
transform-origin: top center;
-webkit-user-select: none;
user-select: none;
cursor: default;
z-index: calc(1000 - var(--toast-index));
height: var(--height);
transition:
transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.5s,
height 0.15s;
/* collapsed — scale + peek downward */
transform: translateX(var(--toast-swipe-movement-x, 0px))
translateY(
calc(var(--toast-swipe-movement-y, 0px) + (var(--toast-index) * var(--peek)) + (var(--shrink) * var(--height)))
)
scale(var(--scale));
}
.ToastTop[data-expanded] {
transform: translateX(var(--toast-swipe-movement-x, 0px)) translateY(var(--offset-y));
height: var(--toast-height);
}
.ToastTop[data-starting-style] {
transform: translateY(-150%);
opacity: 0;
}
.ToastTop[data-ending-style] {
transform: translateY(-150%);
opacity: 0;
}
.ToastTop[data-ending-style][data-swipe-direction="up"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
}
.ToastTop[data-ending-style][data-swipe-direction="left"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%)) translateY(var(--offset-y));
}
.ToastTop[data-ending-style][data-swipe-direction="right"] {
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%)) translateY(var(--offset-y));
}
.ToastTop[data-ending-style][data-swipe-direction="down"] {
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
}
.ToastTop[data-limited] {
opacity: 0;
}
.ToastTop::after {
content: "";
position: absolute;
bottom: 100%;
left: 0;
width: 100%;
height: calc(var(--gap) + 1px);
}
/* ---- Content ---- */
.Content {
overflow: hidden;
transition: opacity 0.25s;
}
.Content[data-behind] {
opacity: 0;
}
.Content[data-expanded] {
opacity: 1;
}
.Title {
font-weight: 500;
font-size: 0.975rem;
line-height: 1.25rem;
margin: 0;
}
.Description {
font-size: 0.925rem;
line-height: 1.25rem;
color: var(--color-gray-600);
margin: 0.125rem 0 0;
}
/* ---- Close button ---- */
.Close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0;
border: none;
background: transparent;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
cursor: pointer;
color: var(--color-gray-500);
}
.Close:hover {
background-color: var(--color-gray-100);
color: var(--color-gray-700);
}
.Close:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.Icon {
width: 1rem;
height: 1rem;
}
/* ---- Action / Undo button ---- */
.UndoButton {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2rem;
padding: 0 0.75rem;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25rem;
border-radius: 0.25rem;
margin-top: 0.625rem;
background-color: var(--color-gray-900);
color: var(--color-gray-50);
border: none;
cursor: pointer;
}
.UndoButton:hover {
background-color: var(--color-gray-700);
}
.UndoButton:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
/* ---- Promise type indicators ---- */
.Toast[data-type="loading"] .Title::before {
content: "⏳ ";
}
.Toast[data-type="success"] {
border-left: 3px solid #16a34a;
}
.Toast[data-type="error"] {
border-left: 3px solid #dc2626;
}:root {
--color-blue: oklch(45% 50% 264deg);
--color-red: oklch(50% 55% 31deg);
--color-gray-50: oklch(98% 0.25% 264deg);
--color-gray-100: oklch(12% 9.5% 264deg / 5%);
--color-gray-200: oklch(12% 9% 264deg / 7%);
--color-gray-300: oklch(12% 8.5% 264deg / 17%);
--color-gray-400: oklch(12% 8% 264deg / 38%);
--color-gray-500: oklch(12% 7.5% 264deg / 50%);
--color-gray-600: oklch(12% 7% 264deg / 67%);
--color-gray-700: oklch(12% 6% 264deg / 77%);
--color-gray-800: oklch(12% 5% 264deg / 85%);
--color-gray-900: oklch(12% 5% 264deg / 90%);
--color-gray-950: oklch(12% 5% 264deg / 95%);
}
@media (prefers-color-scheme: dark) {
:root {
--color-blue: oklch(69% 50% 264deg);
--color-red: oklch(80% 55% 31deg);
--color-gray-50: oklch(17% 0.25% 264deg);
--color-gray-100: oklch(28% 0.75% 264deg / 65%);
--color-gray-200: oklch(29% 0.75% 264deg / 80%);
--color-gray-300: oklch(35% 0.75% 264deg / 80%);
--color-gray-400: oklch(47% 0.875% 264deg / 80%);
--color-gray-500: oklch(64% 1% 264deg / 80%);
--color-gray-600: oklch(82% 1% 264deg / 80%);
--color-gray-700: oklch(92% 1.125% 264deg / 80%);
--color-gray-800: oklch(93% 0.875% 264deg / 85%);
--color-gray-900: oklch(95% 0.5% 264deg / 90%);
--color-gray-950: oklch(94% 0.375% 264deg / 95%);
}
}