feat(ui): add Badge component

Adds a new `Badge` component to the Backstage UI library. Badge shares the same visual appearance as `Tag` (size tokens, colors, border radius, icon slot) but renders as a plain non-interactive `<span>` with no React Aria plumbing.

Key characteristics:
- Plain DOM element — accessible text content exposed to screen readers without any role override
- Background consumer — participates in the bg context system and steps up neutral background levels (`neutral-2` → `neutral-3` → `neutral-4`) when placed inside colored containers
- Supports `icon`, `size` (`small` | `medium`, defaults to `small`), `children`, and `className` props
- Fully themeable via `BadgeDefinition`

Also includes Storybook stories and full docs-ui documentation (props table, examples, theming section, changelog).

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
Made-with: Cursor
This commit is contained in:
Charles de Dreuille
2026-04-03 17:19:02 +01:00
parent 8f9c1d64b8
commit 4032ad7fc4
15 changed files with 413 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/ui': patch
---
Added new `Badge` component for non-interactive labeling and categorization of content. It shares the visual appearance of `Tag` but renders as a plain DOM element with no interactive states.
**Affected components:** Badge
@@ -0,0 +1,16 @@
'use client';
import { Badge } from '../../../../../packages/ui/src/components/Badge/Badge';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { RiBugLine } from '@remixicon/react';
export const Default = () => <Badge>Banana</Badge>;
export const WithIcon = () => <Badge icon={<RiBugLine />}>Banana</Badge>;
export const Sizes = () => (
<Flex direction="row" gap="2">
<Badge size="small">Banana</Badge>
<Badge size="medium">Banana</Badge>
</Flex>
);
+41
View File
@@ -0,0 +1,41 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Default, WithIcon, Sizes } from './components';
import { badgePropDefs } from './props-definition';
import { usage, preview, withIcons, sizes } from './snippets';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { BadgeDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Badge"
description="A non-interactive label for annotating, categorizing, or highlighting content."
/>
<Snippet align="center" py={4} preview={<Default />} code={preview} />
## Usage
<CodeBlock code={usage} />
## API reference
### Badge
<PropsTable data={badgePropDefs} />
## Examples
### With icons
<Snippet align="center" py={4} open preview={<WithIcon />} code={withIcons} />
### Sizes
<Snippet align="center" py={4} open preview={<Sizes />} code={sizes} />
<Theming definition={BadgeDefinition} />
<ChangelogComponent component={['badge']} />
@@ -0,0 +1,27 @@
import {
classNamePropDefs,
childrenPropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const badgePropDefs: Record<string, PropDef> = {
icon: {
type: 'enum',
values: ['ReactNode'],
description: 'Icon displayed before the badge text.',
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
description: (
<>
Visual size of the badge. Use <Chip>small</Chip> for inline or dense
layouts, <Chip>medium</Chip> for standalone badges.
</>
),
},
...childrenPropDefs,
...classNamePropDefs,
};
@@ -0,0 +1,12 @@
export const usage = `import { Badge } from '@backstage/ui';
<Badge>Badge</Badge>`;
export const preview = `<Badge>Banana</Badge>`;
export const withIcons = `<Badge icon={<RiBugLine />}>Banana</Badge>`;
export const sizes = `<Flex direction="row" gap="2">
<Badge size="small">Banana</Badge>
<Badge size="medium">Banana</Badge>
</Flex>`;
+4
View File
@@ -17,6 +17,10 @@ export const components: Page[] = [
title: 'Avatar',
slug: 'avatar',
},
{
title: 'Badge',
slug: 'badge',
},
{
title: 'Box',
slug: 'box',
+39
View File
@@ -318,6 +318,45 @@ export interface AvatarProps
extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children' | 'className'>,
AvatarOwnProps {}
// @public
export const Badge: ForwardRefExoticComponent<
BadgeProps & RefAttributes<HTMLSpanElement>
>;
// @public
export const BadgeDefinition: {
readonly styles: {
readonly [key: string]: string;
};
readonly classNames: {
readonly root: 'bui-Badge';
readonly icon: 'bui-BadgeIcon';
};
readonly bg: 'consumer';
readonly propDefs: {
readonly icon: {};
readonly size: {
readonly dataAttribute: true;
readonly default: 'small';
};
readonly children: {};
readonly className: {};
};
};
// @public
export type BadgeOwnProps = {
icon?: React.ReactNode;
size?: 'small' | 'medium';
children?: React.ReactNode;
className?: string;
};
// @public
export interface BadgeProps
extends BadgeOwnProps,
Omit<React.HTMLAttributes<HTMLSpanElement>, keyof BadgeOwnProps> {}
// @public (undocumented)
export interface BgContextValue {
// (undocumented)
@@ -0,0 +1,65 @@
/*
* 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-Badge {
color: var(--bui-fg-primary);
background-color: var(--bui-bg-neutral-1);
border-radius: var(--bui-radius-2);
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: var(--bui-font-weight-regular);
gap: var(--bui-space-1);
&[data-on-bg='neutral-1'] {
background-color: var(--bui-bg-neutral-2);
}
&[data-on-bg='neutral-2'] {
background-color: var(--bui-bg-neutral-3);
}
&[data-on-bg='neutral-3'] {
background-color: var(--bui-bg-neutral-4);
}
}
.bui-Badge[data-size='small'] {
height: 26px;
padding: 0 var(--bui-space-2);
font-size: var(--bui-font-size-1);
}
.bui-Badge[data-size='medium'] {
height: 32px;
padding: 0 var(--bui-space-2);
font-size: var(--bui-font-size-2);
}
.bui-BadgeIcon {
display: flex;
align-items: center;
justify-content: center;
svg {
width: 1rem;
height: 1rem;
}
}
}
@@ -0,0 +1,61 @@
/*
* 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 { Badge } from '.';
import { Flex } from '../../';
import { BUIProvider } from '../../provider';
import { RiBugLine } from '@remixicon/react';
const meta = preview.meta({
title: 'Backstage UI/Badge',
component: Badge,
decorators: [
Story => (
<BUIProvider>
<Story />
</BUIProvider>
),
],
});
export const Default = meta.story({
args: {
children: 'Banana',
},
});
export const Sizes = meta.story({
render: () => (
<Flex direction="row" gap="2">
<Badge size="small">Banana</Badge>
<Badge size="medium">Banana</Badge>
</Flex>
),
});
export const WithIcon = meta.story({
render: () => (
<Flex direction="row" gap="2">
<Badge size="small" icon={<RiBugLine />}>
Banana
</Badge>
<Badge size="medium" icon={<RiBugLine />}>
Banana
</Badge>
</Flex>
),
});
@@ -0,0 +1,40 @@
/*
* 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 { BadgeProps } from './types';
import { forwardRef } from 'react';
import { useDefinition } from '../../hooks/useDefinition';
import { BadgeDefinition } from './definition';
/**
* A non-interactive badge for labeling or categorizing content.
*
* @public
*/
export const Badge = forwardRef<HTMLSpanElement, BadgeProps>((props, ref) => {
const { ownProps, restProps, dataAttributes } = useDefinition(
BadgeDefinition,
props,
);
const { classes, children, icon } = ownProps;
return (
<span ref={ref} className={classes.root} {...dataAttributes} {...restProps}>
{icon && <span className={classes.icon}>{icon}</span>}
{children}
</span>
);
});
@@ -0,0 +1,38 @@
/*
* 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 { BadgeOwnProps } from './types';
import styles from './Badge.module.css';
/**
* Component definition for Badge
* @public
*/
export const BadgeDefinition = defineComponent<BadgeOwnProps>()({
styles,
classNames: {
root: 'bui-Badge',
icon: 'bui-BadgeIcon',
},
bg: 'consumer',
propDefs: {
icon: {},
size: { dataAttribute: true, default: 'small' },
children: {},
className: {},
},
});
+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 { Badge } from './Badge';
export type { BadgeProps, BadgeOwnProps } from './types';
export { BadgeDefinition } from './definition';
+42
View File
@@ -0,0 +1,42 @@
/*
* 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.
*/
/**
* Own props for the Badge component.
*
* @public
*/
export type BadgeOwnProps = {
/**
* The icon to display before the badge text.
*/
icon?: React.ReactNode;
/**
* The size of the badge.
*/
size?: 'small' | 'medium';
children?: React.ReactNode;
className?: string;
};
/**
* Props for the Badge component.
*
* @public
*/
export interface BadgeProps
extends BadgeOwnProps,
Omit<React.HTMLAttributes<HTMLSpanElement>, keyof BadgeOwnProps> {}
+1
View File
@@ -27,6 +27,7 @@ export {
} from './components/Accordion/definition';
export { AlertDefinition } from './components/Alert/definition';
export { AvatarDefinition } from './components/Avatar/definition';
export { BadgeDefinition } from './components/Badge/definition';
export { BoxDefinition } from './components/Box/definition';
export { ButtonDefinition } from './components/Button/definition';
export { ButtonIconDefinition } from './components/ButtonIcon/definition';
+1
View File
@@ -31,6 +31,7 @@ export * from './components/FullPage';
export * from './components/Accordion';
export * from './components/Alert';
export * from './components/Avatar';
export * from './components/Badge';
export * from './components/Button';
export * from './components/Card';
export * from './components/Dialog';