diff --git a/.changeset/deep-crabs-dig.md b/.changeset/deep-crabs-dig.md
new file mode 100644
index 0000000000..0e0769c4ea
--- /dev/null
+++ b/.changeset/deep-crabs-dig.md
@@ -0,0 +1,5 @@
+---
+'@backstage/ui': patch
+---
+
+Fixed --bui-fg-success token in light mode to be more accessible.
diff --git a/.changeset/deprecate-alert-api.md b/.changeset/deprecate-alert-api.md
index 4041c9ad85..c9cd98a5a1 100644
--- a/.changeset/deprecate-alert-api.md
+++ b/.changeset/deprecate-alert-api.md
@@ -14,7 +14,7 @@ Deprecated `AlertApi` in favor of the new `ToastApi`.
- **Title and Description**: Display a prominent title with optional description text
- **Action Links**: Include clickable links within notifications
-- **Custom Icons**: Override default icons or disable them entirely
+- **Status Variants**: Support for neutral, info, success, warning, and danger statuses
- **Per-toast Timeout**: Control auto-dismiss timing for each notification individually
- **Programmatic Dismiss**: Close notifications via the key returned from `post()`
diff --git a/.changeset/toast-api-introduction.md b/.changeset/toast-api-introduction.md
index 24114797e6..b5fab6cf35 100644
--- a/.changeset/toast-api-introduction.md
+++ b/.changeset/toast-api-introduction.md
@@ -10,7 +10,7 @@ The new `ToastApi` provides enhanced notification capabilities compared to the e
- **Title and Description**: Toasts support both a title and an optional description
- **Custom Timeouts**: Each toast can specify its own timeout duration
- **Links**: Toasts can include action links
-- **Icons**: Support for custom icons or disabling the default icon
+- **Status Variants**: Support for neutral, info, success, warning, and danger statuses
- **Programmatic Dismiss**: Toasts can be dismissed programmatically using the key returned from `post()`
**Usage:**
diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md
index 0373cea873..647533b3b2 100644
--- a/packages/frontend-plugin-api/report.api.md
+++ b/packages/frontend-plugin-api/report.api.md
@@ -19,7 +19,6 @@ import { JSX as JSX_2 } from 'react/jsx-runtime';
import { JSX as JSX_3 } from 'react';
import { Observable } from '@backstage/types';
import { PropsWithChildren } from 'react';
-import { ReactElement } from 'react';
import { ReactNode } from 'react';
import { SwappableComponentRef as SwappableComponentRef_2 } from '@backstage/frontend-plugin-api';
import type { z } from 'zod';
@@ -1957,8 +1956,7 @@ export type ToastLink = {
export type ToastMessage = {
title: ReactNode;
description?: ReactNode;
- status?: 'info' | 'success' | 'warning' | 'danger';
- icon?: boolean | ReactElement;
+ status?: 'neutral' | 'info' | 'success' | 'warning' | 'danger';
links?: ToastLink[];
timeout?: number;
};
diff --git a/packages/frontend-plugin-api/src/apis/definitions/ToastApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ToastApi.ts
index a5d1c9707e..761a62e83f 100644
--- a/packages/frontend-plugin-api/src/apis/definitions/ToastApi.ts
+++ b/packages/frontend-plugin-api/src/apis/definitions/ToastApi.ts
@@ -16,7 +16,7 @@
import { createApiRef, ApiRef } from '../system';
import { Observable } from '@backstage/types';
-import { ReactNode, ReactElement } from 'react';
+import { ReactNode } from 'react';
/**
* Link item for toast notifications.
@@ -41,9 +41,7 @@ export type ToastMessage = {
/** Optional description text */
description?: ReactNode;
/** Status variant of the toast - defaults to 'success' */
- status?: 'info' | 'success' | 'warning' | 'danger';
- /** Whether to show an icon, or a custom icon element */
- icon?: boolean | ReactElement;
+ status?: 'neutral' | 'info' | 'success' | 'warning' | 'danger';
/** Optional array of links to display */
links?: ToastLink[];
/** Timeout in milliseconds before auto-dismiss. If not set, toast is permanent. */
@@ -65,7 +63,7 @@ export type ToastMessageWithKey = ToastMessage & {
*
* @remarks
* This API provides richer notification capabilities than the AlertApi,
- * including title/description, custom icons, links, and per-toast timeout control.
+ * including title/description, links, and per-toast timeout control.
*
* @example
* ```tsx
diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md
index c950aa4f3a..0e50b40e19 100644
--- a/plugins/app/report.api.md
+++ b/plugins/app/report.api.md
@@ -19,7 +19,6 @@ import { JSX as JSX_3 } from 'react/jsx-runtime';
import { NavContentComponent } from '@backstage/plugin-app-react';
import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api';
import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api';
-import type { ReactElement } from 'react';
import { ReactNode } from 'react';
import { RouteRef } from '@backstage/frontend-plugin-api';
import { SignInPageProps } from '@backstage/plugin-app-react';
@@ -1024,9 +1023,8 @@ export default appPlugin;
// @public
export interface ToastContent {
description?: ReactNode;
- icon?: boolean | ReactElement;
links?: ToastLink[];
- status?: 'info' | 'success' | 'warning' | 'danger';
+ status?: 'neutral' | 'info' | 'success' | 'warning' | 'danger';
title: ReactNode;
}
diff --git a/plugins/app/src/components/Toast/Toast.css b/plugins/app/src/components/Toast/Toast.css
index d35079d74a..050b2c6fa2 100644
--- a/plugins/app/src/components/Toast/Toast.css
+++ b/plugins/app/src/components/Toast/Toast.css
@@ -47,16 +47,14 @@
box-sizing: border-box;
display: flex;
align-items: flex-start;
- padding: 1rem;
- gap: 0.75rem;
+ padding: var(--bui-space-4);
+ gap: var(--bui-space-3);
/* Appearance */
- border-radius: 0.5rem;
- background-color: #1f1f1f;
- color: #ffffff;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
- Arial, sans-serif;
- font-size: 0.875rem;
+ border-radius: var(--bui-radius-3);
+ background-color: var(--bui-bg-surface-1);
+ font-family: var(--bui-font-regular);
+ font-size: var(--bui-font-size-3);
outline: none;
cursor: default;
user-select: none;
@@ -68,18 +66,17 @@
transform-origin: bottom center;
/* Shadow */
- box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.4);
-}
+ box-shadow: 0 4px 12px -2px rgba(0 0 0 / 0.4);
-/* Light mode toast (inverted from dark app theme) */
-[data-theme-mode='light'] .toast {
- background-color: #ffffff;
- color: #1f1f1f;
- box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
+ /* Focus ring */
+ &:focus-visible {
+ outline: 2px solid var(--bui-border-focus);
+ outline-offset: 2px;
+ }
}
.toast:focus-visible {
- outline: 2px solid #6366f1;
+ outline: 2px solid var(--bui-border-focus);
outline-offset: 2px;
}
@@ -97,30 +94,34 @@
}
/* Status variants - color icon and title */
+.toast[data-status='neutral'] .toast-title {
+ color: var(--bui-fg-primary);
+}
+
.toast[data-status='info'] .toast-icon,
.toast[data-status='info'] .toast-title {
- color: #3b82f6;
+ color: var(--bui-fg-info);
}
.toast[data-status='success'] .toast-icon,
.toast[data-status='success'] .toast-title {
- color: #22c55e;
+ color: var(--bui-fg-success);
}
.toast[data-status='warning'] .toast-icon,
.toast[data-status='warning'] .toast-title {
- color: #f59e0b;
+ color: var(--bui-fg-warning);
}
.toast[data-status='danger'] .toast-icon,
.toast[data-status='danger'] .toast-title {
- color: #ef4444;
+ color: var(--bui-fg-danger);
}
.toast-wrapper {
display: flex;
align-items: flex-start;
- gap: 0.75rem;
+ gap: var(--bui-space-3);
flex: 1;
min-width: 0;
}
@@ -128,7 +129,7 @@
.toast-content {
display: flex;
flex-direction: column;
- gap: 0.25rem;
+ gap: var(--bui-space-1);
}
/* Icon */
@@ -137,7 +138,7 @@
display: flex;
align-items: center;
justify-content: center;
- margin-top: 0.125rem;
+ margin-top: var(--bui-space-0_5);
}
.toast-icon svg {
@@ -147,29 +148,26 @@
/* Title */
.toast-title {
- font-weight: 600;
- font-size: 0.875rem;
+ font-weight: var(--bui-font-weight-bold);
+ font-size: var(--bui-font-size-3);
word-wrap: break-word;
- margin-top: 0.125rem;
+ margin-top: var(--bui-space-0_5);
}
/* Description */
.toast-description {
- color: rgba(255, 255, 255, 0.7);
- font-size: 0.875rem;
+ color: var(--bui-fg-secondary);
+ font-size: var(--bui-font-size-3);
+ opacity: 0.9;
word-wrap: break-word;
}
-[data-theme-mode='light'] .toast-description {
- color: rgba(0, 0, 0, 0.6);
-}
-
/* Links */
.toast-links {
display: flex;
flex-wrap: wrap;
- gap: 0.75rem;
- margin-top: 0.25rem;
+ gap: var(--bui-space-3);
+ margin-top: var(--bui-space-1);
}
.toast-links a {
@@ -205,11 +203,12 @@
margin: -0.25rem -0.25rem 0 0;
padding: 0;
border: none;
- border-radius: 0.25rem;
+ border-radius: var(--bui-radius-2);
background: transparent;
color: inherit;
cursor: pointer;
transition: background-color 0.15s ease;
+ color: var(--bui-fg-primary);
}
.toast-close-button svg {
@@ -218,22 +217,14 @@
}
.toast-close-button:hover {
- background-color: rgba(255, 255, 255, 0.1);
-}
-
-[data-theme-mode='light'] .toast-close-button:hover {
- background-color: rgba(0, 0, 0, 0.05);
+ background-color: var(--bui-bg-neutral-on-surface-1-hover);
}
.toast-close-button:focus-visible {
- outline: 2px solid #6366f1;
+ outline: 2px solid var(--bui-border-focus);
outline-offset: 1px;
}
.toast-close-button:active {
- background-color: rgba(255, 255, 255, 0.15);
-}
-
-[data-theme-mode='light'] .toast-close-button:active {
- background-color: rgba(0, 0, 0, 0.1);
+ background-color: var(--bui-bg-neutral-on-surface-1-pressed);
}
diff --git a/plugins/app/src/components/Toast/Toast.stories.tsx b/plugins/app/src/components/Toast/Toast.stories.tsx
index e74677e188..14a3dab126 100644
--- a/plugins/app/src/components/Toast/Toast.stories.tsx
+++ b/plugins/app/src/components/Toast/Toast.stories.tsx
@@ -35,10 +35,12 @@ const randomToasts = [
{ title: 'Error', status: 'danger' as const },
{ title: 'New notification', status: 'info' as const },
{ title: 'Warning', status: 'warning' as const },
+ { title: 'Task completed', status: 'neutral' as const },
// Title only - medium
{ title: 'Changes saved successfully', status: 'success' as const },
{ title: 'Connection restored', status: 'info' as const },
{ title: 'Action could not be completed', status: 'danger' as const },
+ { title: 'Background sync in progress', status: 'neutral' as const },
// Title + short description
{
title: 'Files uploaded',
@@ -60,6 +62,11 @@ const randomToasts = [
description: '90% used.',
status: 'warning' as const,
},
+ {
+ title: 'Clipboard updated',
+ description: 'Text copied.',
+ status: 'neutral' as const,
+ },
// Title + medium description
{
title: 'Deployment complete',
@@ -78,6 +85,11 @@ const randomToasts = [
description: 'You do not have access to perform this action.',
status: 'danger' as const,
},
+ {
+ title: 'Preferences updated',
+ description: 'Your display settings have been saved to your profile.',
+ status: 'neutral' as const,
+ },
// Title + long description
{
title: 'Sync completed',
@@ -106,6 +118,10 @@ const randomToasts = [
title: 'Multiple users are currently editing this document',
status: 'info' as const,
},
+ {
+ title: 'Your workspace has been switched to the new project',
+ status: 'neutral' as const,
+ },
];
export const Default = meta.story({
@@ -140,6 +156,17 @@ export const StatusVariants = meta.story({
<>
+
>
@@ -426,9 +452,9 @@ export const QueueManagement = meta.story({
toastQueue.add({
title: `Toast #${i}`,
description: `This is toast number ${i}.`,
- status: ['info', 'success', 'warning', 'danger'][
- (i - 1) % 4
- ] as 'info' | 'success' | 'warning' | 'danger',
+ status: ['neutral', 'info', 'success', 'warning', 'danger'][
+ (i - 1) % 5
+ ] as 'neutral' | 'info' | 'success' | 'warning' | 'danger',
});
}, i * 200);
}
@@ -463,8 +489,21 @@ export const AlertApiIntegration = meta.story({
info → info (blue)
warning → warning (orange)
error → danger (red)
+
+ (ToastApi also supports neutral - no icon, primary color)
+
+
+ toastQueue.add({
+ title: 'Workspace switched',
+ status: 'neutral',
+ })
+ }
+ >
+ Neutral Toast
+
toastQueue.add({
diff --git a/plugins/app/src/components/Toast/Toast.tsx b/plugins/app/src/components/Toast/Toast.tsx
index b910cfb095..c2a1258fa0 100644
--- a/plugins/app/src/components/Toast/Toast.tsx
+++ b/plugins/app/src/components/Toast/Toast.tsx
@@ -14,15 +14,7 @@
* limitations under the License.
*/
-import {
- forwardRef,
- Ref,
- isValidElement,
- ReactElement,
- useRef,
- useLayoutEffect,
- useState,
-} from 'react';
+import { forwardRef, Ref, useRef, useLayoutEffect, useState } from 'react';
import { useToast } from '@react-aria/toast';
import { useButton } from '@react-aria/button';
import { motion } from 'motion/react';
@@ -44,8 +36,8 @@ const manuallyClosingToasts = new Set();
*
* @remarks
* The Toast component is used internally by ToastContainer and managed by a ToastQueue.
- * It supports multiple status variants (info, success, warning, danger) and can display
- * a title, description, and optional icon. Toasts can be dismissed manually or automatically.
+ * It supports multiple status variants (neutral, info, success, warning, danger) and can display
+ * a title and description. Toasts can be dismissed manually or automatically.
*
* @internal
*/
@@ -58,7 +50,6 @@ export const Toast = forwardRef(
isExpanded = false,
onClose,
status,
- icon,
expandedY: expandedYProp = 0,
collapsedHeight,
naturalHeight,
@@ -134,37 +125,23 @@ export const Toast = forwardRef(
// Get content from toast
const content = toast.content;
const finalStatus = status || content.status || 'info';
- const finalIcon = icon !== undefined ? icon : content.icon;
- // Determine which icon to render
- const getStatusIcon = (): ReactElement | null => {
- // If icon is explicitly false, don't render any icon
- if (finalIcon === false) {
- return null;
+ // Determine which icon to render based on status
+ const getStatusIcon = () => {
+ switch (finalStatus) {
+ case 'neutral':
+ // Neutral status has no icon
+ return null;
+ case 'success':
+ return ;
+ case 'warning':
+ return ;
+ case 'danger':
+ return ;
+ case 'info':
+ default:
+ return ;
}
-
- // If icon is a custom React element, use it
- if (isValidElement(finalIcon)) {
- return finalIcon;
- }
-
- // If icon is true or undefined (default to true for toasts), auto-select based on status
- if (finalIcon === true || finalIcon === undefined) {
- switch (finalStatus) {
- case 'success':
- return ;
- case 'warning':
- return ;
- case 'danger':
- return ;
- case 'info':
- default:
- return ;
- }
- }
-
- // Default: no icon
- return null;
};
const statusIcon = getStatusIcon();
diff --git a/plugins/app/src/components/Toast/ToastDisplay.tsx b/plugins/app/src/components/Toast/ToastDisplay.tsx
index 984a484fd0..2740cd335f 100644
--- a/plugins/app/src/components/Toast/ToastDisplay.tsx
+++ b/plugins/app/src/components/Toast/ToastDisplay.tsx
@@ -95,7 +95,6 @@ export function ToastDisplay(props: ToastDisplayProps) {
title: toast.title,
description: toast.description,
status: toast.status ?? 'success',
- icon: toast.icon,
links: toast.links,
};
diff --git a/plugins/app/src/components/Toast/types.ts b/plugins/app/src/components/Toast/types.ts
index a203211c37..8cc6968423 100644
--- a/plugins/app/src/components/Toast/types.ts
+++ b/plugins/app/src/components/Toast/types.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import type { ReactElement, ReactNode } from 'react';
+import type { ReactNode } from 'react';
import type { ToastQueue, ToastState, QueuedToast } from 'react-stately';
/**
@@ -38,9 +38,7 @@ export interface ToastContent {
/** Optional description text */
description?: ReactNode;
/** Status variant of the toast */
- status?: 'info' | 'success' | 'warning' | 'danger';
- /** Whether to show an icon */
- icon?: boolean | ReactElement;
+ status?: 'neutral' | 'info' | 'success' | 'warning' | 'danger';
/** Optional array of links to display */
links?: ToastLink[];
}
@@ -61,9 +59,7 @@ export interface ToastProps {
/** Callback when toast is closed */
onClose?: () => void;
/** Override status from content */
- status?: 'info' | 'success' | 'warning' | 'danger';
- /** Override icon from content */
- icon?: boolean | ReactElement;
+ status?: 'neutral' | 'info' | 'success' | 'warning' | 'danger';
/** Pre-calculated Y position when expanded (based on heights of toasts below) */
expandedY?: number;
/** Height to use when collapsed (front toast's height, for uniform stacking) */