Version 0.13.2

Card

A container with header, body, and footer sections for grouped content.

Header
Body
Footer

Usage#

import { Card, CardHeader, CardBody, CardFooter } from '@backstage/ui';

<Card>
  <CardHeader>Header</CardHeader>
  <CardBody>Body</CardBody>
  <CardFooter>Footer</CardFooter>
</Card>

API reference#

All Card components extend HTMLDivElement attributes.

Card#

PropTypeDefaultDescription
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.

CardHeader#

Fixed at the top of the card.

PropTypeDefaultDescription
children
ReactNode
-Content to display inside the component.
className
string
-Additional CSS class name for custom styling.
style
CSSProperties
-Inline CSS styles object.

CardBody#

Scrollable content area that fills available space.

PropTypeDefaultDescription
children
ReactNode
-Content to display inside the component.
className
string
-Additional CSS class name for custom styling.
style
CSSProperties
-Inline CSS styles object.

CardFooter#

Fixed at the bottom of the card.

PropTypeDefaultDescription
children
ReactNode
-Content to display inside the component.
className
string
-Additional CSS class name for custom styling.
style
CSSProperties
-Inline CSS styles object.

Examples#

Header and body only#

Cards can omit the footer section.

<Card style={{ width: '300px', height: '200px' }}>
  <CardHeader>Header</CardHeader>
  <CardBody>Body content without a footer</CardBody>
</Card>
Header
Body content without a footer

Scrollable body#

When body content exceeds the available height, CardBody scrolls while header and footer remain fixed.

Header
This is the first paragraph of a long body text that demonstrates how the Card component handles extensive content. The card should adjust accordingly to display all the text properly while maintaining its structure.Here's a second paragraph that adds more content to our card body. Having multiple paragraphs helps to visualize how spacing works within the card component.This third paragraph continues to add more text to ensure we have a proper demonstration of a card with significant content. This makes it easier to test scrolling behavior and overall layout when content exceeds the initial view.
Footer

Interactive cards#

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.

Button#

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>
Interactive Card
Click anywhere on this card to trigger the press handler.
Click to interact

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>
Link Card
This card navigates to a URL when clicked.
Opens backstage.io

With nested buttons#

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>
Card with Actions
Clicking the card background triggers the card press handler. The buttons below remain independently interactive.

Theming#

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-Card
  • bui-CardTrigger

Changelog#

Version 0.13.0#

Breaking Changes

  • Card 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>
    

Changes

  • 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

Version 0.12.0#

Breaking Changes

  • 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:

    • Provider-only (Box, Flex, Grid): set 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.
    • Consumer-only (Button, ButtonIcon, ButtonLink): set data-on-bg, inherit the parent container's bg unchanged.
    • Provider + Consumer (Card): sets both 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.