From 23b56c2eabe543a295842a83a96f326c3d07ebf9 Mon Sep 17 00:00:00 2001
From: gitpush-gitpaid
Date: Thu, 5 Mar 2026 03:37:19 -0500
Subject: [PATCH 1/3] Add configurable Toast position
---
packages/kumo/src/components/toast/index.ts | 7 +-
.../kumo/src/components/toast/toast.test.tsx | 140 +++++++++++++++++
packages/kumo/src/components/toast/toast.tsx | 148 ++++++++++++++++--
3 files changed, 282 insertions(+), 13 deletions(-)
create mode 100644 packages/kumo/src/components/toast/toast.test.tsx
diff --git a/packages/kumo/src/components/toast/index.ts b/packages/kumo/src/components/toast/index.ts
index b7735126cd..add2a9c727 100644
--- a/packages/kumo/src/components/toast/index.ts
+++ b/packages/kumo/src/components/toast/index.ts
@@ -1,4 +1,9 @@
export { Toasty, ToastProvider } from "./toast";
export { Toast } from "@base-ui/react/toast";
export { useKumoToastManager, createKumoToastManager } from "./toast";
-export type { KumoToastOptions, KumoToastManagerAddOptions } from "./toast";
+export type {
+ KumoToastOptions,
+ KumoToastManagerAddOptions,
+ KumoToastPosition,
+ ToastyProps,
+} from "./toast";
diff --git a/packages/kumo/src/components/toast/toast.test.tsx b/packages/kumo/src/components/toast/toast.test.tsx
new file mode 100644
index 0000000000..1f145f02ff
--- /dev/null
+++ b/packages/kumo/src/components/toast/toast.test.tsx
@@ -0,0 +1,140 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+import { Toasty, type KumoToastPosition, useKumoToastManager } from "./toast";
+
+const EXPECTED_VIEWPORT_ANCHOR_CLASSES: Record = {
+ "top-left": ["top-4", "left-4"],
+ "top-center": ["top-4", "left-4", "right-4"],
+ "top-right": ["top-4", "right-4"],
+ "bottom-left": ["bottom-4", "left-4"],
+ "bottom-center": ["bottom-4", "left-4", "right-4"],
+ "bottom-right": ["bottom-4", "right-4"],
+};
+
+function TriggerToastButton({
+ label,
+ title,
+ description,
+ position,
+}: {
+ label: string;
+ title: string;
+ description: string;
+ position?: KumoToastPosition;
+}) {
+ const toastManager = useKumoToastManager();
+
+ return (
+ toastManager.add({ title, description, position })}
+ >
+ {label}
+
+ );
+}
+
+function findViewport(position: KumoToastPosition) {
+ return document.querySelector(
+ `[data-kumo-toast-position="${position}"]`,
+ ) as HTMLElement | null;
+}
+
+describe("Toasty position", () => {
+ it("uses bottom-right by default", async () => {
+ render(
+
+
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: "Show toast" }));
+ await screen.findByText("Toast title");
+
+ const viewport = findViewport("bottom-right");
+ expect(viewport).toBeTruthy();
+ expect(viewport?.dataset.kumoToastViewport).toBe("true");
+ expect(viewport?.className).toContain("bottom-4");
+ expect(viewport?.className).toContain("right-4");
+ });
+
+ it.each(
+ Object.entries(EXPECTED_VIEWPORT_ANCHOR_CLASSES) as Array<
+ [KumoToastPosition, string[]]
+ >,
+ )("uses provider position %s", async (position, expectedClasses) => {
+ render(
+
+
+ ,
+ );
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "Show positioned toast" }),
+ );
+ await screen.findByText(`Toast in ${position}`);
+
+ const viewport = findViewport(position);
+ expect(viewport).toBeTruthy();
+
+ for (const className of expectedClasses) {
+ expect(viewport?.className).toContain(className);
+ }
+ });
+
+ it("allows per-toast position override", async () => {
+ render(
+
+
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: "Show top-left toast" }));
+ await screen.findByText("Override position toast");
+
+ expect(findViewport("top-left")).toBeTruthy();
+ expect(findViewport("bottom-right")).toBeNull();
+ });
+
+ it("renders toasts in multiple lanes", async () => {
+ render(
+
+
+
+ ,
+ );
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "Show default lane toast" }),
+ );
+ fireEvent.click(screen.getByRole("button", { name: "Show top lane toast" }));
+
+ await screen.findByText("Default lane toast");
+ await screen.findByText("Top lane toast");
+
+ expect(findViewport("bottom-right")).toBeTruthy();
+ expect(findViewport("top-left")).toBeTruthy();
+ });
+});
diff --git a/packages/kumo/src/components/toast/toast.tsx b/packages/kumo/src/components/toast/toast.tsx
index 50cc52372c..9c0b57c514 100644
--- a/packages/kumo/src/components/toast/toast.tsx
+++ b/packages/kumo/src/components/toast/toast.tsx
@@ -95,6 +95,74 @@ export const KUMO_TOAST_STYLING = {
// Derived types from KUMO_TOAST_VARIANTS
export type KumoToastVariant = keyof typeof KUMO_TOAST_VARIANTS.variant;
+export type KumoToastPosition =
+ | "top-left"
+ | "top-center"
+ | "top-right"
+ | "bottom-left"
+ | "bottom-center"
+ | "bottom-right";
+
+const KUMO_TOAST_POSITIONS = [
+ "top-left",
+ "top-center",
+ "top-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right",
+] as const satisfies ReadonlyArray;
+
+const TOAST_VIEWPORT_BASE_CLASSES =
+ "fixed z-1 flex w-[calc(100%-2rem)] sm:w-[340px]";
+
+const TOAST_VIEWPORT_POSITION_CLASSES: Record = {
+ "top-left": "top-4 left-4 sm:top-8 sm:left-8",
+ "top-center":
+ "top-4 right-4 left-4 mx-auto sm:top-8 sm:right-auto sm:left-1/2 sm:-translate-x-1/2",
+ "top-right": "top-4 right-4 sm:top-8 sm:right-8",
+ "bottom-left": "bottom-4 left-4 sm:bottom-8 sm:left-8",
+ "bottom-center":
+ "right-4 bottom-4 left-4 mx-auto sm:right-auto sm:bottom-8 sm:left-1/2 sm:-translate-x-1/2",
+ "bottom-right": "right-4 bottom-4 sm:right-8 sm:bottom-8",
+};
+
+const TOAST_ROOT_POSITION_CLASSES: Record = {
+ "top-left": "left-0 right-auto top-0 bottom-auto origin-top",
+ "top-center": "left-0 right-0 top-0 bottom-auto mx-auto origin-top",
+ "top-right": "right-0 left-auto top-0 bottom-auto origin-top",
+ "bottom-left": "left-0 right-auto bottom-0 top-auto origin-bottom",
+ "bottom-center": "left-0 right-0 bottom-0 top-auto mx-auto origin-bottom",
+ "bottom-right": "right-0 left-auto bottom-0 top-auto origin-bottom",
+};
+
+type ToastVerticalEdge = "top" | "bottom";
+type AnyKumoToast = KumoToastOptions;
+type ToastLanes = Record>;
+
+function getToastVerticalEdge(position: KumoToastPosition): ToastVerticalEdge {
+ return position.startsWith("top") ? "top" : "bottom";
+}
+
+function groupToastsByPosition(
+ toasts: Array,
+ defaultPosition: KumoToastPosition,
+): ToastLanes {
+ const lanes: ToastLanes = {
+ "top-left": [],
+ "top-center": [],
+ "top-right": [],
+ "bottom-left": [],
+ "bottom-center": [],
+ "bottom-right": [],
+ };
+
+ for (const toast of toasts) {
+ const resolvedPosition = toast.position ?? defaultPosition;
+ lanes[resolvedPosition].push(toast);
+ }
+
+ return lanes;
+}
export interface KumoToastVariantsProps {
variant?: KumoToastVariant;
@@ -132,10 +200,14 @@ export function toastVariants({
export interface ToastyProps extends KumoToastVariantsProps {
/** Application content. Toasts render via a portal above this. */
children: React.ReactNode;
+ /** Default position used for toasts unless overridden per toast. @default "bottom-right" */
+ position?: KumoToastPosition;
}
type KumoToastOptionsBase = {
variant?: KumoToastVariant;
+ /** Optional per-toast placement override. */
+ position?: KumoToastPosition;
content?: React.ReactNode;
actions?: Array;
bump?: boolean;
@@ -249,7 +321,7 @@ export const createKumoToastManager = () => {
/**
* Toasty — toast notification provider and viewport.
*
- * Renders a `Toast.Provider` with a fixed-position viewport in the bottom-right corner.
+ * Renders a `Toast.Provider` with fixed-position viewport lanes.
* Toasts stack with smooth enter/exit animations, swipe-to-dismiss, and expand-on-hover.
*
* Built on `@base-ui/react/toast`.
@@ -261,14 +333,12 @@ export const createKumoToastManager = () => {
*
* ```
*/
-export function Toasty({ children }: ToastyProps) {
+export function Toasty({ children, position = "bottom-right" }: ToastyProps) {
return (
{children}
-
-
-
+
);
@@ -277,24 +347,78 @@ export function Toasty({ children }: ToastyProps) {
/** Alias for Toasty — provided for discoverability when migrating from other libraries */
export const ToastProvider = Toasty;
-function ToastList() {
+function ToastViewports({
+ defaultPosition,
+}: {
+ defaultPosition: KumoToastPosition;
+}) {
const { toasts } = useKumoToastManager();
+ const lanes = groupToastsByPosition(toasts, defaultPosition);
+
+ return KUMO_TOAST_POSITIONS.map((position) => {
+ return (
+
+ );
+ });
+}
+
+function ToastViewportLane({
+ position,
+ toasts,
+}: {
+ position: KumoToastPosition;
+ toasts: Array;
+}) {
+ if (toasts.length === 0) return null;
+
+ return (
+
+
+
+ );
+}
+
+function ToastList({
+ position,
+ toasts,
+}: {
+ position: KumoToastPosition;
+ toasts: Array;
+}) {
+ const verticalEdge = getToastVerticalEdge(position);
+
return toasts.map((toast) => (
From 0e5aef6a98f6fa25764a374c3874b6a7b8ebd326 Mon Sep 17 00:00:00 2001
From: gitpush-gitpaid
Date: Thu, 5 Mar 2026 03:43:40 -0500
Subject: [PATCH 2/3] chore(changeset): add toast position changeset
---
.changeset/tough-experts-glow.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/tough-experts-glow.md
diff --git a/.changeset/tough-experts-glow.md b/.changeset/tough-experts-glow.md
new file mode 100644
index 0000000000..5a10a1ef14
--- /dev/null
+++ b/.changeset/tough-experts-glow.md
@@ -0,0 +1,5 @@
+---
+"@cloudflare/kumo": minor
+---
+
+Added a configurable toast positioning that default to bottom-right with per-toast overrides
From fd101803906944619ebabbb1691d082b80182eb6 Mon Sep 17 00:00:00 2001
From: gitpush-gitpaid
Date: Wed, 18 Mar 2026 17:43:01 -0400
Subject: [PATCH 3/3] docs(toast): document Toasty position prop
---
.../src/components/demos/ToastDemo.tsx | 25 ++++++++
.../src/pages/components/toast.astro | 57 +++++++++++++++++++
2 files changed, 82 insertions(+)
diff --git a/packages/kumo-docs-astro/src/components/demos/ToastDemo.tsx b/packages/kumo-docs-astro/src/components/demos/ToastDemo.tsx
index 0b6cd5f11a..43d1928658 100644
--- a/packages/kumo-docs-astro/src/components/demos/ToastDemo.tsx
+++ b/packages/kumo-docs-astro/src/components/demos/ToastDemo.tsx
@@ -304,3 +304,28 @@ export function ToastPromiseDemo() {
);
}
+
+function ToastPositionButton() {
+ const toastManager = useKumoToastManager();
+
+ return (
+
+ toastManager.add({
+ title: "Toast positioned",
+ description: "This toast uses the Toasty provider position.",
+ })
+ }
+ >
+ Show top-right toast
+
+ );
+}
+
+export function ToastPositionDemo() {
+ return (
+
+
+
+ );
+}
diff --git a/packages/kumo-docs-astro/src/pages/components/toast.astro b/packages/kumo-docs-astro/src/pages/components/toast.astro
index 1d3f56d1a9..1e91fa838b 100644
--- a/packages/kumo-docs-astro/src/pages/components/toast.astro
+++ b/packages/kumo-docs-astro/src/pages/components/toast.astro
@@ -16,6 +16,7 @@ import {
ToastCustomContentDemo,
ToastActionsDemo,
ToastPromiseDemo,
+ ToastPositionDemo,
} from "../../components/demos/ToastDemo";
---
@@ -168,6 +169,45 @@ export function Layout({ children }) {
+
+
Position
+
+ Use the position prop on Toasty to control where toast notifications appear by default. The default
+ value is "bottom-right".
+
+
+
+ toastManager.add({
+ title: "Toast positioned",
+ description: "This toast uses the Toasty provider position.",
+ })
+ }
+ >
+ Show top-right toast
+
+`}
+ >
+
+
+
+ Supported values are "top-left", "top-center", "top-right", "bottom-left", "bottom-center", and "bottom-right". Individual toasts can also override the provider default by passing{" "}
+ position to toastManager.add(), so different toasts can render in different lanes at the same
+ time.
+
+
+
Success Action
@@ -285,6 +325,7 @@ toastManager.add({ title: "Third toast" });`}
+
@@ -298,6 +339,21 @@ toastManager.add({ title: "Third toast" });`}
The provider component that wraps your app and manages the toast system.
+
+ Toasty also accepts a position prop to control the default toast viewport placement. Supported
+ values are "top-left", "top-center", "top-right", "bottom-left", "bottom-center", and "bottom-right". The default is "bottom-right". Individual toasts can override that default with their own position value.
+
@@ -377,5 +433,6 @@ toastManager.promise(asyncFn(), {
+