diff --git a/.changeset/card-compact-layout.md b/.changeset/card-compact-layout.md
new file mode 100644
index 00000000000..c676741df9a
--- /dev/null
+++ b/.changeset/card-compact-layout.md
@@ -0,0 +1,5 @@
+---
+'@primer/react': minor
+---
+
+Card: Add `layout="compact"` prop for a compact card layout with tighter spacing, no icon background, and smaller title
diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-colorblind-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-colorblind-linux.png
new file mode 100644
index 00000000000..3018dd0ec27
Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-dimmed-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-dimmed-linux.png
new file mode 100644
index 00000000000..63a36f1a7e2
Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-dimmed-linux.png differ
diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-high-contrast-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-high-contrast-linux.png
new file mode 100644
index 00000000000..f10b130dd8b
Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-linux.png
new file mode 100644
index 00000000000..3018dd0ec27
Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-linux.png differ
diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-tritanopia-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-tritanopia-linux.png
new file mode 100644
index 00000000000..3018dd0ec27
Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-dark-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-colorblind-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-colorblind-linux.png
new file mode 100644
index 00000000000..daba2addb44
Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-high-contrast-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-high-contrast-linux.png
new file mode 100644
index 00000000000..f51a13ecef8
Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-linux.png
new file mode 100644
index 00000000000..daba2addb44
Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-linux.png differ
diff --git a/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-tritanopia-linux.png b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-tritanopia-linux.png
new file mode 100644
index 00000000000..daba2addb44
Binary files /dev/null and b/.playwright/snapshots/components/Card.test.ts-snapshots/Card-Compact-light-tritanopia-linux.png differ
diff --git a/e2e/components/Card.test.ts b/e2e/components/Card.test.ts
index 8c4e77acfde..1b583566aac 100644
--- a/e2e/components/Card.test.ts
+++ b/e2e/components/Card.test.ts
@@ -56,4 +56,22 @@ test.describe('Card', () => {
})
}
})
+
+ test.describe('Compact', () => {
+ for (const theme of themes) {
+ test.describe(theme, () => {
+ test('default @vrt', async ({page}) => {
+ await visit(page, {
+ id: 'experimental-components-card-features--compact',
+ globals: {
+ colorScheme: theme,
+ },
+ })
+
+ // Default state
+ expect(await page.screenshot()).toMatchSnapshot(`Card.Compact.${theme}.png`)
+ })
+ })
+ }
+ })
})
diff --git a/packages/react/src/Card/Card.features.stories.tsx b/packages/react/src/Card/Card.features.stories.tsx
index a8ef6d5a1e3..dc6b2f286d2 100644
--- a/packages/react/src/Card/Card.features.stories.tsx
+++ b/packages/react/src/Card/Card.features.stories.tsx
@@ -28,6 +28,22 @@ export const WithImage = () => {
)
}
+export const Compact = () => {
+ return (
+
+
+ primer/react
+
+ The compact layout uses tighter spacing, an icon without a background container, and a smaller title.
+
+
+
+ 1.2k stars
+
+
+ )
+}
+
export const WithMetadata = () => {
return (
diff --git a/packages/react/src/Card/Card.module.css b/packages/react/src/Card/Card.module.css
index 53360e1e55d..e0e463e81d9 100644
--- a/packages/react/src/Card/Card.module.css
+++ b/packages/react/src/Card/Card.module.css
@@ -29,12 +29,28 @@
&[data-padding='none'] {
padding: 0;
}
+
+ &[data-layout='compact'] {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--stack-gap-condensed);
+ }
+
+ &[data-layout='compact'][data-padding='normal'] {
+ /* stylelint-disable-next-line primer/spacing */
+ padding: var(--stack-padding-normal);
+ }
}
.CardHeader {
display: block;
width: 100%;
height: auto;
+
+ .Card:where([data-layout='compact']) & {
+ flex: 0 0 auto;
+ width: auto;
+ }
}
.CardHeaderEdgeToEdge {
@@ -55,17 +71,25 @@
.CardIcon {
display: flex;
align-items: center;
- justify-content: center;
- width: var(--base-size-32);
- height: var(--base-size-32);
- border-radius: var(--borderRadius-medium);
- background-color: var(--bgColor-muted);
+ justify-content: flex-start;
color: var(--fgColor-muted);
+
+ .Card:where([data-layout='default']) & {
+ justify-content: center;
+ width: var(--base-size-32);
+ height: var(--base-size-32);
+ border-radius: var(--borderRadius-medium);
+ background-color: var(--bgColor-muted);
+ }
}
.CardBody {
display: grid;
gap: var(--stack-gap-normal);
+
+ .Card:where([data-layout='compact']) & {
+ flex: 1 1 auto;
+ }
}
.CardContent {
@@ -77,6 +101,12 @@
font: var(--text-title-shorthand-small);
color: var(--fgColor-default);
margin: 0;
+
+ .Card:where([data-layout='compact']) & {
+ position: relative;
+ top: calc(-1 * var(--base-size-4));
+ font-size: var(--text-body-size-medium);
+ }
}
.CardDescription {
diff --git a/packages/react/src/Card/Card.stories.tsx b/packages/react/src/Card/Card.stories.tsx
index 3f0f9d30e54..eef867be2a4 100644
--- a/packages/react/src/Card/Card.stories.tsx
+++ b/packages/react/src/Card/Card.stories.tsx
@@ -33,12 +33,13 @@ export const Default = () => {
type PlaygroundArgs = {
showIcon: boolean
showMetadata: boolean
+ layout: 'default' | 'compact'
padding: 'none' | 'condensed' | 'normal'
borderRadius: 'medium' | 'large'
}
-export const Playground: StoryFn = ({showIcon, showMetadata, padding, borderRadius}) => (
-
+export const Playground: StoryFn = ({showIcon, showMetadata, layout, padding, borderRadius}) => (
+
{showIcon && }
Playground Card
Experiment with the Card component and its subcomponents.
@@ -49,6 +50,7 @@ export const Playground: StoryFn = ({showIcon, showMetadata, pad
Playground.args = {
showIcon: true,
showMetadata: true,
+ layout: 'default',
padding: 'normal',
borderRadius: 'large',
}
@@ -62,6 +64,11 @@ Playground.argTypes = {
control: {type: 'boolean'},
description: 'Show or hide the Card.Metadata subcomponent',
},
+ layout: {
+ control: {type: 'radio'},
+ options: ['default', 'compact'],
+ description: 'Controls the layout of the Card',
+ },
padding: {
control: {type: 'radio'},
options: ['none', 'condensed', 'normal'],
diff --git a/packages/react/src/Card/Card.test.tsx b/packages/react/src/Card/Card.test.tsx
index c20efab9ca6..aeab542fe61 100644
--- a/packages/react/src/Card/Card.test.tsx
+++ b/packages/react/src/Card/Card.test.tsx
@@ -277,4 +277,32 @@ describe('Card', () => {
)
expect(container.firstChild).not.toHaveAttribute('as')
})
+
+ it('should set data-layout to default by default', () => {
+ const {container} = render(
+
+ Default Variant
+ ,
+ )
+ expect(container.firstChild).toHaveAttribute('data-layout', 'default')
+ })
+
+ it('should set data-layout to compact when layout="compact"', () => {
+ const {container} = render(
+
+
+ Compact Card
+ ,
+ )
+ expect(container.firstChild).toHaveAttribute('data-layout', 'compact')
+ })
+
+ it('should set data-layout on custom content cards', () => {
+ const {container} = render(
+
+ Custom
+ ,
+ )
+ expect(container.firstChild).toHaveAttribute('data-layout', 'compact')
+ })
})
diff --git a/packages/react/src/Card/Card.tsx b/packages/react/src/Card/Card.tsx
index dec74a45f0b..46f79f41727 100644
--- a/packages/react/src/Card/Card.tsx
+++ b/packages/react/src/Card/Card.tsx
@@ -22,6 +22,13 @@ export type CardProps = PolymorphicProps<
/** Border radius. @default 'large' */
borderRadius?: 'medium' | 'large'
+ /**
+ * Layout of the card. `compact` uses tighter spacing, removes the icon
+ * background container, and renders a smaller title.
+ * @default 'default'
+ */
+ layout?: 'default' | 'compact'
+
/**
* Card contents. Provide either `Card.*` subcomponents (e.g. `Card.Heading`,
* `Card.Description`, `Card.Metadata`) or custom content.
@@ -93,6 +100,7 @@ function CardComponent(
className,
padding = 'normal',
borderRadius = 'large',
+ layout = 'default',
as = 'div',
...rest
} = props as CardProps
@@ -149,6 +157,7 @@ function CardComponent(
data-component="Card"
data-padding={padding}
data-border-radius={borderRadius}
+ data-layout={layout}
{...rest}
>
{children}
@@ -165,6 +174,7 @@ function CardComponent(
data-component="Card"
data-padding={padding}
data-border-radius={borderRadius}
+ data-layout={layout}
{...rest}
>
{(image || icon) && (