Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7055932
Card: allow FullBleed bump against top border inside Header
simison Apr 30, 2026
74781d5
Restore padding above header title when combined with full bleed image
simison Apr 30, 2026
4c86943
Add 'full bleed hero only' story
simison Apr 30, 2026
86a0b97
Implement cover "full bleed"
simison Apr 30, 2026
6b1798b
Add PR to changelog
simison Apr 30, 2026
6034a4b
Fix missing gap between content and header when header isn't first child
simison May 5, 2026
57fed7a
Revert "Fix missing gap between content and header when header isn't …
simison May 5, 2026
451deff
Update JSDoc to mention all possible interactions with other subcompo…
simison May 14, 2026
21550a0
Remove superflous style
simison May 14, 2026
32b2c54
Condence styles
simison May 14, 2026
4c56075
Add examples for collapsable card
simison May 14, 2026
40d4a97
Put back padding between full bleed and header title
simison May 14, 2026
c5021f0
Fix collapsable card
simison May 14, 2026
42b69b2
Remove couple collapsable card stories
simison May 15, 2026
b8822a3
Update JSDocs about unsupported FullBleed in CollapsibleCard Header
simison May 15, 2026
90aef37
Update packages/ui/CHANGELOG.md
simison May 15, 2026
fc2c6b7
Update JSDoc for FullBleed
simison May 15, 2026
a3d2945
Update comment in CSS
simison May 15, 2026
1c0a1d2
Fix gap between text content and full bleed in "featured image" example
simison May 16, 2026
96488fe
Use Stack via `render` for inter-sibling spacing in FullBleed stories
ciampo May 18, 2026
b833b09
Drop asymmetric `Header` + `FullBleed` gap rule
ciampo May 18, 2026
2905e8d
Restore Header bullet in FullBleed JSDoc and document Stack pattern
ciampo May 18, 2026
ceb7e12
Rename Card FullBleed stories for clarity
ciampo May 18, 2026
2da3944
Update stale references after FullBleed story renames and rule removal
ciampo May 18, 2026
b34df23
Move FullBleed CHANGELOG entry back under Unreleased
ciampo May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

### New Features

- `Card`: `Card.FullBleed` now supports edge-bumping in additional positions ([#77856](https://github.com/WordPress/gutenberg/pull/77856)):
- As the **first child of `Card.Header`**, it extends flush to the card's top and side edges — ideal for hero images. Inter-sibling spacing is consumer-managed: compose `Card.Header` with `Stack` via the `render` prop to add a gap between the hero and following siblings (e.g. `Card.Title`).
- As the **sole child of `Card.Content`**, it extends flush to the card's side edges and additionally to the top edge when `Content` is the first card child, or the bottom edge when it is the last. This enables full-bleed content panels with or without a header above them.

### Enhancements

- Export `getWpCompatOverlaySlot()` so consumers can route their own portals into the compat overlay slot ([#78183](https://github.com/WordPress/gutenberg/pull/78183)).
Expand Down
18 changes: 18 additions & 0 deletions packages/ui/src/card/full-bleed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ import type { FullBleedProps } from './types';
* A container that breaks out of the card's padding to span edge-to-edge.
* Useful for full-width images, dividers, or embedded content.
*
* Additional edge-bumping behavior based on placement:
*
* - As the **first child** of `Card.Header`, it extends flush to the card's
* top edge — ideal for hero images.
* - As the **only child** of `Card.Content`, it extends flush to the card's
* top edge when `Content` is the first card section, and to the bottom edge
* when it is the last.
*
* Inter-sibling spacing inside `Card.Header` / `Card.Content` is consumer-
* managed. To add space between a hero `FullBleed` and the following
* siblings, compose the parent with `Stack` via the `render` prop:
* `<Card.Header render={ <Stack direction="column" gap="lg" /> }>`. This
* keeps `FullBleed` a direct child of `Card.Header` so the edge-bump still
* fires, while `Stack` provides the gap.
*
* Inside `CollapsibleCard`, place full-bleed media in `CollapsibleCard.Content`
* (not the header). The trigger/panel gap is preserved by design.
*
Comment thread
ciampo marked this conversation as resolved.
* Must be used as a direct child of `Card.Content` or `Card.Header`.
*/
export const FullBleed = forwardRef< HTMLDivElement, FullBleedProps >(
Expand Down
116 changes: 115 additions & 1 deletion packages/ui/src/card/stories/index.story.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Stack } from '@wordpress/ui';
import * as Card from '../index';

/**
Expand Down Expand Up @@ -71,6 +72,56 @@ export const Default: Story = {
},
};

/**
* `Card.FullBleed` as the sole child of `Card.Content` spans edge-to-edge
* with no padding around it.
*/
export const FullBleedCoverOnly: Story = {
args: {
children: (
<Card.Content>
<Card.FullBleed>
<div
style={ {
height: 180,
background:
'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
} }
/>
</Card.FullBleed>
</Card.Content>
),
},
};

/**
* When `Card.FullBleed` is the sole child of `Card.Content` and a
* `Card.Header` sits above it, the image bumps against the card's side and
* bottom edges while the header retains its normal padding.
*/
export const FullBleedCoverWithHeader: Story = {
args: {
children: (
<>
<Card.Header>
<Card.Title>Card title</Card.Title>
</Card.Header>
<Card.Content>
<Card.FullBleed>
<div
style={ {
height: 180,
background:
'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
} }
/>
</Card.FullBleed>
</Card.Content>
</>
),
},
};

/**
* `Card.FullBleed` breaks out of the card's padding to span
* edge-to-edge. Useful for images, dividers, or embedded content.
Expand All @@ -82,7 +133,7 @@ export const WithFullBleed: Story = {
<Card.Header>
<Card.Title>Featured image</Card.Title>
</Card.Header>
<Card.Content>
<Card.Content render={ <Stack direction="column" gap="lg" /> }>
<Card.FullBleed>
<div
style={ {
Expand Down Expand Up @@ -112,6 +163,69 @@ export const HeaderOnly: Story = {
},
};

/**
* When `Card.FullBleed` is the **first child** of `Card.Header`, it extends
* flush to the card's top and side edges — ideal for hero images. Content
* that follows inside the header is padded normally.
*/
export const FullBleedHeroWithTitle: Story = {
args: {
children: (
<>
<Card.Header render={ <Stack direction="column" gap="lg" /> }>
<Card.FullBleed>
<div
style={ {
height: 180,
background:
'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
} }
/>
</Card.FullBleed>
<Card.Title>Hero image card</Card.Title>
</Card.Header>
<Card.Content>
<Text>
The image above bleeds to the card&apos;s top and side
edges.
</Text>
</Card.Content>
</>
),
},
};

/**
* When `Card.FullBleed` is the **only child** of `Card.Header`, it fills the
* header entirely — top and sides flush to the card edges, no extra padding
* below.
*/
export const FullBleedHeroOnly: Story = {
args: {
children: (
<>
<Card.Header>
<Card.FullBleed>
<div
style={ {
height: 180,
background:
'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
} }
/>
</Card.FullBleed>
</Card.Header>
<Card.Content>
<Text>
The image above bleeds to the card&apos;s top and side
edges.
</Text>
</Card.Content>
</>
),
},
};

/**
* Use the `render` prop to change the underlying HTML elements for
* better semantics. Here, `Card.Root` renders as a `<section>` and
Expand Down
16 changes: 16 additions & 0 deletions packages/ui/src/card/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,20 @@
margin-inline: calc(-1 * var(--wp-ui-card-padding));
width: calc(100% + 2 * var(--wp-ui-card-padding));
}

/*
* When FullBleed sits at the start of the first *root* card section, extend
* it flush to the card's top edge. Must use `.root >` so `Card.Content`
* nested inside `CollapsibleCard`'s panel is not treated as `:first-child`
* (which would pull FullBleed up into the header gap).
*/
.root > :is(.header, .content):first-child > .fullbleed:first-child {
margin-block-start: calc(-1 * var(--wp-ui-card-padding));
}

/* When FullBleed sits at the end of the last card section, extend it flush to the card's bottom edge. */
:is(.header, .content):last-child > .fullbleed:last-child {
margin-block-end: calc(-1 * var(--wp-ui-card-padding));
}

}
19 changes: 18 additions & 1 deletion packages/ui/src/card/test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe( 'Card', () => {
} );

describe( 'fullbleed', () => {
it( 'renders children', () => {
it( 'renders children inside Content', () => {
render(
<Card.Root>
<Card.Content>
Expand All @@ -65,6 +65,23 @@ describe( 'Card', () => {

expect( screen.getByRole( 'img', { name: 'test' } ) ).toBeVisible();
} );

it( 'renders children inside Header', () => {
render(
<Card.Root>
<Card.Header>
<Card.FullBleed>
<img
src="https://example.com/hero.jpg"
alt="hero"
/>
</Card.FullBleed>
</Card.Header>
</Card.Root>
);

expect( screen.getByRole( 'img', { name: 'hero' } ) ).toBeVisible();
} );
} );

describe( 'render prop', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/collapsible-card/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import type { HeaderProps } from './types';
* Avoid placing interactive elements (buttons, links, inputs) inside the
* header, since the entire area is clickable and their events will bubble
* to trigger the collapse toggle.
*
* Place full-bleed media in `CollapsibleCard.Content`, not the header.
*/
export const Header = forwardRef< HTMLDivElement, HeaderProps >(
function CollapsibleCardHeader(
Expand Down
66 changes: 66 additions & 0 deletions packages/ui/src/collapsible-card/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const meta: Meta< typeof CollapsibleCard.Root > = {
'CollapsibleCard.Header': CollapsibleCard.Header,
'CollapsibleCard.HeaderDescription': CollapsibleCard.HeaderDescription,
'CollapsibleCard.Content': CollapsibleCard.Content,
'Card.FullBleed': Card.FullBleed,
},
parameters: {
componentStatus: {
Expand Down Expand Up @@ -314,3 +315,68 @@ export const ComparedToCard: Story = {
</div>
),
};

/**
* When `Card.FullBleed` is the sole child of `CollapsibleCard.Content` and a
* header sits above it, the media bumps against the card&apos;s side and
* bottom edges while the header retains its normal padding. (Unlike a plain
* `Card`, a header is always required here for the collapse trigger — see
* `Card` stories for a body-only `FullBleedCoverOnly` example.)
*/
export const FullBleedCoverWithHeader: Story = {
argTypes: { open: { control: false } },
args: {
defaultOpen: true,
children: (
<>
<CollapsibleCard.Header>
<Card.Title>Card title</Card.Title>
</CollapsibleCard.Header>
<CollapsibleCard.Content>
<Card.FullBleed>
<div
style={ {
height: 180,
background:
'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
} }
/>
</Card.FullBleed>
</CollapsibleCard.Content>
</>
),
},
};

/**
* `Card.FullBleed` breaks out of the content padding to span edge-to-edge.
* Useful for images, dividers, or embedded content inside the collapsible
* region.
*/
export const WithFullBleed: Story = {
argTypes: { open: { control: false } },
args: {
defaultOpen: true,
children: (
<>
<CollapsibleCard.Header>
<Card.Title>Featured image</Card.Title>
</CollapsibleCard.Header>
<CollapsibleCard.Content
render={ <Stack direction="column" gap="lg" /> }
>
<Card.FullBleed>
<div
style={ {
height: 160,
background:
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
} }
/>
</Card.FullBleed>
<Text>Content below the full-bleed area.</Text>
</CollapsibleCard.Content>
</>
),
},
};
Loading