A container with header, body, and footer sections for grouped content.
import { Card, CardHeader, CardBody, CardFooter } from '@backstage/ui';
<Card>
<CardHeader>Header</CardHeader>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
</Card>All Card components extend HTMLDivElement attributes.
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | - | Content to display inside the component. |
| onPress | () => void | - | Handler called when the card is pressed. Makes the card interactive as a button. Requires label. |
| href | string | - | URL to navigate to. Makes the card interactive as a link. Mutually exclusive with onPress. |
| label | string | - | Accessible label announced by screen readers for the interactive overlay. Required when onPress or href is provided. |
| className | string | - | Additional CSS class name for custom styling. |
| style | CSSProperties | - | Inline CSS styles object. |
Fixed at the top of the card.
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | - | Content to display inside the component. |
| className | string | - | Additional CSS class name for custom styling. |
| style | CSSProperties | - | Inline CSS styles object. |
Scrollable content area that fills available space.
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | - | Content to display inside the component. |
| className | string | - | Additional CSS class name for custom styling. |
| style | CSSProperties | - | Inline CSS styles object. |
Fixed at the bottom of the card.
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | - | Content to display inside the component. |
| className | string | - | Additional CSS class name for custom styling. |
| style | CSSProperties | - | Inline CSS styles object. |
Cards can omit the footer section.
<Card style={{ width: '300px', height: '200px' }}>
<CardHeader>Header</CardHeader>
<CardBody>Body content without a footer</CardBody>
</Card>When body content exceeds the available height, CardBody scrolls while header and footer remain fixed.
Cards can be made interactive without wrapping the entire card in a button or link — which would conflict with any interactive elements inside. Instead, a transparent overlay covers the card surface, and nested buttons and links remain independently clickable above it.
Pass onPress and a label (used as the accessible name for screen readers) to make the whole card surface pressable.
<Card
style={{ width: '300px' }}
onPress={() => console.log('Card pressed')}
label="View component details"
>
<CardHeader>Interactive Card</CardHeader>
<CardBody>Click anywhere on this card to trigger the press handler.</CardBody>
<CardFooter>Click to interact</CardFooter>
</Card>Pass href to make the card surface navigate to a URL. label is required — it provides the accessible name for the invisible overlay link read by screen readers.
<Card
style={{ width: '300px' }}
href="https://backstage.io"
label="Open Backstage documentation"
>
<CardHeader>Link Card</CardHeader>
<CardBody>This card navigates to a URL when clicked.</CardBody>
<CardFooter>Opens backstage.io</CardFooter>
</Card>Buttons and links inside the card remain independently interactive. Clicking them does not trigger the card's onPress handler.
import { Button, Flex } from '@backstage/ui';
<Card
style={{ width: '300px' }}
onPress={() => console.log('Card pressed')}
label="View plugin details"
>
<CardHeader>Card with Actions</CardHeader>
<CardBody>
Clicking the card background triggers the card press handler.
The buttons below remain independently interactive.
</CardBody>
<CardFooter>
<Flex gap="2">
<Button size="small" variant="secondary" onPress={() => console.log('Primary')}>
Primary
</Button>
<Button size="small" variant="tertiary" onPress={() => console.log('Secondary')}>
Secondary
</Button>
</Flex>
</CardFooter>
</Card>Our theming system is based on a mix between CSS classes, CSS variables and data attributes. If you want to customise this component, you can use one of these class names below.
bui-Cardbui-CardTriggerCard Breaking Simplified the neutral background prop API for container components. The explicit neutral-1, neutral-2, neutral-3, and neutral-auto values have been removed from ProviderBg. They are replaced by a single 'neutral' value that always auto-increments from the parent context, making it impossible to skip or pin to an explicit neutral level. #33002
Migration Guide:
Replace any explicit bg="neutral-1", bg="neutral-2", bg="neutral-3", or bg="neutral-auto" props with bg="neutral". To achieve a specific neutral level in stories or tests, use nested containers — each additional bg="neutral" wrapper increments by one level.
// Before
<Box bg="neutral-2">...</Box>
// After
<Box bg="neutral">
<Box bg="neutral">...</Box>
</Box>
Card Added interactive support to the Card component. Pass onPress to make the entire card surface pressable, or href to make it navigate to a URL. A transparent overlay handles the interaction while nested buttons and links remain independently clickable. #33116
Card Fixed interactive cards so that CardBody can scroll when the card has a constrained height. Previously, the overlay element blocked scroll events. #33151
Card Fixed focus ring styles to use React Aria's [data-focus-visible] data attribute instead of the native CSS :focus-visible pseudo-class. This ensures keyboard focus rings render reliably when focus is managed programmatically by React Aria (e.g. inside a GridList, Menu, or Select). #33358
Card Fixed Card interactive cards not firing the onPress handler when clicking the card surface. #33343
Card Breaking Replaced Surface / onSurface system with new provider/consumer background system
The old Surface type ('0'–'3', 'auto') and its associated props (surface, onSurface) have been replaced by a provider/consumer bg architecture.
Types:
ContainerBg — 'neutral-1' | 'neutral-2' | 'neutral-3' | 'danger' | 'warning' | 'success'ProviderBg — ContainerBg | 'neutral-auto'Consumer components (e.g. Button) inherit the parent's bg via data-on-bg, and CSS handles the visual step-up. See "Neutral level capping" below for details on how levels are bounded.
Hooks:
useBgProvider(bg?) — for provider components. Returns { bg: undefined } when no bg is given (transparent). Supports 'neutral-auto' to auto-increment from the parent context.useBgConsumer() — for consumer components. Returns the parent container's bg unchanged.Component roles:
data-bg, wrap children in BgProvider. Transparent by default — they do not auto-increment; pass bg="neutral-auto" explicitly if you want automatic neutral stepping.data-on-bg, inherit the parent container's bg unchanged.data-bg and data-on-bg, wraps children. Card passes bg="neutral-auto" to its inner Box, so it auto-increments from the parent context.Neutral level capping:
Provider components cap at neutral-3. There is no neutral-4 prop value. The neutral-4 level exists only in consumer component CSS — for example, a Button sitting on a neutral-3 surface uses neutral-4 tokens internally via data-on-bg. #32711
Migration Guide:
Rename the surface prop to bg on provider components and update values:
- <Box surface="1">
+ <Box bg="neutral-1">
- <Card surface="2">
+ <Card bg="neutral-2">
- <Flex surface="0">
+ <Flex bg="neutral-1">
- <Grid.Root surface="1">
+ <Grid.Root bg="neutral-1">
Remove onSurface from consumer components — they now always inherit from the parent container:
- <Button onSurface="1" variant="secondary">
+ <Button variant="secondary">
- <ButtonIcon onSurface="2" variant="secondary" />
+ <ButtonIcon variant="secondary" />
- <ToggleButton onSurface="1">
+ <ToggleButton>
Update type imports:
- import type { Surface, LeafSurfaceProps, ContainerSurfaceProps } from '@backstage/ui';
+ import type { ContainerBg, ProviderBg } from '@backstage/ui';
Replace hook usage in custom components:
- import { useSurface, SurfaceProvider } from '@backstage/ui';
+ import { useBgProvider, useBgConsumer, BgProvider } from '@backstage/ui';
- const { surface } = useSurface({ surface: props.surface });
+ const { bg } = useBgProvider(props.bg);
- const { surface } = useSurface({ onSurface: props.onSurface });
+ const { bg } = useBgConsumer();
Update CSS selectors targeting surface data attributes:
- [data-surface='1'] { ... }
+ [data-bg='neutral-1'] { ... }
- [data-on-surface='1'] { ... }
+ [data-on-bg='neutral-1'] { ... }
Note: Provider components use data-bg (values: neutral-1 through neutral-3, plus intent values). Consumer components use data-on-bg, which reflects the parent container's bg directly. The neutral-4 level never appears as a prop or data-bg value — it is used only in consumer CSS.