From 2c219b98172209a245550ca85fc3f4d870885467 Mon Sep 17 00:00:00 2001 From: Johan Persson Date: Wed, 28 Jan 2026 16:27:01 +0100 Subject: [PATCH] feat(ui): add danger variant to Button component Added a `danger` variant for destructive actions like delete or remove. Uses solid red background with white text in both light and dark modes. - Added CSS styles with custom properties for future token migration - Updated Storybook stories to include danger variant examples - Updated docs-ui documentation to reflect the new variant Signed-off-by: Johan Persson --- .changeset/floppy-actors-show.md | 7 + .../src/app/components/button/components.tsx | 16 ++ docs-ui/src/app/components/button/page.mdx | 17 +- .../components/button/props-definition.tsx | 6 + docs-ui/src/app/components/button/snippets.ts | 12 ++ packages/ui/report.api.md | 4 + .../src/components/Button/Button.module.css | 86 ++++++++ .../src/components/Button/Button.stories.tsx | 184 ++++++++++++++++-- .../ui/src/components/Button/definition.ts | 1 + packages/ui/src/components/Button/types.ts | 1 + 10 files changed, 322 insertions(+), 12 deletions(-) create mode 100644 .changeset/floppy-actors-show.md diff --git a/.changeset/floppy-actors-show.md b/.changeset/floppy-actors-show.md new file mode 100644 index 0000000000..95bb711660 --- /dev/null +++ b/.changeset/floppy-actors-show.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Added `destructive` prop to Button for dangerous actions like delete or remove. Works with all variants (primary, secondary, tertiary). + +**Affected components:** Button diff --git a/docs-ui/src/app/components/button/components.tsx b/docs-ui/src/app/components/button/components.tsx index 558c852e9c..1fe13f6630 100644 --- a/docs-ui/src/app/components/button/components.tsx +++ b/docs-ui/src/app/components/button/components.tsx @@ -59,6 +59,22 @@ export const Disabled = () => { ); }; +export const Destructive = () => { + return ( + + + + + + ); +}; + export const Loading = () => { return ( `; +export const destructiveSnippet = ` + + + +`; + export const loadingSnippet = ``; diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index b6f1a173ef..b4211d2499 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -330,6 +330,9 @@ export const ButtonDefinition: { readonly dataAttribute: true; readonly default: 'primary'; }; + readonly destructive: { + readonly dataAttribute: true; + }; readonly loading: { readonly dataAttribute: true; }; @@ -445,6 +448,7 @@ export interface ButtonLinkProps export type ButtonOwnProps = LeafSurfaceProps & { size?: Responsive<'small' | 'medium'>; variant?: Responsive<'primary' | 'secondary' | 'tertiary'>; + destructive?: boolean; iconStart?: ReactElement; iconEnd?: ReactElement; loading?: boolean; diff --git a/packages/ui/src/components/Button/Button.module.css b/packages/ui/src/components/Button/Button.module.css index fd57682406..740e18f59e 100644 --- a/packages/ui/src/components/Button/Button.module.css +++ b/packages/ui/src/components/Button/Button.module.css @@ -79,6 +79,39 @@ } } + .bui-Button[data-variant='primary'][data-destructive='true'] { + /* Custom properties matching future token names (without bui- prefix) */ + --bg-solid-danger: #dc2626; + --bg-solid-danger-hover: #b91c1c; + --bg-solid-danger-pressed: #991b1b; + --bg-solid-danger-disabled: #fca5a5; + --fg-solid-danger: var(--bui-white); + + [data-theme-mode='dark'] & { + --bg-solid-danger: #ef4444; + --bg-solid-danger-hover: #dc2626; + --bg-solid-danger-pressed: #b91c1c; + --bg-solid-danger-disabled: #7f1d1d; + } + + --bg: var(--bg-solid-danger); + --bg-hover: var(--bg-solid-danger-hover); + --bg-active: var(--bg-solid-danger-pressed); + --fg: var(--fg-solid-danger); + + &[data-disabled='true'], + &[data-loading='true'] { + --bg: var(--bg-solid-danger-disabled); + --bg-hover: var(--bg-solid-danger-disabled); + --bg-active: var(--bg-solid-danger-disabled); + } + + &:focus-visible { + outline: 2px solid var(--bui-border-danger); + outline-offset: 2px; + } + } + .bui-Button[data-variant='secondary'] { --bg: var(--bui-bg-neutral-on-surface-0); --bg-hover: var(--bui-bg-neutral-on-surface-0-hover); @@ -117,6 +150,33 @@ } } + .bui-Button[data-variant='secondary'][data-destructive='true'] { + /* Custom properties for hover/active states (no tokens exist yet) */ + --bg-danger-hover: #fecaca; + --bg-danger-pressed: #fca5a5; + + [data-theme-mode='dark'] & { + --bg-danger-hover: #450a0a; + --bg-danger-pressed: #7f1d1d; + } + + --bg: var(--bui-bg-danger); + --bg-hover: var(--bg-danger-hover); + --bg-active: var(--bg-danger-pressed); + --fg: var(--bui-fg-danger); + + &[data-disabled='true'], + &[data-loading='true'] { + --bg-hover: var(--bg); + --bg-active: var(--bg); + --fg: var(--bui-fg-disabled); + } + + &:focus-visible { + box-shadow: inset 0 0 0 2px var(--bui-border-danger); + } + } + .bui-Button[data-variant='tertiary'] { --bg-hover: var(--bui-bg-neutral-on-surface-0-hover); --bg-active: var(--bui-bg-neutral-on-surface-0-pressed); @@ -151,6 +211,32 @@ } } + .bui-Button[data-variant='tertiary'][data-destructive='true'] { + /* Custom properties for hover/active states (no tokens exist yet) */ + --bg-danger-hover: #fecaca; + --bg-danger-pressed: #fca5a5; + + [data-theme-mode='dark'] & { + --bg-danger-hover: #450a0a; + --bg-danger-pressed: #7f1d1d; + } + + --bg-hover: var(--bui-bg-danger); + --bg-active: var(--bg-danger-pressed); + --fg: var(--bui-fg-danger); + + &[data-disabled='true'], + &[data-loading='true'] { + --bg-hover: var(--bg); + --bg-active: var(--bg); + --fg: var(--bui-fg-disabled); + } + + &:focus-visible { + box-shadow: inset 0 0 0 2px var(--bui-border-danger); + } + } + .bui-Button[data-size='small'] { font-size: var(--bui-font-size-3); padding: 0 var(--bui-space-2); diff --git a/packages/ui/src/components/Button/Button.stories.tsx b/packages/ui/src/components/Button/Button.stories.tsx index 22d21b08f9..8d5a67c9fe 100644 --- a/packages/ui/src/components/Button/Button.stories.tsx +++ b/packages/ui/src/components/Button/Button.stories.tsx @@ -31,7 +31,10 @@ const meta = preview.meta({ }, variant: { control: 'select', - options: ['primary', 'secondary'], + options: ['primary', 'secondary', 'tertiary'], + }, + destructive: { + control: 'boolean', }, }, }); @@ -67,6 +70,15 @@ export const Variants = meta.story({ + + + @@ -81,6 +93,15 @@ export const Variants = meta.story({ + + + @@ -95,6 +116,15 @@ export const Variants = meta.story({ + + + @@ -109,6 +139,15 @@ export const Variants = meta.story({ + + + @@ -123,6 +162,98 @@ export const Variants = meta.story({ + + + + + + + ), +}); + +export const Destructive = meta.story({ + render: () => ( + + + Primary Destructive + + + + + + + + + Secondary Destructive + + + + + + + + + Tertiary Destructive + + + + + + + + + On Surface 1 + + + + + + + + Sizes + + + @@ -181,16 +312,29 @@ export const FullWidth = meta.story({ export const Disabled = meta.story({ render: () => ( - - - - + + + + + + + + + + + ), }); @@ -270,6 +414,24 @@ export const LoadingVariants = meta.story({ + Primary Destructive + + + + + + Loading vs Disabled