diff --git a/packages/ui/src/components/Card/Card.module.css b/packages/ui/src/components/Card/Card.module.css
index 5e59d1e930..075a8dc556 100644
--- a/packages/ui/src/components/Card/Card.module.css
+++ b/packages/ui/src/components/Card/Card.module.css
@@ -30,12 +30,29 @@
position: relative;
}
+ /*
+ * Cursor and hover tint are applied at the card level so they cover the
+ * entire surface — including CardBody which sits above the overlay.
+ */
+ .bui-Card[data-interactive] {
+ cursor: pointer;
+
+ &:hover::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: color-mix(in srgb, currentColor 5%, transparent);
+ border-radius: inherit;
+ pointer-events: none;
+ z-index: 3;
+ }
+ }
+
.bui-CardOverlay {
position: absolute;
inset: 0;
z-index: 1;
background: transparent;
- cursor: pointer;
border-radius: inherit;
border: none;
padding: 0;
@@ -48,7 +65,10 @@
outline-offset: -2px;
}
- &:hover::after,
+ /*
+ * Keep focus tint for keyboard navigation (hover tint has moved to the
+ * card container above).
+ */
&[data-focused]::after {
content: '';
position: absolute;
@@ -60,13 +80,24 @@
}
/*
- * Interactive elements inside the card must sit above the overlay so they
- * remain independently clickable. The overlay handles the rest of the surface.
+ * CardBody and interactive elements must sit above the overlay (z-index: 1)
+ * so that:
+ * - scroll events reach CardBody's overflow container
+ * - buttons/links/inputs remain independently clickable
+ * Text clicks in CardBody that bypass the overlay are caught by the
+ * container-level onClick in Card.tsx.
*/
.bui-Card[data-interactive]
- :is(button, a[href], [role='button'], [role='link'], input, select, textarea):not(
- .bui-CardOverlay
- ) {
+ :is(
+ button,
+ a[href],
+ [role='button'],
+ [role='link'],
+ input,
+ select,
+ textarea,
+ .bui-CardBody
+ ):not(.bui-CardOverlay) {
position: relative;
z-index: 2;
}
diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx
index 41b43b89a4..e51a2c748b 100644
--- a/packages/ui/src/components/Card/Card.stories.tsx
+++ b/packages/ui/src/components/Card/Card.stories.tsx
@@ -231,6 +231,39 @@ export const BgOnProviders = meta.story({
),
});
+export const InteractiveWithScrollableBody = meta.story({
+ render: () => (
+ alert('Card pressed')}
+ label="View details"
+ >
+
+ Scrollable Interactive Card
+
+
+
+ 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.
+
+
+
+ ),
+});
+
export const Interactive = meta.story({
render: () => (
((props, ref) => {
const { classes, children, onPress, href, label } = ownProps;
const isInteractive = !!(onPress || href);
+ // CardBody sits above the overlay (z-index: 2) so that scroll events reach
+ // its overflow container. As a result, text clicks inside CardBody bypass
+ // the overlay and bubble up to here instead. We fire the card action only
+ // when the click did not originate from a nested interactive element.
+ const { onClick: userOnClick, ...restPropsWithoutClick } =
+ restProps as React.HTMLAttributes;
+
+ const handleContainerClick = useCallback(
+ (e: React.MouseEvent) => {
+ userOnClick?.(e);
+ if (!isInteractive) return;
+ const target = e.target as HTMLElement;
+ if (
+ target.closest?.(
+ 'button, a[href], input, select, textarea, [role="button"], [role="link"]',
+ )
+ )
+ return;
+ onPress?.();
+ },
+ [userOnClick, isInteractive, onPress],
+ );
+
return (
((props, ref) => {
className={classes.root}
data-interactive={isInteractive || undefined}
{...dataAttributes}
- {...restProps}
+ {...restPropsWithoutClick}
+ onClick={handleContainerClick}
>
{href && (
-
+ e.stopPropagation()}
+ />
)}
{onPress && !href && (
e.stopPropagation()}
/>
)}
{children}