Add new Interactive card

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-03-04 15:24:23 +00:00
parent d757071651
commit db927510dc
11 changed files with 417 additions and 13 deletions
@@ -7,6 +7,8 @@ import {
CardFooter,
} from '../../../../../packages/ui/src/components/Card/Card';
import { Text } from '../../../../../packages/ui/src/components/Text/Text';
import { Button } from '../../../../../packages/ui/src/components/Button/Button';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
export const Default = () => {
return (
@@ -32,6 +34,70 @@ export const HeaderAndBody = () => {
);
};
export const InteractiveButton = () => {
return (
<Card
style={{ width: '300px' }}
onPress={() => {}}
label="View component details"
>
<CardHeader>Interactive Card</CardHeader>
<CardBody>
Click anywhere on this card to trigger the press handler.
</CardBody>
<CardFooter>
<Text variant="body-small" color="secondary">
Click to interact
</Text>
</CardFooter>
</Card>
);
};
export const InteractiveLink = () => {
return (
<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>
<Text variant="body-small" color="secondary">
Opens backstage.io
</Text>
</CardFooter>
</Card>
);
};
export const InteractiveWithNestedButtons = () => {
return (
<Card
style={{ width: '300px' }}
onPress={() => {}}
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={() => {}}>
Primary
</Button>
<Button size="small" variant="tertiary" onPress={() => {}}>
Secondary
</Button>
</Flex>
</CardFooter>
</Card>
);
};
export const WithLongBody = () => {
return (
<Card style={{ width: '300px', height: '200px' }}>
+54 -1
View File
@@ -12,8 +12,18 @@ import {
defaultSnippet,
headerAndBodySnippet,
withLongBodySnippet,
interactiveButtonSnippet,
interactiveLinkSnippet,
interactiveWithNestedButtonsSnippet,
} from './snippets';
import { Default, HeaderAndBody, WithLongBody } from './components';
import {
Default,
HeaderAndBody,
WithLongBody,
InteractiveButton,
InteractiveLink,
InteractiveWithNestedButtons,
} from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { CardDefinition } from '../../../utils/definitions';
@@ -81,6 +91,49 @@ When body content exceeds the available height, CardBody scrolls while header an
code={withLongBodySnippet}
/>
## 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.
<Snippet
align="center"
py={4}
open
layout="side-by-side"
preview={<InteractiveButton />}
code={interactiveButtonSnippet}
/>
### Link
Pass `href` to make the card surface navigate to a URL. The `label` prop is optional but recommended for accessibility when the card content alone may not sufficiently describe the destination.
<Snippet
align="center"
py={4}
open
layout="side-by-side"
preview={<InteractiveLink />}
code={interactiveLinkSnippet}
/>
### With nested buttons
Buttons and links inside the card remain independently interactive. Clicking them does not trigger the card's `onPress` handler.
<Snippet
align="center"
py={4}
open
layout="side-by-side"
preview={<InteractiveWithNestedButtons />}
code={interactiveWithNestedButtonsSnippet}
/>
<Theming definition={CardDefinition} />
<ChangelogComponent component="card" />
@@ -15,6 +15,25 @@ const optionalChildrenPropDef: Record<string, PropDef> = {
export const cardPropDefs: Record<string, PropDef> = {
...optionalChildrenPropDef,
onPress: {
type: 'enum',
values: ['() => void'],
responsive: false,
description:
'Handler called when the card is pressed. Makes the card interactive as a button. Requires label.',
},
href: {
type: 'string',
responsive: false,
description:
'URL to navigate to. Makes the card interactive as a link. Mutually exclusive with onPress.',
},
label: {
type: 'string',
responsive: false,
description:
'Accessible label announced by screen readers for the interactive overlay. Required when onPress is provided.',
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -17,6 +17,50 @@ export const headerAndBodySnippet = `<Card style={{ width: '300px', height: '200
<CardBody>Body content without a footer</CardBody>
</Card>`;
export const interactiveButtonSnippet = `<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>`;
export const interactiveLinkSnippet = `<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>`;
export const interactiveWithNestedButtonsSnippet = `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>`;
export const withLongBodySnippet = `import { Text } from '@backstage/ui';
<Card style={{ width: '300px', height: '200px' }}>