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 Updated BUI links to use the hosting application's client-side router, including relative destinations and the application's configured router base path, while preserving native browser navigation where required.
Anchor-based components no longer accept the React Aria render prop. BUI now owns the underlying anchor so routing behavior remains consistent across application and plugin package versions.
ListRow, Tag, and table Row now retain client-side navigation when application and plugin packages load separate React Aria copies. Their existing modifier-key, target, download, and link-metadata behavior is unchanged. #35299
Migration Guide:
Remove render props from ButtonLink, ComboboxItem, Link, MenuItem, MenuListBoxItem, SearchAutocompleteItem, SelectItem, and Tab. BUI now selects and renders the appropriate anchor or router link automatically.
grow, shrink, and basis) to Box, Card, Grid, and Flex itself. #33948Card Disabled Card scroll shadow in browsers that don't support animation-timeline: scroll(). Prevents the shadow from being always visible over the CardBody when there's nothing to scroll or the body is not scrolled. #34025
Card Fixed CardBody showing an unwanted scrollbar when constrained below the scroll shadow height. #34069
Card Fixed external URLs in BUI link components being rewritten as in-app paths when the app is served under a non-root base path. Absolute URLs (http://, https://, //, mailto:, tel:) are now passed through unchanged. Internal href values are resolved against the current basename exactly once, which also fixes a latent issue where internal link clicks under a non-root base path could navigate to a URL with the basename prefix doubled. #34004
href resolution for BUI link components. Relative paths like ../other are now correctly turned into absolute paths before reaching the React Aria layer, ensuring client-side navigation goes to the right place. #33597Card 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.