CardView
A CardView displays a group of related objects, with support for selection and bulk actions.
variant
density
import {CardView, AssetCard, CardPreview, Image, Content, Text} from '@react-spectrum/s2';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
<CardView
aria-label="Nature photos"
selectionMode="multiple"
styles={style({width: 'full', height: 500})}>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1705034598432-1694e203cdf3?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width={600} height={400} />
</CardPreview>
<Content>
<Text slot="title">Desert Sunset</Text>
<Text slot="description">PNG • 2/3/2024</Text>
</Content>
</AssetCard>
<AssetCard>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1722233987129-61dc344db8b6?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width={600} height={900} />
</CardPreview>
<Content>
<Text slot="title">Hiking Trail</Text>
<Text slot="description">JPEG • 1/10/2022</Text>
</Content>
</AssetCard>
<AssetCard>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1629812456605-4a044aa38fbc?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width={600} height={899} />
</CardPreview>
<Content>
<Text slot="title">Lion</Text>
<Text slot="description">JPEG • 8/28/2021</Text>
</Content>
</AssetCard>
<AssetCard>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1722172118908-1a97c312ce8c?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width={600} height={900} />
</CardPreview>
<Content>
<Text slot="title">Mountain Sunrise</Text>
<Text slot="description">PNG • 3/15/2015</Text>
</Content>
</AssetCard>
<AssetCard>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1574870111867-089730e5a72b?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width={600} height={900} />
</CardPreview>
<Content>
<Text slot="title">Giraffe tongue</Text>
<Text slot="description">PNG • 11/27/2019</Text>
</Content>
</AssetCard>
<AssetCard>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1718378037953-ab21bf2cf771?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width={600} height={402} />
</CardPreview>
<Content>
<Text slot="title">Golden Hour</Text>
<Text slot="description">WEBP • 7/24/2024</Text>
</Content>
</AssetCard>
<AssetCard>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1721661657253-6621d52db753?w=600&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHx0b3BpYy1mZWVkfDYxfE04alZiTGJUUndzfHxlbnwwfHx8fHw%3D" width={600} height={900} />
</CardPreview>
<Content>
<Text slot="title">Architecture</Text>
<Text slot="description">PNG • 12/24/2016</Text>
</Content>
</AssetCard>
<AssetCard>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1456926631375-92c8ce872def?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width={600} height={400} />
</CardPreview>
<Content>
<Text slot="title">Peeking leopard</Text>
<Text slot="description">JPEG • 3/2/2016</Text>
</Content>
</AssetCard>
<AssetCard>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1721598359121-363311b3b263?w=600&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHx0b3BpYy1mZWVkfDc0fE04alZiTGJUUndzfHxlbnwwfHx8fHw%3D" width={600} height={900} />
</CardPreview>
<Content>
<Text slot="title">Roofs</Text>
<Text slot="description">JPEG • 4/24/2025</Text>
</Content>
</AssetCard>
<AssetCard>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1472396961693-142e6e269027?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width={600} height={990} />
</CardPreview>
<Content>
<Text slot="title">Half Dome Deer</Text>
<Text slot="description">DNG • 8/28/2018</Text>
</Content>
</AssetCard>
</CardView>
Content
CardView
follows the Collection Components API, accepting both static and dynamic collections. This example shows a dynamic collection, passing a list of objects to the items
prop, and a recursive function to render the children.
import {CardView, Collection, SkeletonCollection, AssetCard, CardPreview, Image, Content, Text, ActionMenu, MenuItem, Avatar} from '@react-spectrum/s2';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
{
id: "8SXaMMWCTGc",
title: "A Ficus Lyrata Leaf in the sunlight (2/2) (IG: @clay.banks)",
user: "Clay Banks",
image: "https://images.unsplash.com/photo-1580133318324-f2f76d987dd8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "pYjCqqDEOFo",
title: "beach of Italy",
user: "alan bajura",
image: "https://images.unsplash.com/photo-1737100522891-e8946ac97fd1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "CF-2tl6MQj0",
title: "A winding road in the middle of a forest",
user: "Artem Stoliar",
image: "https://images.unsplash.com/photo-1738249034651-1896f689be58?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 300
},
{
id: "OW97sLU0cOw",
title: "A green and purple aurora over a snow covered forest",
user: "Janosch Diggelmann",
image: "https://images.unsplash.com/photo-1738189669835-61808a9d5981?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "WfeLZ02IhkM",
title: "A blue and white firework is seen from above",
user: "Janosch Diggelmann",
image: "https://images.unsplash.com/photo-1738168601630-1c1f3ef5a95a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 300
},
{
id: "w1GpST72Bg8",
title: "A snow covered mountain with a sky background",
user: "Daniil Silantev",
image: "https://images.unsplash.com/photo-1738165170747-ecc6e3a4d97c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 267
},
{
id: "0iN0KIt6lYI",
title: "\"Pastel Sunset\"",
user: "Marek Piwnicki",
image: "https://images.unsplash.com/photo-1737917818689-f3b3708de5d7?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 640
},
{
id: "-mFKPfXXUG0",
title: "Leave the weight behind! You must make yourself light to strive upwards — to reach the light. (A serene winter landscape featuring a dense collection of bare, white trees.)",
user: "Simon Berger",
image: "https://images.unsplash.com/photo-1737972970322-cc2e255021bd?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 400
},
{
id: "MOk6URQ28R4",
title: "A snow covered tree with a sky background",
user: "Daniil Silantev",
image: "https://images.unsplash.com/photo-1738081359113-a7a33c509cf9?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "y36Nj_edtRE",
title: "A lake surrounded by trees covered in snow",
user: "Daniel Seßler",
image: "https://images.unsplash.com/photo-1736018545810-3de4c7ec25fa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "NvBV-YwlgBw",
title: "The night sky with stars above a rock formation",
user: "Dennis Haug",
image: "https://images.unsplash.com/photo-1735528655501-cf671a3323c3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 400
},
{
id: "UthQdrPFxt0",
title: "A pine tree covered in snow in a forest",
user: "Anita Austvika",
image: "https://images.unsplash.com/photo-1737312905026-5dfdff1097bc?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "2k74xaf8dfc",
title: "The sun shines through the trees in the forest",
user: "Joyce G",
image: "https://images.unsplash.com/photo-1736185597807-371cae1c7e4e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "Yje5kgfvCm0",
title: "A blurry photo of a field of flowers",
user: "Eugene Golovesov",
image: "https://images.unsplash.com/photo-1736483065204-e55e62092780?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "G2bsj2LVttI",
title: "A foggy road lined with trees and grass",
user: "Ingmar H",
image: "https://images.unsplash.com/photo-1737903071772-4d20348b4d81?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 533
},
{
id: "ppyNBOkfiuY",
title: "A close up of a green palm tree",
user: "Junel Mujar",
image: "https://images.unsplash.com/photo-1736849544918-6ddb5cfc2c42?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 533
},
{
id: "UcWUMqIsld8",
title: "A green leaf floating on top of a body of water",
user: "Allec Gomes",
image: "https://images.unsplash.com/photo-1737559217439-a5703e9b65cb?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "xHqOVq9w8OI",
title: "green-leafed plant",
user: "Joshua Michaels",
image: "https://images.unsplash.com/photo-1563364664-399838d1394c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 266
},
{
id: "uWx3_XEc-Jw",
title: "A view of a mountain covered in fog",
user: "iuliu illes",
image: "https://images.unsplash.com/photo-1737403428945-c584529b7b17?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 298
},
{
id: "2_3lhGt8i-Y",
title: "A field with tall grass and fog in the background",
user: "Ingmar H",
image: "https://images.unsplash.com/photo-1737439987404-a3ee9fb95351?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "FV-__IOxb08",
title: "A close up of a wave on a sandy beach",
user: "Jonathan Borba",
image: "https://images.unsplash.com/photo-1726502102472-2108ef2a5cae?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "_BS-vK3boOU",
title: "Desert textures",
user: "Braden Jarvis",
image: "https://images.unsplash.com/photo-1722359546494-8e3a00f88e95?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 561
},
{
id: "LjAcS9lJdBg",
title: "Tew Falls, waterfall, in Hamilton, Canada.",
user: "Andre Portolesi",
image: "https://images.unsplash.com/photo-1705021246536-aecfad654893?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 500
},
{
id: "hlj6xJG30FE",
title: "Find me on Instagram! @intricateexplorer",
user: "Intricate Explorer",
image: "https://images.unsplash.com/photo-1631641551473-fbe46919289d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 267
},
{
id: "vMoZvKeZOhw",
title: "Salt Marshes, Isle of Harris, Scotland by Nils Leonhardt. Visit my website: https://nilsleonhardt.com/storytelling-harris/ Instagram: @am.basteir",
user: "Nils Leonhardt",
image: "https://images.unsplash.com/photo-1585951301678-8fd6f3b32c7e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "wCLCK9LDDjI",
title: "An aerial view of a snow covered forest",
user: "Lukas Hädrich",
image: "https://images.unsplash.com/photo-1737405555489-78b3755eaa81?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 267
},
{
id: "OdDx3_NB-Wk",
title: "A close up of a tall grass with a sky in the background",
user: "Ingmar H",
image: "https://images.unsplash.com/photo-1737301519296-062cd324dbfa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "Gn-FOw1geFc",
title: "Larches on Maple Pass, Washington",
user: "noelle",
image: "https://images.unsplash.com/photo-1737496538329-a59d10148a08?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
},
{
id: "VhKJHOz2tJ8",
title: "IC 1805 La nébuleuse du coeur",
user: "arnaud girault",
image: "https://images.unsplash.com/photo-1737478598284-b9bc11cb1e9b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 266
},
{
id: "w5QmH_uqB0U",
title: "A pile of shells sitting on top of a sandy beach",
user: "Toa Heftiba",
image: "https://images.unsplash.com/photo-1725366351350-a64a1be919ef?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400",
width: 400,
height: 600
}
];
for (let i = 0; images.length < 200; i++) {
images.push({...images[i % 30], id: String(i)});
}
<CardView
aria-label="Nature photos"
styles={style({width: 'full', height: 500})}
layout="waterfall"
size="S"
items={images}>
{image => (
<AssetCard>
<CardPreview>
<Image src={image.image} width={image.width} height={image.height} />
</CardPreview>
<Content>
<Text slot="title">{image.title}</Text>
<Text slot="description">{image.user}</Text>
</Content>
</AssetCard>
)}
</CardView>
Asynchronous loading
Use the loadingState
and onLoadMore
props to enable async loading and infinite scrolling. When loading, render a <SkeletonCollection>
to generate placeholder content to display as skeleton cards.
loadingState
import {CardView, Collection, SkeletonCollection, Card, CardPreview, Image, Content, Text, Avatar} from '@react-spectrum/s2';
import {useAsyncList} from 'react-stately';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
export default function Example(props) {
let list = useAsyncList<Item, number | null>({
async load({signal, cursor, items}) {
let page = cursor || 1;
let res = await fetch(
`https://api.unsplash.com/topics/nature/photos?page=${page}&per_page=30&client_id=AJuU-FPh11hn7RuumUllp4ppT8kgiLS7LtOHp_sp4nc`,
{signal}
);
let nextItems = await res.json();
// Filter duplicates which might be returned by the API.
let existingKeys = new Set(items.map(i => i.id));
nextItems = nextItems.filter(i => !existingKeys.has(i.id) && (i.description || i.alt_description));
return {items: nextItems, cursor: nextItems.length ? page + 1 : null};
}
});
let loadingState = props.loadingState || list.loadingState;
return (
<CardView
aria-label="Nature photos"
size="S"
layout="waterfall"
loadingState={loadingState}
onLoadMore={list.loadMore}
onLoadMore={props.loadingState ? undefined : list.loadMore}
styles={style({width: 'full', height: 500})}>
<Collection items={loadingState === 'loading' ? [] : list.items}>
{item => <PhotoCard item={item} />}
</Collection>
{(loadingState === 'loading' || loadingState === 'loadingMore') && (
<SkeletonCollection>
{() => (
<PhotoCard
item={{
id: Math.random(),
user: {name: 'Placeholder name', profile_image: {small: ''}},
urls: {regular: ''},
description: 'This is a fake description. Kinda long so it wraps to a new line.',
alt_description: '',
width: 400,
height: 200 + Math.max(0, Math.round(Math.random() * 400))
}} />
)}
</SkeletonCollection>
)}
</CardView>
);
}
function PhotoCard({item, layout}: {item: Item, layout: string}) {
return (
<Card id={item.id} textValue={item.description || item.alt_description}>
{({size}) => (<>
<CardPreview>
<Image
src={item.urls.regular}
width={item.width}
height={item.height}
styles={style({
width: 'full',
pointerEvents: 'none',
objectFit: 'cover'
})} />
</CardPreview>
<Content>
<Text slot="title">{item.description || item.alt_description}</Text>
<div className={style({display: 'flex', alignItems: 'center', gap: 8, gridArea: 'description'})}>
<Avatar src={item.user.profile_image.small} size={size} />
<Text slot="description">{item.user.name}</Text>
</div>
</Content>
</>)}
</Card>
);
}
Links
Use the href
prop on a Card to create a link. See the client side routing guide to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the selection guide for more details.
import {CardView, Card, CollectionCardPreview, Image, Content, Text} from '@react-spectrum/s2';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
import Folder from '@react-spectrum/s2/icons/Folder';
{
"id": "bo8jQKTaE0Y",
"href": "https://unsplash.com/t/wallpapers",
"photos": [
{
"id": "pH6ff1GpUMo",
"src": "https://plus.unsplash.com/premium_photo-1700558685040-a75735b86bb7?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "CPjNU3ChRl4",
"src": "https://images.unsplash.com/photo-1756187793625-4a29fef1f4f8?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "G-5JCERzbE8",
"src": "https://images.unsplash.com/photo-1755838692094-49a97b9fb9ab?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "KAyZPC_Q5YM",
"src": "https://images.unsplash.com/photo-1755997234962-931d86bee287?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
}
],
"title": "Wallpapers",
"count": 16689
},
{
"id": "6sMVjTLSkeQ",
"href": "https://unsplash.com/t/nature",
"photos": [
{
"id": "G-5JCERzbE8",
"src": "https://images.unsplash.com/photo-1755838692094-49a97b9fb9ab?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "I8vmQEZDhM0",
"src": "https://plus.unsplash.com/premium_photo-1678483692858-d9ca6e9c67f9?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "puQ8iqUvs9o",
"src": "https://images.unsplash.com/photo-1756129725795-127bef414c6c?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "KAyZPC_Q5YM",
"src": "https://images.unsplash.com/photo-1755997234962-931d86bee287?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
}
],
"title": "Nature",
"count": 32010
},
{
"id": "Fzo3zuOHN6w",
"href": "https://unsplash.com/t/travel",
"photos": [
{
"id": "ghMslmoI6Sk",
"src": "https://plus.unsplash.com/premium_photo-1756175546675-f55b02bfa6e2?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "BQUtnTW8wVk",
"src": "https://images.unsplash.com/photo-1756155062023-524adfefb747?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "Ngj2u4PHjBY",
"src": "https://images.unsplash.com/photo-1755877379664-2f809909cbec?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "xWArdKbfrpw",
"src": "https://images.unsplash.com/photo-1755878008095-37b948fd2770?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
}
],
"title": "Travel",
"count": 9434
},
{
"id": "hmenvQhUmxM",
"href": "https://unsplash.com/t/film",
"photos": [
{
"id": "2bdJqBj0MI8",
"src": "https://images.unsplash.com/photo-1691055712387-28587d0e7088?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "OOS7uVLLCk0",
"src": "https://images.unsplash.com/photo-1691055712477-7f4dea1c6faa?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "lQ2XoRRX9Q0",
"src": "https://plus.unsplash.com/premium_photo-1751921504814-6259b4fa5df7?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "znGu7rwagmk",
"src": "https://images.unsplash.com/photo-1755867395694-97a254aaa314?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
}
],
"title": "Film",
"count": 12201
},
{
"id": "towJZFskpGg",
"href": "https://unsplash.com/t/people",
"photos": [
{
"id": "AcgICnidawU",
"src": "https://plus.unsplash.com/premium_photo-1755938644663-3d697c1b6eb0?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "qkkWjOHIgrk",
"src": "https://images.unsplash.com/photo-1755529905229-e0536cf889d7?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "BQUtnTW8wVk",
"src": "https://images.unsplash.com/photo-1756155062023-524adfefb747?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "9c8ymKQ6mRc",
"src": "https://images.unsplash.com/photo-1755542366683-282c366982a1?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
}
],
"title": "People",
"count": 13356
},
{
"id": "M8jVbLbTRws",
"href": "https://unsplash.com/t/architecture-interior",
"photos": [
{
"id": "gUDpShSl1qU",
"src": "https://plus.unsplash.com/premium_photo-1733054181243-d908a10d666e?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "x5UifGqA5NE",
"src": "https://images.unsplash.com/photo-1755669933959-377ab117bb8a?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "hGwY0X_afOw",
"src": "https://images.unsplash.com/photo-1755018237290-e2c1b65b3dd0?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "xgOabz-pz1k",
"src": "https://images.unsplash.com/photo-1753596109566-feccd5cd1ff8?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
}
],
"title": "Architecture & Interiors",
"count": 18937
},
{
"id": "xHxYTMHLgOc",
"href": "https://unsplash.com/t/street-photography",
"photos": [
{
"id": "Wv-0I3ft3DQ",
"src": "https://plus.unsplash.com/premium_photo-1748783614194-ee4f9c017a7e?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "cOT7dI7Tt2k",
"src": "https://images.unsplash.com/photo-1756135886621-58fbd0c924fb?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "u84fzygfTlo",
"src": "https://images.unsplash.com/photo-1755977546165-1d4e955ff63e?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "6ht6ysje16U",
"src": "https://images.unsplash.com/photo-1736595628509-da51701804f6?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
}
],
"title": "Street Photography",
"count": 13157
},
{
"id": "upmleWZC83Y",
"href": "https://unsplash.com/t/patterns",
"photos": [
{
"id": "ha0NjEvPq7g",
"src": "https://plus.unsplash.com/premium_vector-1752071909053-843cc5180171?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "geqxnWVZkpY",
"src": "https://images.unsplash.com/vector-1753790541089-1a7aba4523ce?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "eyhWE-fPPLY",
"src": "https://plus.unsplash.com/premium_vector-1734281622914-590e005ba22a?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "lhQmfqOtaOs",
"src": "https://images.unsplash.com/vector-1751865858655-a876e394d7a7?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
}
],
"title": "Patterns",
"count": 570
},
{
"id": "S4MKLAsBB74",
"href": "https://unsplash.com/t/fashion-beauty",
"photos": [
{
"id": "YzoJ0OMY4HE",
"src": "https://plus.unsplash.com/premium_photo-1755004629206-20d15cea7aa4?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "Leh7iPE0bKY",
"src": "https://images.unsplash.com/photo-1629960414183-fba0e2e137e7?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "RWLTHuxCqYo",
"src": "https://images.unsplash.com/photo-1754555009599-9f0d848748e7?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "KPLjHKBoLqM",
"src": "https://images.unsplash.com/photo-1753549839629-2eb4f552e0e6?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
}
],
"title": "Fashion & Beauty",
"count": 10413
},
{
"id": "aeu6rL-j6ew",
"href": "https://unsplash.com/t/business-work",
"photos": [
{
"id": "pQjoH4COskY",
"src": "https://images.unsplash.com/photo-1749880164389-e14710d2f397?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "CRuEm_IEC3I",
"src": "https://images.unsplash.com/photo-1650029609434-55bbbb38ab5c?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "nWb7Jxh5hxE",
"src": "https://images.unsplash.com/photo-1743343852416-e5eec987a627?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "xGklNeRfBK8",
"src": "https://images.unsplash.com/photo-1744686959591-eaaec00c999c?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
}
],
"title": "Business & Work",
"count": 7071
},
{
"id": "xjPR4hlkBGA",
"href": "https://unsplash.com/t/food-drink",
"photos": [
{
"id": "yvEl8b1EIeA",
"src": "https://images.unsplash.com/photo-1755605983542-a525a0975a25?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "erJuRsCRB2E",
"src": "https://images.unsplash.com/photo-1755004609214-c252674df1ca?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "JJZrip9P_5k",
"src": "https://images.unsplash.com/photo-1754992599453-01e809722aa1?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "WZe2v04wiv0",
"src": "https://images.unsplash.com/photo-1754836717974-d43bd9c798ae?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
}
],
"title": "Food & Drink",
"count": 10811
},
{
"id": "Bn-DjrcBrwo",
"href": "https://unsplash.com/t/sports",
"photos": [
{
"id": "0mERTWSD7po",
"src": "https://images.unsplash.com/photo-1755303238751-d04f190c96dd?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "2F3I5NuRZk4",
"src": "https://images.unsplash.com/photo-1755554857887-935f8a49f635?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "NkicHgUbtJQ",
"src": "https://plus.unsplash.com/premium_photo-1754258494576-f15dc2bb4d3f?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
},
{
"id": "dJt7EmomG78",
"src": "https://images.unsplash.com/photo-1755628931496-5b08b241567c?ixlib=rb-4.1.0&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max"
}
],
"title": "Sports",
"count": 4005
}
];
<CardView
aria-label="Collections"
styles={style({width: 'full', height: 500})}
size="S"
items={topics}>
{topic => (
<Card href={topic.href} target="_blank" textValue={topic.title}>
<CollectionCardPreview>
{topic.photos.map(photo => (
<Image key={photo.id} alt="" src={photo.src} />
))}
</CollectionCardPreview>
<Content>
<Text slot="title">{topic.title}</Text>
<div className={style({display: 'flex', alignItems: 'center', gap: 8})}>
<Folder />
<Text slot="description">{topic.count} photos</Text>
</div>
</Content>
</Card>
)}
</CardView>
Empty state
Use renderEmptyState
to render placeholder content when the CardView is empty.
import {CardView, IllustratedMessage, Heading, Content} from '@react-spectrum/s2';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
import Image from '@react-spectrum/s2/illustrations/gradient/generic1/Image';
<CardView
aria-label="Assets"
styles={style({width: 'full', height: 300})}
renderEmptyState={() => (
<IllustratedMessage size="L">
<Image />
<Heading>Create your first asset.</Heading>
<Content>Get started by uploading or importing some assets.</Content>
</IllustratedMessage>
)}>
{[]}
</CardView>
Selection and actions
Use selectionMode
to enable single or multiple selection, and selectedKeys
(matching each card's id
) to control the selected cards. Return an ActionBar from renderActionBar
to handle bulk actions, and use onAction
for row navigation. Disable cards with isDisabled
. See the selection guide for details.
Current selection:
import {CardView, AssetCard, CardPreview, Image, Content, Text, ActionBar, ActionButton, type Selection} from '@react-spectrum/s2';
import Edit from '@react-spectrum/s2/icons/Edit';
import Copy from '@react-spectrum/s2/icons/Copy';
import Delete from '@react-spectrum/s2/icons/Delete';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
import {useState} from 'react';
function Example(props) {
let [selected, setSelected] = useState<Selection>(new Set());
return (
<>
<CardView
aria-label="Nature photos"
styles={style({width: 'full', height: 500})}
{...props}
selectionMode="multiple"
selectedKeys={selected}
onSelectionChange={setSelected}
onAction={key => alert(`Clicked ${key}`)}
renderActionBar={(selectedKeys) => {
let selection = selectedKeys === 'all' ? 'all' : [...selectedKeys].join(', ');
return (
<ActionBar>
<ActionButton onPress={() => alert(`Edit ${selection}`)}>
<Edit />
<Text>Edit</Text>
</ActionButton>
<ActionButton onPress={() => alert(`Copy ${selection}`)}>
<Copy />
<Text>Copy</Text>
</ActionButton>
<ActionButton onPress={() => alert(`Delete ${selection}`)}>
<Delete />
<Text>Delete</Text>
</ActionButton>
</ActionBar>
);
}}>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1705034598432-1694e203cdf3?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</CardPreview>
<Content>
<Text slot="title">Desert Sunset</Text>
<Text slot="description">PNG • 2/3/2024</Text>
</Content>
</AssetCard>
<AssetCard id={2} isDisabled>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1722233987129-61dc344db8b6?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</CardPreview>
<Content>
<Text slot="title">Hiking Trail</Text>
<Text slot="description">JPEG • 1/10/2022</Text>
</Content>
</AssetCard>
<AssetCard id={3}>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1629812456605-4a044aa38fbc?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</CardPreview>
<Content>
<Text slot="title">Lion</Text>
<Text slot="description">JPEG • 8/28/2021</Text>
</Content>
</AssetCard>
<AssetCard id={4}>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1722172118908-1a97c312ce8c?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</CardPreview>
<Content>
<Text slot="title">Mountain Sunrise</Text>
<Text slot="description">PNG • 3/15/2015</Text>
</Content>
</AssetCard>
<AssetCard id={5}>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1574870111867-089730e5a72b?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</CardPreview>
<Content>
<Text slot="title">Giraffe tongue</Text>
<Text slot="description">PNG • 11/27/2019</Text>
</Content>
</AssetCard>
<AssetCard id={6}>
<CardPreview>
<Image src="https://images.unsplash.com/photo-1718378037953-ab21bf2cf771?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</CardPreview>
<Content>
<Text slot="title">Golden Hour</Text>
<Text slot="description">WEBP • 7/24/2024</Text>
</Content>
</AssetCard>
</CardView>
<p>Current selection: {selected === 'all' ? 'all' : [...selected].join(', ')}</p>
</>
);
}
API
<CardView>
<Card />
<SkeletonCollection />
</CardView>
CardView
Name | Type | Default |
---|---|---|
layout | 'grid' | 'waterfall' | Default: 'grid'
|
The layout of the cards. | ||
size | 'XS'
| 'S'
| 'M'
| 'L'
| 'XL' | Default: 'M'
|
The size of the cards. | ||
density | 'compact'
| 'regular'
| 'spacious' | Default: 'regular'
|
The amount of space between the cards. | ||
variant | 'primary'
| 'secondary'
| 'tertiary'
| 'quiet' | Default: 'primary'
|
The visual style of the cards. | ||
selectionStyle | 'checkbox' | 'highlight' | Default: 'checkbox'
|
How selection should be displayed. | ||
styles | StylesPropWithHeight | Default: — |
Spectrum-defined styles, returned by the style() macro. | ||
renderActionBar |
| Default: — |
Provides the ActionBar to render when cards are selected in the CardView. | ||
disallowTypeAhead | boolean | Default: false
|
Whether typeahead navigation is disabled. | ||
dragAndDropHooks | DragAndDropHooks | Default: — |
The drag and drop hooks returned by useDragAndDrop used to enable drag and drop behavior for the GridList. | ||
children | ReactNode | | Default: — |
The contents of the collection. | ||
items | Iterable | Default: — |
Item objects in the collection. | ||
loadingState | LoadingState | Default: — |
The loading state of the CardView. | ||
onLoadMore |
| Default: — |
Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. | ||
renderEmptyState |
| Default: — |
Provides content to display when there are no items in the list. | ||
dependencies | ReadonlyArray | Default: — |
Values that should invalidate the item cache when using dynamic collections. | ||
selectionMode | SelectionMode | Default: — |
The type of selection that is allowed in the collection. | ||
selectedKeys | 'all' | Iterable | Default: — |
The currently selected keys in the collection (controlled). | ||
defaultSelectedKeys | 'all' | Iterable | Default: — |
The initial selected keys in the collection (uncontrolled). | ||
onSelectionChange |
| Default: — |
Handler that is called when the selection changes. | ||
disabledKeys | Iterable | Default: — |
The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. | ||
disabledBehavior | DisabledBehavior | Default: "all"
|
Whether disabledKeys applies to all interactions, or only selection. | ||
disallowEmptySelection | boolean | Default: — |
Whether the collection allows empty selection. | ||
shouldSelectOnPressUp | boolean | Default: — |
Whether selection should occur on press up instead of press down. | ||
escapeKeyBehavior | 'clearSelection' | 'none' | Default: 'clearSelection'
|
Whether pressing the escape key should clear selection in the grid list or not. Most experiences should not modify this option as it eliminates a keyboard user's ability to easily clear selection. Only use if the escape key is being handled externally or should not trigger selection clearing contextually. | ||