Merge pull request #32520 from backstage/bui-alert

BUI - Add new Alert component
This commit is contained in:
Charles de Dreuille
2026-01-26 19:56:05 +00:00
committed by GitHub
20 changed files with 1457 additions and 80 deletions
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/ui': patch
---
Added new `Alert` component with support for status variants (info, success, warning, danger), icons, loading states, and custom actions.
Updated status color tokens for improved contrast and consistency across light and dark themes:
- Added new `--bui-bg-info` and `--bui-fg-info` tokens for info status
- Updated `--bui-bg-danger`, `--bui-fg-danger` tokens
- Updated `--bui-bg-warning`, `--bui-fg-warning` tokens
- Updated `--bui-bg-success`, `--bui-fg-success` tokens
**Affected components**: Alert
File diff suppressed because one or more lines are too long
@@ -0,0 +1,168 @@
'use client';
import { Alert } from '../../../../../packages/ui/src/components/Alert/Alert';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { Button } from '../../../../../packages/ui/src/components/Button/Button';
import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
import { RiCloseLine, RiCloudLine } from '@remixicon/react';
export const Default = () => {
return <Alert status="info" icon={true} title="This is an alert message" />;
};
export const StatusVariants = () => {
return (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
title="This is an informational alert with helpful information."
/>
<Alert
status="success"
icon={true}
title="Your changes have been saved successfully."
/>
<Alert
status="warning"
icon={true}
title="This action may have unintended consequences."
/>
<Alert
status="danger"
icon={true}
title="An error occurred while processing your request."
/>
</Flex>
);
};
export const WithDescription = () => {
return (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
title="New Feature Available"
description="We've added support for custom table columns. Check the documentation to learn more."
/>
<Alert
status="success"
icon={true}
title="Deployment Successful"
description="Your application has been deployed to production. All health checks passed."
/>
<Alert
status="warning"
icon={true}
title="Pending Review"
description="Please review the following items before proceeding with the deployment."
/>
<Alert
status="danger"
icon={true}
title="Authentication Failed"
description="Unable to verify your credentials. Please check your username and password and try again."
/>
</Flex>
);
};
export const WithActions = () => {
return (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
title="This alert has a dismiss action on the right."
customActions={
<Button size="small" variant="tertiary">
Dismiss
</Button>
}
/>
<Alert
status="success"
icon={true}
title="Your changes have been saved. Would you like to continue?"
customActions={
<ButtonIcon
size="small"
variant="tertiary"
icon={<RiCloseLine />}
aria-label="Close"
/>
}
/>
</Flex>
);
};
export const WithActionsAndDescriptions = () => {
return (
<Alert
status="warning"
icon={true}
title="Update Available"
description="A new version of the application is ready to install. This will require a brief restart."
customActions={
<>
<Button size="small" variant="tertiary">
Later
</Button>
<Button size="small" variant="primary">
Update Now
</Button>
</>
}
/>
);
};
export const LoadingStates = () => {
return (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
loading
title="Processing your request..."
/>
<Alert status="success" icon={true} loading title="Saving changes..." />
<Alert
status="info"
icon={true}
loading
title="Processing your request"
description="This may take a few moments. Please do not close this window."
/>
</Flex>
);
};
export const WithoutIcons = () => {
return (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={false}
title="This is an informational alert without an icon."
/>
<Alert
status="success"
icon={false}
title="Your changes have been saved successfully."
/>
</Flex>
);
};
export const CustomIcon = () => {
return (
<Alert
status="info"
icon={<RiCloudLine />}
title="This alert uses a custom cloud icon instead of the default info icon."
/>
);
};
+120
View File
@@ -0,0 +1,120 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { alertPropDefs } from './props-definition';
import {
alertUsageSnippet,
defaultSnippet,
statusVariantsSnippet,
withDescriptionSnippet,
withActionsSnippet,
loadingStatesSnippet,
withoutIconsSnippet,
customIconSnippet,
} from './snippets';
import {
Default,
StatusVariants,
WithDescription,
WithActions,
LoadingStates,
WithoutIcons,
CustomIcon,
} from './components';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { AlertDefinition } from '../../../utils/definitions';
<PageTitle
title="Alert"
description="A component for displaying alert messages with different status levels and optional actions."
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
## Usage
<CodeBlock code={alertUsageSnippet} />
## API reference
<PropsTable data={alertPropDefs} />
## Examples
### Status Variants
The Alert component supports four status variants, each with its own color theme.
<Snippet
align="center"
py={4}
open
preview={<StatusVariants />}
code={statusVariantsSnippet}
/>
### With Description
Add a description to provide additional context or details.
<Snippet
align="center"
py={4}
open
preview={<WithDescription />}
code={withDescriptionSnippet}
/>
### With Actions
Include custom actions like buttons for interactive alerts.
<Snippet
align="center"
py={4}
open
preview={<WithActions />}
code={withActionsSnippet}
/>
### Loading States
The loading spinner replaces the icon to indicate an ongoing process.
<Snippet
align="center"
py={4}
open
preview={<LoadingStates />}
code={loadingStatesSnippet}
/>
### Without Icons
Disable icons for a simpler appearance.
<Snippet
align="center"
py={4}
open
preview={<WithoutIcons />}
code={withoutIconsSnippet}
/>
### Custom Icon
Provide a custom icon element instead of the default status icon.
<Snippet
align="center"
py={4}
open
preview={<CustomIcon />}
code={customIconSnippet}
/>
<Theming definition={AlertDefinition} />
<ChangelogComponent component="alert" />
@@ -0,0 +1,76 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const alertPropDefs: Record<string, PropDef> = {
status: {
type: 'enum',
values: ['info', 'success', 'warning', 'danger'],
responsive: true,
default: 'info',
},
icon: {
type: 'enum',
values: ['boolean', 'React.ReactElement'],
responsive: false,
},
loading: {
type: 'enum',
values: ['boolean'],
responsive: false,
},
title: {
type: 'enum',
values: ['React.ReactNode'],
responsive: false,
},
description: {
type: 'enum',
values: ['React.ReactNode'],
responsive: false,
},
customActions: {
type: 'enum',
values: ['React.ReactNode'],
responsive: false,
},
m: {
type: 'enum',
values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
responsive: true,
},
mx: {
type: 'enum',
values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
responsive: true,
},
my: {
type: 'enum',
values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
responsive: true,
},
mt: {
type: 'enum',
values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
responsive: true,
},
mb: {
type: 'enum',
values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
responsive: true,
},
ml: {
type: 'enum',
values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
responsive: true,
},
mr: {
type: 'enum',
values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
responsive: true,
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,131 @@
export const alertUsageSnippet = `import { Alert } from '@backstage/ui';
<Alert status="info" title="This is an informational message" />`;
export const defaultSnippet = `<Alert status="info" icon={true} title="This is an alert message" />`;
export const statusVariantsSnippet = `<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
title="This is an informational alert with helpful information."
/>
<Alert
status="success"
icon={true}
title="Your changes have been saved successfully."
/>
<Alert
status="warning"
icon={true}
title="This action may have unintended consequences."
/>
<Alert
status="danger"
icon={true}
title="An error occurred while processing your request."
/>
</Flex>`;
export const withDescriptionSnippet = `<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
title="New Feature Available"
description="We've added support for custom table columns. Check the documentation to learn more."
/>
<Alert
status="success"
icon={true}
title="Deployment Successful"
description="Your application has been deployed to production. All health checks passed."
/>
<Alert
status="warning"
icon={true}
title="Pending Review"
description="Please review the following items before proceeding with the deployment."
/>
<Alert
status="danger"
icon={true}
title="Authentication Failed"
description="Unable to verify your credentials. Please check your username and password and try again."
/>
</Flex>`;
export const withActionsSnippet = `<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
title="This alert has a dismiss action on the right."
customActions={
<Button size="small" variant="tertiary">
Dismiss
</Button>
}
/>
<Alert
status="success"
icon={true}
title="Your changes have been saved. Would you like to continue?"
customActions={
<ButtonIcon
size="small"
variant="tertiary"
icon={<RiCloseLine />}
aria-label="Close"
/>
}
/>
</Flex>`;
export const withActionsAndDescriptionsSnippet = `<Alert
status="warning"
icon={true}
title="Update Available"
description="A new version of the application is ready to install. This will require a brief restart."
customActions={
<>
<Button size="small" variant="tertiary">
Later
</Button>
<Button size="small" variant="primary">
Update Now
</Button>
</>
}
/>`;
export const loadingStatesSnippet = `<Flex direction="column" gap="4">
<Alert status="info" icon={true} loading title="Processing your request..." />
<Alert status="success" icon={true} loading title="Saving changes..." />
<Alert
status="info"
icon={true}
loading
title="Processing your request"
description="This may take a few moments. Please do not close this window."
/>
</Flex>`;
export const withoutIconsSnippet = `<Flex direction="column" gap="4">
<Alert
status="info"
icon={false}
title="This is an informational alert without an icon."
/>
<Alert
status="success"
icon={false}
title="Your changes have been saved successfully."
/>
</Flex>`;
export const customIconSnippet = `import { RiCloudLine } from '@remixicon/react';
<Alert
status="info"
icon={<RiCloudLine />}
title="This alert uses a custom cloud icon instead of the default info icon."
/>`;
+4
View File
@@ -28,6 +28,10 @@ export const components: Page[] = [
title: 'Accordion',
slug: 'accordion',
},
{
title: 'Alert',
slug: 'alert',
},
{
title: 'Avatar',
slug: 'avatar',
+26 -25
View File
@@ -1,38 +1,39 @@
export type Component =
| 'accordion'
| 'alert'
| 'avatar'
| 'box'
| 'button'
| 'button-link'
| 'heading'
| 'text'
| 'button-icon'
| 'icon'
| 'tabs'
| 'menu'
| 'textfield'
| 'datatable'
| 'select'
| 'collapsible'
| 'accordion'
| 'checkbox'
| 'container'
| 'link'
| 'tooltip'
| 'scrollarea'
| 'flex'
| 'switch'
| 'grid'
| 'searchfield'
| 'radio-group'
| 'button-link'
| 'card'
| 'skeleton'
| 'checkbox'
| 'collapsible'
| 'container'
| 'datatable'
| 'dialog'
| 'flex'
| 'grid'
| 'header'
| 'header-page'
| 'heading'
| 'icon'
| 'link'
| 'menu'
| 'password-field'
| 'radio-group'
| 'scrollarea'
| 'searchfield'
| 'select'
| 'skeleton'
| 'switch'
| 'table'
| 'visually-hidden'
| 'dialog'
| 'tag-group';
| 'tabs'
| 'tag-group'
| 'text'
| 'textfield'
| 'tooltip'
| 'visually-hidden';
export type Version = `${number}.${number}.${number}`;
+17 -11
View File
@@ -75,9 +75,10 @@
--bui-bg-neutral-on-surface-3-hover: oklch(0% 0 0 / .12);
--bui-bg-neutral-on-surface-3-pressed: oklch(0% 0 0 / .16);
--bui-bg-neutral-on-surface-3-disabled: oklch(0% 0 0 / .06);
--bui-bg-danger: #feebe7;
--bui-bg-warning: #fff2b2;
--bui-bg-success: #e6f6eb;
--bui-bg-danger: #ffe2e2;
--bui-bg-warning: #ffedd5;
--bui-bg-success: #dcfce7;
--bui-bg-info: #dbeafe;
--bui-fg-primary: var(--bui-black);
--bui-fg-secondary: var(--bui-gray-7);
--bui-fg-link: #1f5493;
@@ -87,13 +88,15 @@
--bui-fg-solid-disabled: #98a8bc;
--bui-fg-tint: #1f5493;
--bui-fg-tint-disabled: var(--bui-gray-5);
--bui-fg-danger: #e22b2b;
--bui-fg-warning: #e36d05;
--bui-fg-success: #1db954;
--bui-fg-danger: #991919;
--bui-fg-warning: #92310a;
--bui-fg-success: #116932;
--bui-fg-info: #173da6;
--bui-border: #0000001a;
--bui-border-hover: #0003;
--bui-border-pressed: #0006;
--bui-border-disabled: #0000001a;
--bui-border-info: #7ea9d6;
--bui-border-danger: #f87a7a;
--bui-border-warning: #e36d05;
--bui-border-success: #53db83;
@@ -137,9 +140,10 @@
--bui-bg-neutral-on-surface-3-hover: oklch(100% 0 0 / .12);
--bui-bg-neutral-on-surface-3-pressed: oklch(100% 0 0 / .2);
--bui-bg-neutral-on-surface-3-disabled: oklch(100% 0 0 / .08);
--bui-bg-danger: #3b1219;
--bui-bg-danger: #300c0c;
--bui-bg-warning: #302008;
--bui-bg-success: #132d21;
--bui-bg-success: #042713;
--bui-bg-info: #132049;
--bui-fg-primary: var(--bui-white);
--bui-fg-secondary: var(--bui-gray-7);
--bui-fg-link: #9cc9ff;
@@ -149,13 +153,15 @@
--bui-fg-solid-disabled: #6191cc;
--bui-fg-tint: #9cc9ff;
--bui-fg-tint-disabled: var(--bui-gray-5);
--bui-fg-danger: #e22b2b;
--bui-fg-warning: #e36d05;
--bui-fg-success: #1db954;
--bui-fg-danger: #fca5a5;
--bui-fg-warning: #fdba74;
--bui-fg-success: #86efac;
--bui-fg-info: #a3cfff;
--bui-border: #ffffff1f;
--bui-border-hover: #fff6;
--bui-border-pressed: #ffffff80;
--bui-border-disabled: #fff3;
--bui-border-info: #7ea9d6;
--bui-border-danger: #f87a7a;
--bui-border-warning: #e36d05;
--bui-border-success: #53db83;
+92 -30
View File
@@ -124,6 +124,61 @@ export interface AccordionTriggerProps extends HeadingProps {
title?: string;
}
// @public
export const Alert: ForwardRefExoticComponent<
AlertProps & RefAttributes<HTMLDivElement>
>;
// @public
export const AlertDefinition: {
readonly styles: {
readonly [key: string]: string;
};
readonly classNames: {
readonly root: 'bui-Alert';
readonly contentWrapper: 'bui-AlertContentWrapper';
readonly content: 'bui-AlertContent';
readonly title: 'bui-AlertTitle';
readonly description: 'bui-AlertDescription';
readonly icon: 'bui-AlertIcon';
readonly spinner: 'bui-AlertSpinner';
readonly actions: 'bui-AlertActions';
};
readonly surface: 'container';
readonly propDefs: {
readonly status: {
readonly dataAttribute: true;
readonly default: 'info';
};
readonly loading: {
readonly dataAttribute: true;
};
readonly icon: {};
readonly customActions: {};
readonly title: {};
readonly description: {};
readonly surface: {};
readonly className: {};
readonly style: {};
};
readonly utilityProps: readonly ['m', 'mb', 'ml', 'mr', 'mt', 'mx', 'my'];
};
// @public (undocumented)
export type AlertOwnProps = ContainerSurfaceProps & {
status?: Responsive<'info' | 'success' | 'warning' | 'danger'>;
icon?: boolean | ReactElement;
loading?: boolean;
customActions?: ReactNode;
title?: ReactNode;
description?: ReactNode;
className?: string;
style?: CSSProperties;
};
// @public
export interface AlertProps extends MarginProps, AlertOwnProps {}
// @public (undocumented)
export type AlignItems = 'stretch' | 'start' | 'center' | 'end';
@@ -1074,6 +1129,24 @@ export interface LinkProps extends LinkProps_2 {
weight?: TextWeights | Partial<Record<Breakpoint, TextWeights>>;
}
// @public (undocumented)
export interface MarginProps {
// (undocumented)
m?: Responsive<Space>;
// (undocumented)
mb?: Responsive<Space>;
// (undocumented)
ml?: Responsive<Space>;
// (undocumented)
mr?: Responsive<Space>;
// (undocumented)
mt?: Responsive<Space>;
// (undocumented)
mx?: Responsive<Space>;
// (undocumented)
my?: Responsive<Space>;
}
// @public (undocumented)
export const Menu: (props: MenuProps<object>) => JSX_2.Element;
@@ -1261,6 +1334,24 @@ type Option_2 = {
};
export { Option_2 as Option };
// @public (undocumented)
export interface PaddingProps {
// (undocumented)
p?: Responsive<Space>;
// (undocumented)
pb?: Responsive<Space>;
// (undocumented)
pl?: Responsive<Space>;
// (undocumented)
pr?: Responsive<Space>;
// (undocumented)
pt?: Responsive<Space>;
// (undocumented)
px?: Responsive<Space>;
// (undocumented)
py?: Responsive<Space>;
}
// @public (undocumented)
export interface PagePagination extends TablePaginationProps {
// (undocumented)
@@ -1516,36 +1607,7 @@ export type Space =
| string;
// @public (undocumented)
export interface SpaceProps {
// (undocumented)
m?: Responsive<Space>;
// (undocumented)
mb?: Responsive<Space>;
// (undocumented)
ml?: Responsive<Space>;
// (undocumented)
mr?: Responsive<Space>;
// (undocumented)
mt?: Responsive<Space>;
// (undocumented)
mx?: Responsive<Space>;
// (undocumented)
my?: Responsive<Space>;
// (undocumented)
p?: Responsive<Space>;
// (undocumented)
pb?: Responsive<Space>;
// (undocumented)
pl?: Responsive<Space>;
// (undocumented)
pr?: Responsive<Space>;
// (undocumented)
pt?: Responsive<Space>;
// (undocumented)
px?: Responsive<Space>;
// (undocumented)
py?: Responsive<Space>;
}
export interface SpaceProps extends MarginProps, PaddingProps {}
// @public (undocumented)
export const SubmenuTrigger: (props: SubmenuTriggerProps) => JSX_2.Element;
@@ -0,0 +1,137 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@layer tokens, base, components, utilities;
@layer components {
.bui-Alert {
--alert-bg: var(--bui-bg-surface-1);
--alert-fg: var(--bui-fg-primary);
display: flex;
gap: var(--bui-space-6);
padding: var(--bui-space-3) var(--bui-space-3);
border-radius: var(--bui-radius-3);
font-family: var(--bui-font-regular);
font-size: var(--bui-font-size-3);
line-height: 1.5;
/* Apply variables */
background-color: var(--alert-bg);
color: var(--alert-fg);
}
.bui-Alert[data-status='info'] {
--alert-bg: var(--bui-bg-info);
--alert-fg: var(--bui-fg-info);
}
.bui-Alert[data-status='success'] {
--alert-bg: var(--bui-bg-success);
--alert-fg: var(--bui-fg-success);
}
.bui-Alert[data-status='warning'] {
--alert-bg: var(--bui-bg-warning);
--alert-fg: var(--bui-fg-warning);
}
.bui-Alert[data-status='danger'] {
--alert-bg: var(--bui-bg-danger);
--alert-fg: var(--bui-fg-danger);
}
.bui-AlertIcon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
svg {
width: 1rem;
height: 1rem;
}
/* Add slight top margin when there's a description for better alignment */
.bui-Alert[data-has-description='true'] & {
margin-top: 0.125rem;
}
}
.bui-AlertContentWrapper {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: var(--bui-space-3);
flex: 1;
min-width: 0;
/* Center align when there's no description */
.bui-Alert[data-has-description='false'] & {
align-items: center;
}
}
.bui-AlertContent {
flex: 1;
min-width: 0;
word-wrap: break-word;
}
.bui-AlertTitle {
font-weight: var(--bui-font-weight-bold);
font-size: var(--bui-font-size-3);
}
.bui-AlertDescription {
font-size: var(--bui-font-size-2);
opacity: 0.9;
}
.bui-AlertSpinner {
display: flex;
& svg {
width: 1rem;
height: 1rem;
animation: bui-spin 1s linear infinite;
}
}
.bui-AlertActions {
flex-shrink: 0;
display: flex;
align-items: center;
gap: var(--bui-space-2);
margin-left: auto;
}
@media (prefers-reduced-motion: reduce) {
.bui-AlertSpinner svg {
animation: none;
}
}
@keyframes bui-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
@@ -0,0 +1,368 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import preview from '../../../../../.storybook/preview';
import { Alert } from './Alert';
import { Flex } from '../Flex';
import { Box } from '../Box';
import { Text } from '../Text';
import { Button } from '../Button';
import { RiCloudLine } from '@remixicon/react';
const meta = preview.meta({
title: 'Backstage UI/Alert',
component: Alert,
argTypes: {
status: {
control: 'select',
options: ['info', 'success', 'warning', 'danger'],
},
icon: {
control: 'boolean',
},
loading: {
control: 'boolean',
},
},
});
export const Default = meta.story({
args: {
title: 'This is an alert message',
icon: true,
},
});
export const StatusVariants = meta.story({
args: {
title: 'This is an alert message',
},
parameters: {
argTypes: {
status: {
control: false,
},
},
},
render: () => (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
title="This is an informational alert with helpful information."
/>
<Alert
status="success"
icon={true}
title="Your changes have been saved successfully."
/>
<Alert
status="warning"
icon={true}
title="This action may have unintended consequences."
/>
<Alert
status="danger"
icon={true}
title="An error occurred while processing your request."
/>
</Flex>
),
});
export const WithDescription = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
title="New Feature Available"
description="We've added support for custom table columns. Check the documentation to learn more."
/>
<Alert
status="success"
icon={true}
title="Deployment Successful"
description="Your application has been deployed to production. All health checks passed."
/>
<Alert
status="warning"
icon={true}
title="Pending Review"
description="Please review the following items before proceeding with the deployment."
/>
<Alert
status="danger"
icon={true}
title="Authentication Failed"
description="Unable to verify your credentials. Please check your username and password and try again."
/>
</Flex>
),
});
export const WithoutIcons = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={false}
title="This is an informational alert without an icon."
/>
<Alert
status="success"
icon={false}
title="Your changes have been saved successfully."
/>
<Alert
status="warning"
icon={false}
title="This action may have unintended consequences."
/>
<Alert
status="danger"
icon={false}
title="An error occurred while processing your request."
/>
</Flex>
),
});
export const CustomIcon = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={<RiCloudLine aria-hidden="true" />}
title="This alert uses a custom cloud icon instead of the default info icon."
/>
<Alert
status="success"
icon={<RiCloudLine aria-hidden="true" />}
title="Custom icons work with any status variant."
/>
</Flex>
),
});
export const WithActions = meta.story({
render: args => (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
title="This alert has a dismiss action on the right."
customActions={
<Button size="small" variant="tertiary">
Dismiss
</Button>
}
{...args}
/>
<Alert
status="success"
icon={true}
title="Your changes have been saved. Would you like to continue?"
customActions={
<>
<Button size="small" variant="tertiary">
Cancel
</Button>
<Button size="small" variant="primary">
Continue
</Button>
</>
}
{...args}
/>
<Alert
status="danger"
icon={true}
title="An error occurred while processing your request. Please try again."
customActions={
<Button size="small" variant="primary">
Retry
</Button>
}
{...args}
/>
</Flex>
),
});
export const WithActionsAndDescriptions = WithActions.extend({
args: {
description: 'This is a description of the alert.',
},
});
export const LoadingVariants = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Text>Info</Text>
<Alert
status="info"
icon={true}
loading
title="Processing your request..."
/>
<Text>Success</Text>
<Alert status="success" icon={true} loading title="Saving changes..." />
<Text>Warning</Text>
<Alert
status="warning"
icon={true}
loading
title="Checking for issues..."
/>
<Text>Danger</Text>
<Alert
status="danger"
icon={true}
loading
title="Attempting recovery..."
/>
</Flex>
),
});
export const LoadingWithDescription = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
loading
title="Processing your request"
description="This may take a few moments. Please do not close this window."
/>
<Alert
status="success"
icon={true}
loading
title="Deployment in Progress"
description="Your application is being deployed to production. You'll receive a notification when complete."
/>
</Flex>
),
});
export const LongContent = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
title="This is a longer alert message that demonstrates how the component handles multiple lines of text. The content will wrap naturally and maintain proper spacing with the icon and any actions. This is useful for providing detailed information to users when necessary."
/>
<Alert
status="warning"
icon={true}
title="This alert combines long content with actions. The actions remain aligned to the right even when the content wraps to multiple lines. This ensures a consistent and predictable layout regardless of content length."
customActions={
<Button size="small" variant="tertiary">
Dismiss
</Button>
}
/>
</Flex>
),
});
export const OnDifferentSurfaces = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Flex direction="column" gap="4">
<Text>Default Surface</Text>
<Flex direction="column" gap="2" p="4">
<Alert status="info" icon={true} title="Alert on default surface" />
<Alert
status="success"
icon={true}
title="Alert on default surface"
/>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 0</Text>
<Flex direction="column" gap="2" surface="0" p="4">
<Alert status="info" icon={true} title="Alert on surface 0" />
<Alert status="success" icon={true} title="Alert on surface 0" />
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 1</Text>
<Flex direction="column" gap="2" surface="1" p="4">
<Alert status="info" icon={true} title="Alert on surface 1" />
<Alert status="success" icon={true} title="Alert on surface 1" />
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 2</Text>
<Flex direction="column" gap="2" surface="2" p="4">
<Alert status="info" icon={true} title="Alert on surface 2" />
<Alert status="success" icon={true} title="Alert on surface 2" />
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 3</Text>
<Flex direction="column" gap="2" surface="3" p="4">
<Alert status="info" icon={true} title="Alert on surface 3" />
<Alert status="success" icon={true} title="Alert on surface 3" />
</Flex>
</Flex>
</Flex>
),
});
export const Responsive = meta.story({
args: {
title: 'This alert changes status responsively',
icon: true,
status: {
initial: 'info',
sm: 'success',
md: 'warning',
lg: 'danger',
},
},
});
export const WithUtilityProps = meta.story({
render: () => (
<Box surface="1" py="4">
<Alert
status="success"
icon={true}
title="Alert with custom margin"
mb="4"
mx="4"
/>
<Alert
status="success"
icon={true}
title="Alert with custom margin"
mx="4"
/>
</Box>
),
});
+165
View File
@@ -0,0 +1,165 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { forwardRef, Ref, isValidElement, ReactElement } from 'react';
import { ProgressBar } from 'react-aria-components';
import {
RiLoader4Line,
RiInformationLine,
RiCheckLine,
RiErrorWarningLine,
RiAlertLine,
} from '@remixicon/react';
import type { AlertProps } from './types';
import { useDefinition } from '../../hooks/useDefinition';
import { AlertDefinition } from './definition';
/**
* A component for displaying alert messages with different status levels.
*
* @remarks
* The Alert component supports multiple status variants (info, success, warning, danger)
* and can display icons, loading states, and custom actions. It automatically handles
* icon selection based on status when the icon prop is set to true.
*
* @example
* Basic usage with title only:
* ```tsx
* <Alert status="info" title="This is an informational message" />
* ```
*
* @example
* With title and description:
* ```tsx
* <Alert
* status="warning"
* icon={true}
* title="Pending Review"
* description="Please review the following items before proceeding."
* />
* ```
*
* @example
* With custom actions and loading state:
* ```tsx
* <Alert
* status="success"
* icon={true}
* title="Operation completed"
* description="Your changes have been saved successfully."
* loading={isProcessing}
* customActions={
* <>
* <Button size="small" variant="tertiary">Dismiss</Button>
* <Button size="small" variant="primary">View</Button>
* </>
* }
* />
* ```
*
* @public
*/
export const Alert = forwardRef(
(props: AlertProps, ref: Ref<HTMLDivElement>) => {
const { ownProps, restProps, dataAttributes, utilityStyle } = useDefinition(
AlertDefinition,
props,
);
const {
classes,
status,
icon,
loading,
customActions,
title,
description,
style,
} = ownProps;
// Determine which icon to render
const getStatusIcon = (): ReactElement | null => {
// If icon is explicitly false, don't render any icon
if (icon === false) {
return null;
}
// If icon is a custom React element, use it
if (isValidElement(icon)) {
return icon;
}
// If icon is true, auto-select based on status
if (icon === true) {
switch (status) {
case 'success':
return <RiCheckLine aria-hidden="true" />;
case 'warning':
return <RiErrorWarningLine aria-hidden="true" />;
case 'danger':
return <RiAlertLine aria-hidden="true" />;
case 'info':
default:
return <RiInformationLine aria-hidden="true" />;
}
}
// Default: no icon
return null;
};
const statusIcon = getStatusIcon();
return (
<div
className={classes.root}
ref={ref}
style={{ ...style, ...utilityStyle }}
{...dataAttributes}
{...restProps}
data-has-description={description ? 'true' : 'false'}
>
<div className={classes.contentWrapper}>
{loading ? (
<div className={classes.icon}>
<ProgressBar
aria-label="Loading"
isIndeterminate
className={classes.spinner}
>
<RiLoader4Line aria-hidden="true" />
</ProgressBar>
</div>
) : (
statusIcon && <div className={classes.icon}>{statusIcon}</div>
)}
<div className={classes.content}>
{title && <div className={classes.title}>{title}</div>}
{description && (
<div className={classes.description}>{description}</div>
)}
</div>
</div>
{customActions && (
<div className={classes.actions}>{customActions}</div>
)}
</div>
);
},
);
Alert.displayName = 'Alert';
@@ -0,0 +1,50 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineComponent } from '../../hooks/useDefinition';
import type { AlertOwnProps } from './types';
import styles from './Alert.module.css';
/**
* Component definition for Alert
* @public
*/
export const AlertDefinition = defineComponent<AlertOwnProps>()({
styles,
classNames: {
root: 'bui-Alert',
contentWrapper: 'bui-AlertContentWrapper',
content: 'bui-AlertContent',
title: 'bui-AlertTitle',
description: 'bui-AlertDescription',
icon: 'bui-AlertIcon',
spinner: 'bui-AlertSpinner',
actions: 'bui-AlertActions',
},
surface: 'container',
propDefs: {
status: { dataAttribute: true, default: 'info' },
loading: { dataAttribute: true },
icon: {},
customActions: {},
title: {},
description: {},
surface: {},
className: {},
style: {},
},
utilityProps: ['m', 'mb', 'ml', 'mr', 'mt', 'mx', 'my'],
});
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './Alert';
export * from './types';
export { AlertDefinition } from './definition';
+41
View File
@@ -0,0 +1,41 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ReactElement, ReactNode, CSSProperties } from 'react';
import type {
ContainerSurfaceProps,
Responsive,
MarginProps,
} from '../../types';
/** @public */
export type AlertOwnProps = ContainerSurfaceProps & {
status?: Responsive<'info' | 'success' | 'warning' | 'danger'>;
icon?: boolean | ReactElement;
loading?: boolean;
customActions?: ReactNode;
title?: ReactNode;
description?: ReactNode;
className?: string;
style?: CSSProperties;
};
/**
* Properties for {@link Alert}
*
* @public
*/
export interface AlertProps extends MarginProps, AlertOwnProps {}
+17 -11
View File
@@ -114,9 +114,10 @@
--bui-bg-neutral-on-surface-3-disabled: oklch(0% 0 0 / 6%);
/* Status background colors */
--bui-bg-danger: #feebe7;
--bui-bg-warning: #fff2b2;
--bui-bg-success: #e6f6eb;
--bui-bg-danger: #ffe2e2;
--bui-bg-warning: #ffedd5;
--bui-bg-success: #dcfce7;
--bui-bg-info: #dbeafe;
/* Foreground colors */
--bui-fg-primary: var(--bui-black);
@@ -128,15 +129,17 @@
--bui-fg-solid-disabled: #98a8bc;
--bui-fg-tint: #1f5493;
--bui-fg-tint-disabled: var(--bui-gray-5);
--bui-fg-danger: #e22b2b;
--bui-fg-warning: #e36d05;
--bui-fg-success: #1db954;
--bui-fg-danger: #991919;
--bui-fg-warning: #92310a;
--bui-fg-success: #116932;
--bui-fg-info: #173da6;
/* Border colors */
--bui-border: rgba(0, 0, 0, 0.1);
--bui-border-hover: rgba(0, 0, 0, 0.2);
--bui-border-pressed: rgba(0, 0, 0, 0.4);
--bui-border-disabled: rgba(0, 0, 0, 0.1);
--bui-border-info: #7ea9d6;
--bui-border-danger: #f87a7a;
--bui-border-warning: #e36d05;
--bui-border-success: #53db83;
@@ -199,9 +202,10 @@
--bui-bg-neutral-on-surface-3-disabled: oklch(100% 0 0 / 8%);
/* Status background colors */
--bui-bg-danger: #3b1219;
--bui-bg-danger: #300c0c;
--bui-bg-warning: #302008;
--bui-bg-success: #132d21;
--bui-bg-success: #042713;
--bui-bg-info: #132049;
/* Foreground colors */
--bui-fg-primary: var(--bui-white);
@@ -213,15 +217,17 @@
--bui-fg-solid-disabled: #6191cc;
--bui-fg-tint: #9cc9ff;
--bui-fg-tint-disabled: var(--bui-gray-5);
--bui-fg-danger: #e22b2b;
--bui-fg-warning: #e36d05;
--bui-fg-success: #1db954;
--bui-fg-danger: #fca5a5;
--bui-fg-warning: #fdba74;
--bui-fg-success: #86efac;
--bui-fg-info: #a3cfff;
/* Border colors */
--bui-border: rgba(255, 255, 255, 0.12);
--bui-border-hover: rgba(255, 255, 255, 0.4);
--bui-border-pressed: rgba(255, 255, 255, 0.5);
--bui-border-disabled: rgba(255, 255, 255, 0.2);
--bui-border-info: #7ea9d6;
--bui-border-danger: #f87a7a;
--bui-border-warning: #e36d05;
--bui-border-success: #53db83;
+1
View File
@@ -20,6 +20,7 @@
*/
export { AccordionDefinition } from './components/Accordion/definition';
export { AlertDefinition } from './components/Alert/definition';
export { AvatarDefinition } from './components/Avatar/definition';
export { BoxDefinition } from './components/Box/definition';
export { ButtonDefinition } from './components/Button/definition';
+2 -1
View File
@@ -27,10 +27,11 @@ export * from './components/Flex';
export * from './components/Container';
// UI components
export * from './components/Accordion';
export * from './components/Alert';
export * from './components/Avatar';
export * from './components/Button';
export * from './components/Card';
export * from './components/Accordion';
export * from './components/Dialog';
export * from './components/FieldLabel';
export * from './components/Header';
+8 -1
View File
@@ -92,7 +92,7 @@ export type Columns =
| 'auto';
/** @public */
export interface SpaceProps {
export interface MarginProps {
m?: Responsive<Space>;
mb?: Responsive<Space>;
ml?: Responsive<Space>;
@@ -100,6 +100,10 @@ export interface SpaceProps {
mt?: Responsive<Space>;
mx?: Responsive<Space>;
my?: Responsive<Space>;
}
/** @public */
export interface PaddingProps {
p?: Responsive<Space>;
pb?: Responsive<Space>;
pl?: Responsive<Space>;
@@ -109,6 +113,9 @@ export interface SpaceProps {
py?: Responsive<Space>;
}
/** @public */
export interface SpaceProps extends MarginProps, PaddingProps {}
/** @public */
export type TextVariants =
| 'title-large'