CardView
CardViews display a collection of items in a variety of layouts, providing users with a visual representation of the collection's contents.
| install | yarn add @react-spectrum/card |
|---|---|
| version | 3.0.0-alpha.1 |
| usage | import {Card, CardView, GalleryLayout, GridLayout, WaterfallLayout} from '@react-spectrum/card' |
Example#
import {Content} from '@react-spectrum/view';
import {Heading, Text} from '@react-spectrum/text';
import {Image} from '@react-spectrum/image';
<CardView
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="Static CardView Pokemon example"
>
<Card key="1" width={475} height={475} textValue="Bulbasaur">
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/001.png" />
<Heading>Bulbasaur</Heading>
<Text slot="detail">PNG</Text>
<Content>Grass, Poison</Content>
</Card>
<Card key="2" width={475} height={475} textValue="Ivysaur">
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/002.png" />
<Heading>Ivysaur</Heading>
<Text slot="detail">PNG</Text>
<Content>Grass, Poison</Content>
</Card>
<Card key="3" width={475} height={475} textValue="Venusaur">
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/003.png" />
<Heading>Venusaur</Heading>
<Text slot="detail">PNG</Text>
<Content>Grass, Poison</Content>
</Card>
<Card key="4" width={475} height={475} textValue="Charmander">
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/004.png" />
<Heading>Charmander</Heading>
<Text slot="detail">PNG</Text>
<Content>Fire</Content>
</Card>
<Card key="5" width={475} height={475} textValue="Charmeleon">
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/005.png" />
<Heading>Charmeleon</Heading>
<Text slot="detail">PNG</Text>
<Content>Fire</Content>
</Card>
<Card key="6" width={475} height={475} textValue="Charizard">
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/006.png" />
<Heading>Charizard</Heading>
<Text slot="detail">PNG</Text>
<Content>Fire, Flying</Content>
</Card>
</CardView>
import {Content} from '@react-spectrum/view';
import {Heading, Text} from '@react-spectrum/text';
import {Image} from '@react-spectrum/image';
<CardView
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="Static CardView Pokemon example"
>
<Card
key="1"
width={475}
height={475}
textValue="Bulbasaur"
>
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/001.png" />
<Heading>Bulbasaur</Heading>
<Text slot="detail">PNG</Text>
<Content>Grass, Poison</Content>
</Card>
<Card
key="2"
width={475}
height={475}
textValue="Ivysaur"
>
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/002.png" />
<Heading>Ivysaur</Heading>
<Text slot="detail">PNG</Text>
<Content>Grass, Poison</Content>
</Card>
<Card
key="3"
width={475}
height={475}
textValue="Venusaur"
>
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/003.png" />
<Heading>Venusaur</Heading>
<Text slot="detail">PNG</Text>
<Content>Grass, Poison</Content>
</Card>
<Card
key="4"
width={475}
height={475}
textValue="Charmander"
>
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/004.png" />
<Heading>Charmander</Heading>
<Text slot="detail">PNG</Text>
<Content>Fire</Content>
</Card>
<Card
key="5"
width={475}
height={475}
textValue="Charmeleon"
>
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/005.png" />
<Heading>Charmeleon</Heading>
<Text slot="detail">PNG</Text>
<Content>Fire</Content>
</Card>
<Card
key="6"
width={475}
height={475}
textValue="Charizard"
>
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/006.png" />
<Heading>Charizard</Heading>
<Text slot="detail">PNG</Text>
<Content>Fire, Flying</Content>
</Card>
</CardView>
import {Content} from '@react-spectrum/view';
import {
Heading,
Text
} from '@react-spectrum/text';
import {Image} from '@react-spectrum/image';
<CardView
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="Static CardView Pokemon example"
>
<Card
key="1"
width={475}
height={475}
textValue="Bulbasaur"
>
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/001.png" />
<Heading>
Bulbasaur
</Heading>
<Text slot="detail">
PNG
</Text>
<Content>
Grass, Poison
</Content>
</Card>
<Card
key="2"
width={475}
height={475}
textValue="Ivysaur"
>
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/002.png" />
<Heading>
Ivysaur
</Heading>
<Text slot="detail">
PNG
</Text>
<Content>
Grass, Poison
</Content>
</Card>
<Card
key="3"
width={475}
height={475}
textValue="Venusaur"
>
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/003.png" />
<Heading>
Venusaur
</Heading>
<Text slot="detail">
PNG
</Text>
<Content>
Grass, Poison
</Content>
</Card>
<Card
key="4"
width={475}
height={475}
textValue="Charmander"
>
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/004.png" />
<Heading>
Charmander
</Heading>
<Text slot="detail">
PNG
</Text>
<Content>
Fire
</Content>
</Card>
<Card
key="5"
width={475}
height={475}
textValue="Charmeleon"
>
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/005.png" />
<Heading>
Charmeleon
</Heading>
<Text slot="detail">
PNG
</Text>
<Content>
Fire
</Content>
</Card>
<Card
key="6"
width={475}
height={475}
textValue="Charizard"
>
<Image src="https://assets.pokemon.com/assets/cms2/img/pokedex/full/006.png" />
<Heading>
Charizard
</Heading>
<Text slot="detail">
PNG
</Text>
<Content>
Fire, Flying
</Content>
</Card>
</CardView>
Content#
CardView is a collection component that displays a collection of cards in a variety of layouts. It follows the Collection Components API, accepting both static and dynamic collections.
It expects <Card> elements as children, which in turn accept a variety of other content elements as described here. Static collections, as in the previous example, can be used when the full list of cards is known ahead of time.
Dynamic collections, as shown below, can be used when the card data come from an external data source such as an API call, or update over time. As seen below, an iterable list of items is passed to the CardView using the items prop.
Each card accepts a key prop, which is passed to the onSelectionChange handler to identify the selected card. Alternatively, if the item objects contain an id property, as shown in the example below, then this is used automatically and a key prop is not required.
See the Selection section for more detail on selection.
let items = [
{
id: 1,
name: 'Bulbasaur',
type: 'Grass, Poison',
width: 475,
height: 475,
src: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/001.png'
},
{
id: 2,
name: 'Ivysaur',
type: 'Grass, Poison',
width: 475,
height: 475,
src: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/002.png'
},
{
id: 3,
name: 'Venusaur',
type: 'Grass, Poison',
width: 475,
height: 475,
src: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/003.png'
},
{
id: 4,
name: 'Charmander',
type: 'Fire',
width: 475,
height: 475,
src: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/004.png'
},
{
id: 5,
name: 'Charmeleon',
type: 'Fire',
width: 475,
height: 475,
src: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/005.png'
},
{
id: 6,
name: 'Charizard',
type: 'Fire, Flying',
width: 475,
height: 475,
src: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/006.png'
}
];
<CardView
items={items}
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="Dynamic CardView Pokemon example"
>
{(item) => (
<Card textValue={item.name} width={item.width} height={item.height}>
<Image src={item.src} />
<Heading>
{item.name}
</Heading>
<Text slot="detail">
PNG
</Text>
<Content>{item.type}</Content>
</Card>
)}
</CardView>
let items = [
{
id: 1,
name: 'Bulbasaur',
type: 'Grass, Poison',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/001.png'
},
{
id: 2,
name: 'Ivysaur',
type: 'Grass, Poison',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/002.png'
},
{
id: 3,
name: 'Venusaur',
type: 'Grass, Poison',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/003.png'
},
{
id: 4,
name: 'Charmander',
type: 'Fire',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/004.png'
},
{
id: 5,
name: 'Charmeleon',
type: 'Fire',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/005.png'
},
{
id: 6,
name: 'Charizard',
type: 'Fire, Flying',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/006.png'
}
];
<CardView
items={items}
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="Dynamic CardView Pokemon example"
>
{(item) => (
<Card
textValue={item.name}
width={item.width}
height={item.height}
>
<Image src={item.src} />
<Heading>{item.name}</Heading>
<Text slot="detail">PNG</Text>
<Content>{item.type}</Content>
</Card>
)}
</CardView>
let items = [
{
id: 1,
name: 'Bulbasaur',
type:
'Grass, Poison',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/001.png'
},
{
id: 2,
name: 'Ivysaur',
type:
'Grass, Poison',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/002.png'
},
{
id: 3,
name: 'Venusaur',
type:
'Grass, Poison',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/003.png'
},
{
id: 4,
name: 'Charmander',
type: 'Fire',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/004.png'
},
{
id: 5,
name: 'Charmeleon',
type: 'Fire',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/005.png'
},
{
id: 6,
name: 'Charizard',
type: 'Fire, Flying',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/006.png'
}
];
<CardView
items={items}
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="Dynamic CardView Pokemon example"
>
{(item) => (
<Card
textValue={item
.name}
width={item
.width}
height={item
.height}
>
<Image
src={item.src}
/>
<Heading>
{item.name}
</Heading>
<Text slot="detail">
PNG</Text>
<Content>
{item.type}
</Content>
</Card>
)}
</CardView>
Cards#
A standard Card consists of a preview image and a title, and allows for a optional detail area next to the title and an optional content area immediately below the
title. The image, title, and content area can be populated by providing a Image, Heading, and Content respectively as children to
the Card. The detail area can be populated through a Text element with an accompanying slot="detail" prop. Please note that a Spectrum compliant Card should not contain
focusable or interactive content within the content area and that the content itself should only display the most essential pieces of information. If you need to perform actions
on a selected card(s), please use the ActionBar component in lieu of adding an action menu to the Card itself. See the Card Actions section for an example.
The properties that the Card supports can be found in the Props table below. Note that the height and width props refer to the image's height and width, not the Card's height and
width. These values are used to calculate the proper positioning and size of each card within the CardView's container.
Internationalization#
To internationalize a CardView, all text content within the Cards should be localized. The aria-label provided to the CardView should also be localized any.
For languages that are read right-to-left (e.g. Hebrew and Arabic), the layout of CardView is automatically flipped.
Layout#
CardView supports three different visual layouts: GridLayout, GalleryLayout
and WaterfallLayout. You can set the CardView's layout by passing the imported class directly to the layout prop as shown in the previous examples.
This will handle instantiating said layout with its default options. Alternatively, you can modify the layout's various options (e.g. margin, collator, etc) by constructing your own instance of the
class and providing that to the CardView instead. See the sections below for examples of this.
GridLayout#
The GridLayout renders each card with a uniform height and width regardless of the image size. The customizable options available to the GridLayout are as follows:
// TODO figure out how to show a table for the GridLayoutOptions
The example below shows a CardView with a modified GridLayout that decreases the minimum required space between the cards and sets the maximum number of columns to 2.
import {Size} from '@react-stately/virtualizer';
import {useCollator} from '@react-aria/i18n';
import {useProvider} from '@react-spectrum/provider';
function CustomLayout(props) {
let items = [
{
width: 1001,
height: 381,
src: 'https://i.imgur.com/Z7AzH2c.jpg',
title: 'Temple Roof',
ext: 'JPG'
},
{
width: 640,
height: 640,
src: 'https://i.imgur.com/DhygPot.jpg',
title: 'Starry Sky',
ext: 'JPG'
},
{
width: 1516,
height: 1009,
src: 'https://i.imgur.com/1nScMIH.jpg',
title: 'Molten glass',
ext: 'JPG'
},
{
width: 1215,
height: 121,
src: 'https://i.imgur.com/zzwWogn.jpg',
title: 'Mountain Range',
ext: 'JPG'
}
];
let { scale } = useProvider();
let collator = useCollator({ usage: 'search', sensitivity: 'base' });
let gridLayout = React.useMemo(() =>
new GridLayout({
scale,
collator,
minSpace: new Size(5, 5),
maxColumns: 2
}), [collator, scale]);
let {
layout = gridLayout,
...otherProps
} = props;
return (
<CardView
items={items}
layout={layout}
maxWidth="800px"
height="500px"
aria-label="CardView with modified GridLayout"
{...otherProps}
>
{(item) => (
<Card
key={item.title}
textValue={item.title}
width={item.width}
height={item.height}
>
<Image src={item.src} />
<Heading>{item.title}</Heading>
<Text slot="detail">{item.ext}</Text>
<Content>Image dimensions: {item.width}x{item.height}</Content>
</Card>
)}
</CardView>
);
}
<CustomLayout />
import {Size} from '@react-stately/virtualizer';
import {useCollator} from '@react-aria/i18n';
import {useProvider} from '@react-spectrum/provider';
function CustomLayout(props) {
let items = [
{
width: 1001,
height: 381,
src: 'https://i.imgur.com/Z7AzH2c.jpg',
title: 'Temple Roof',
ext: 'JPG'
},
{
width: 640,
height: 640,
src: 'https://i.imgur.com/DhygPot.jpg',
title: 'Starry Sky',
ext: 'JPG'
},
{
width: 1516,
height: 1009,
src: 'https://i.imgur.com/1nScMIH.jpg',
title: 'Molten glass',
ext: 'JPG'
},
{
width: 1215,
height: 121,
src: 'https://i.imgur.com/zzwWogn.jpg',
title: 'Mountain Range',
ext: 'JPG'
}
];
let { scale } = useProvider();
let collator = useCollator({
usage: 'search',
sensitivity: 'base'
});
let gridLayout = React.useMemo(() =>
new GridLayout({
scale,
collator,
minSpace: new Size(5, 5),
maxColumns: 2
}), [collator, scale]);
let {
layout = gridLayout,
...otherProps
} = props;
return (
<CardView
items={items}
layout={layout}
maxWidth="800px"
height="500px"
aria-label="CardView with modified GridLayout"
{...otherProps}
>
{(item) => (
<Card
key={item.title}
textValue={item.title}
width={item.width}
height={item.height}
>
<Image src={item.src} />
<Heading>{item.title}</Heading>
<Text slot="detail">{item.ext}</Text>
<Content>
Image dimensions: {item.width}x{item.height}
</Content>
</Card>
)}
</CardView>
);
}
<CustomLayout />
import {Size} from '@react-stately/virtualizer';
import {useCollator} from '@react-aria/i18n';
import {useProvider} from '@react-spectrum/provider';
function CustomLayout(
props
) {
let items = [
{
width: 1001,
height: 381,
src:
'https://i.imgur.com/Z7AzH2c.jpg',
title:
'Temple Roof',
ext: 'JPG'
},
{
width: 640,
height: 640,
src:
'https://i.imgur.com/DhygPot.jpg',
title:
'Starry Sky',
ext: 'JPG'
},
{
width: 1516,
height: 1009,
src:
'https://i.imgur.com/1nScMIH.jpg',
title:
'Molten glass',
ext: 'JPG'
},
{
width: 1215,
height: 121,
src:
'https://i.imgur.com/zzwWogn.jpg',
title:
'Mountain Range',
ext: 'JPG'
}
];
let { scale } =
useProvider();
let collator =
useCollator({
usage: 'search',
sensitivity: 'base'
});
let gridLayout = React
.useMemo(
() =>
new GridLayout({
scale,
collator,
minSpace:
new Size(
5,
5
),
maxColumns: 2
}),
[collator, scale]
);
let {
layout = gridLayout,
...otherProps
} = props;
return (
<CardView
items={items}
layout={layout}
maxWidth="800px"
height="500px"
aria-label="CardView with modified GridLayout"
{...otherProps}
>
{(item) => (
<Card
key={item
.title}
textValue={item
.title}
width={item
.width}
height={item
.height}
>
<Image
src={item
.src}
/>
<Heading>
{item.title}
</Heading>
<Text slot="detail">
{item.ext}
</Text>
<Content>
Image
dimensions:
{' '}
{item
.width}x{item
.height}
</Content>
</Card>
)}
</CardView>
);
}
<CustomLayout />
GalleryLayout#
The GalleryLayout arranges each card in such a way that each card in a row shares the same height but has a width that accommodates the image's aspect ratio. The layout will attempt to take up all available space in the CardView and thus will distribute the cards in each row accordingly. The customizable options available to the GalleryLayout are as follows:
// TODO figure out how to show a table for the GalleryLayoutOptions
The example below shows a CardView with a modified GalleryLayout that adjusts the space between the cards and increases the ideal row height used in the layout calculations.
// Using the same CardView as above the GridLayout example, but applying a modified GalleryLayout instead
function CustomGalleryLayout() {
let { scale } = useProvider();
let collator = useCollator({ usage: 'search', sensitivity: 'base' });
let galleryLayout = React.useMemo(() =>
new GalleryLayout({
collator,
scale,
itemSpacing: new Size(50, 18)
}), [collator, scale]);
return (
<CustomLayout
layout={galleryLayout}
aria-label="CardView with modified GalleryLayout"
/>
);
}
<CustomGalleryLayout />
// Using the same CardView as above the GridLayout example, but applying a modified GalleryLayout instead
function CustomGalleryLayout() {
let { scale } = useProvider();
let collator = useCollator({
usage: 'search',
sensitivity: 'base'
});
let galleryLayout = React.useMemo(
() =>
new GalleryLayout({
collator,
scale,
itemSpacing: new Size(50, 18)
}),
[collator, scale]
);
return (
<CustomLayout
layout={galleryLayout}
aria-label="CardView with modified GalleryLayout"
/>
);
}
<CustomGalleryLayout />
// Using the same CardView as above the GridLayout example, but applying a modified GalleryLayout instead
function CustomGalleryLayout() {
let { scale } =
useProvider();
let collator =
useCollator({
usage: 'search',
sensitivity: 'base'
});
let galleryLayout =
React.useMemo(
() =>
new GalleryLayout(
{
collator,
scale,
itemSpacing:
new Size(
50,
18
)
}
),
[collator, scale]
);
return (
<CustomLayout
layout={galleryLayout}
aria-label="CardView with modified GalleryLayout"
/>
);
}
<CustomGalleryLayout />
WaterfallLayout#
The WaterfallLayout organizes the cards into equal width columns but allows each card to have a variable height. Similarly to GalleryLayout, it attempts to accommodate the aspect ratio of each card's image. The customizable options available to the GalleryLayout are as follows:
// TODO figure out how to show a table for the WaterfallLayoutOptions
The example below shows a CardView with a modified WaterfallLayout adjusts the minimum required space between the cards and sets the maximum number of columns to 2.
// Using the same CardView as above the GridLayout example, but applying a modified WaterfallLayout instead
function CustomWaterfallLayout() {
let { scale } = useProvider();
let collator = useCollator({ usage: 'search', sensitivity: 'base' });
let waterfallLayout = React.useMemo(() =>
new WaterfallLayout({
collator,
scale,
maxColumns: 2,
minSpace: new Size(50, 50)
}), [collator, scale]);
return (
<CustomLayout
layout={waterfallLayout}
aria-label="CardView with modified WaterfallLayout"
/>
);
}
<CustomWaterfallLayout />
// Using the same CardView as above the GridLayout example, but applying a modified WaterfallLayout instead
function CustomWaterfallLayout() {
let { scale } = useProvider();
let collator = useCollator({
usage: 'search',
sensitivity: 'base'
});
let waterfallLayout = React.useMemo(
() =>
new WaterfallLayout({
collator,
scale,
maxColumns: 2,
minSpace: new Size(50, 50)
}),
[collator, scale]
);
return (
<CustomLayout
layout={waterfallLayout}
aria-label="CardView with modified WaterfallLayout"
/>
);
}
<CustomWaterfallLayout />
// Using the same CardView as above the GridLayout example, but applying a modified WaterfallLayout instead
function CustomWaterfallLayout() {
let { scale } =
useProvider();
let collator =
useCollator({
usage: 'search',
sensitivity: 'base'
});
let waterfallLayout =
React.useMemo(
() =>
new WaterfallLayout(
{
collator,
scale,
maxColumns:
2,
minSpace:
new Size(
50,
50
)
}
),
[collator, scale]
);
return (
<CustomLayout
layout={waterfallLayout}
aria-label="CardView with modified WaterfallLayout"
/>
);
}
<CustomWaterfallLayout />
Labeling#
Accessibility#
An aria-label must be provided to the CardView for accessibility. If the CardView is labeled by a separate element, an aria-labelledby prop must be provided using the id of the labeling element instead.
Asynchronous loading#
CardView supports loading data asynchronously, and will display a progress circle reflecting the current load state,
set by the loadingState prop. It also supports infinite scrolling to load more data on demand as the user scrolls, via the onLoadMore prop.
This example uses the useAsyncList hook to handle loading the data. See the docs for more information.
import {useAsyncList} from '@react-stately/data';
function AsyncLoadingCardView() {
let imageLinks = React.useRef(null);
let list = useAsyncList({
async load({ signal, cursor }) {
if (cursor) {
cursor = cursor.replace(/^http:\/\//i, 'https://');
}
if (imageLinks.current == null) {
let fullRes = await fetch(
'https://rawcdn.githack.com/akabab/starwars-api/0.2.1/api/all.json'
);
imageLinks.current = await fullRes.json();
}
let items = [];
let res = await fetch(
cursor || 'https://swapi.py4e.com/api/people/?search',
{ signal }
);
let json = await res.json();
items = json.results.map((element) => ({
src: imageLinks.current.find((char) => char.name === element.name)
?.image,
title: element.name,
mass: element.mass
}));
return {
items: items,
cursor: json.next
};
}
});
return (
<CardView
items={list.items}
onLoadMore={list.loadMore}
loadingState={list.loadingState}
layout={GridLayout}
maxWidth="800px"
height="500px"
aria-label="Async Star Wars CardView"
>
{(item) => (
<Card key={item.title} textValue={item.title}>
<Image src={item.src} />
<Heading>{item.title}</Heading>
<Content>Mass: {item.mass}</Content>
</Card>
)}
</CardView>
);
}
import {useAsyncList} from '@react-stately/data';
function AsyncLoadingCardView() {
let imageLinks = React.useRef(null);
let list = useAsyncList({
async load({ signal, cursor }) {
if (cursor) {
cursor = cursor.replace(/^http:\/\//i, 'https://');
}
if (imageLinks.current == null) {
let fullRes = await fetch(
'https://rawcdn.githack.com/akabab/starwars-api/0.2.1/api/all.json'
);
imageLinks.current = await fullRes.json();
}
let items = [];
let res = await fetch(
cursor ||
'https://swapi.py4e.com/api/people/?search',
{ signal }
);
let json = await res.json();
items = json.results.map((element) => ({
src: imageLinks.current.find((char) =>
char.name === element.name
)?.image,
title: element.name,
mass: element.mass
}));
return {
items: items,
cursor: json.next
};
}
});
return (
<CardView
items={list.items}
onLoadMore={list.loadMore}
loadingState={list.loadingState}
layout={GridLayout}
maxWidth="800px"
height="500px"
aria-label="Async Star Wars CardView"
>
{(item) => (
<Card key={item.title} textValue={item.title}>
<Image src={item.src} />
<Heading>
{item.title}
</Heading>
<Content>Mass: {item.mass}</Content>
</Card>
)}
</CardView>
);
}
import {useAsyncList} from '@react-stately/data';
function AsyncLoadingCardView() {
let imageLinks = React
.useRef(null);
let list =
useAsyncList({
async load(
{
signal,
cursor
}
) {
if (cursor) {
cursor = cursor
.replace(
/^http:\/\//i,
'https://'
);
}
if (
imageLinks
.current ==
null
) {
let fullRes =
await fetch(
'https://rawcdn.githack.com/akabab/starwars-api/0.2.1/api/all.json'
);
imageLinks
.current =
await fullRes
.json();
}
let items = [];
let res =
await fetch(
cursor ||
'https://swapi.py4e.com/api/people/?search',
{ signal }
);
let json =
await res
.json();
items = json
.results.map((
element
) => ({
src:
imageLinks
.current
.find(
(char) =>
char
.name ===
element
.name
)?.image,
title:
element
.name,
mass:
element
.mass
}));
return {
items: items,
cursor:
json.next
};
}
});
return (
<CardView
items={list.items}
onLoadMore={list
.loadMore}
loadingState={list
.loadingState}
layout={GridLayout}
maxWidth="800px"
height="500px"
aria-label="Async Star Wars CardView"
>
{(item) => (
<Card
key={item
.title}
textValue={item
.title}
>
<Image
src={item
.src}
/>
<Heading>
{item.title}
</Heading>
<Content>Mass:
{' '}
{item.mass}
</Content>
</Card>
)}
</CardView>
);
}
Selection#
By default, CardView doesn't allow card selection but this can be enabled using the selectionMode prop.
Use defaultSelectedKeys to provide a default set of selected Cards. Note that the value of the selected keys must match the key prop of the card.
The example below enables multiple selection mode, and uses defaultSelectedKeys to select the Cards with keys 2 and 4.
let items = [
{
id: 1,
name: 'Bulbasaur',
type: 'Grass, Poison',
width: 475,
height: 475,
src: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/001.png'
},
{
id: 2,
name: 'Ivysaur',
type: 'Grass, Poison',
width: 475,
height: 475,
src: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/002.png'
},
{
id: 3,
name: 'Venusaur',
type: 'Grass, Poison',
width: 475,
height: 475,
src: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/003.png'
},
{
id: 4,
name: 'Charmander',
type: 'Fire',
width: 475,
height: 475,
src: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/004.png'
},
{
id: 5,
name: 'Charmeleon',
type: 'Fire',
width: 475,
height: 475,
src: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/005.png'
},
{
id: 6,
name: 'Charizard',
type: 'Fire, Flying',
width: 475,
height: 475,
src: 'https://assets.pokemon.com/assets/cms2/img/pokedex/full/006.png'
}
];
<CardView
items={items}
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="Default selection CardView Pokemon example"
selectionMode="multiple"
defaultSelectedKeys={[2, 4]}
>
{(item) => (
<Card textValue={item.name} width={item.width} height={item.height}>
<Image src={item.src} />
<Heading>
{item.name}
</Heading>
<Text slot="detail">
PNG
</Text>
<Content>{item.type}</Content>
</Card>
)}
</CardView>
let items = [
{
id: 1,
name: 'Bulbasaur',
type: 'Grass, Poison',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/001.png'
},
{
id: 2,
name: 'Ivysaur',
type: 'Grass, Poison',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/002.png'
},
{
id: 3,
name: 'Venusaur',
type: 'Grass, Poison',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/003.png'
},
{
id: 4,
name: 'Charmander',
type: 'Fire',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/004.png'
},
{
id: 5,
name: 'Charmeleon',
type: 'Fire',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/005.png'
},
{
id: 6,
name: 'Charizard',
type: 'Fire, Flying',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/006.png'
}
];
<CardView
items={items}
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="Default selection CardView Pokemon example"
selectionMode="multiple"
defaultSelectedKeys={[2, 4]}
>
{(item) => (
<Card
textValue={item.name}
width={item.width}
height={item.height}
>
<Image src={item.src} />
<Heading>{item.name}</Heading>
<Text slot="detail">PNG</Text>
<Content>{item.type}</Content>
</Card>
)}
</CardView>
let items = [
{
id: 1,
name: 'Bulbasaur',
type:
'Grass, Poison',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/001.png'
},
{
id: 2,
name: 'Ivysaur',
type:
'Grass, Poison',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/002.png'
},
{
id: 3,
name: 'Venusaur',
type:
'Grass, Poison',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/003.png'
},
{
id: 4,
name: 'Charmander',
type: 'Fire',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/004.png'
},
{
id: 5,
name: 'Charmeleon',
type: 'Fire',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/005.png'
},
{
id: 6,
name: 'Charizard',
type: 'Fire, Flying',
width: 475,
height: 475,
src:
'https://assets.pokemon.com/assets/cms2/img/pokedex/full/006.png'
}
];
<CardView
items={items}
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="Default selection CardView Pokemon example"
selectionMode="multiple"
defaultSelectedKeys={[
2,
4
]}
>
{(item) => (
<Card
textValue={item
.name}
width={item
.width}
height={item
.height}
>
<Image
src={item.src}
/>
<Heading>
{item.name}
</Heading>
<Text slot="detail">
PNG</Text>
<Content>
{item.type}
</Content>
</Card>
)}
</CardView>
Controlled selection#
To programmatically control card selection, use the selectedKeys prop paired with the onSelectionChange callback. The key prop from the selected card will
be passed into the callback when the card is pressed, allowing you to update state accordingly.
Here is how you would control selection for the above example.
function PokemonCardView(props) {
let items = [
// Same items as in the pokemon examples...
];
let [selectedKeys, setSelectedKeys] = React.useState(new Set([2]));
return (
<CardView
items={items}
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="Controlled selection CardView Pokemon example"
selectionMode="multiple"
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
{...props}
>
{(item) => (
<Card textValue={item.name} width={item.width} height={item.height}>
<Image src={item.src} />
<Heading>
{item.name}
</Heading>
<Text slot="detail">
PNG
</Text>
<Content>{item.type}</Content>
</Card>
)}
</CardView>
);
}
function PokemonCardView(props) {
let items = [
// Same items as in the pokemon examples...
];
let [selectedKeys, setSelectedKeys] = React.useState(
new Set([2])
);
return (
<CardView
items={items}
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="Controlled selection CardView Pokemon example"
selectionMode="multiple"
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
{...props}
>
{(item) => (
<Card
textValue={item.name}
width={item.width}
height={item.height}
>
<Image src={item.src} />
<Heading>{item.name}</Heading>
<Text slot="detail">PNG</Text>
<Content>{item.type}</Content>
</Card>
)}
</CardView>
);
}
function PokemonCardView(
props
) {
let items = [
// Same items as in the pokemon examples...
];
let [
selectedKeys,
setSelectedKeys
] = React.useState(
new Set([2])
);
return (
<CardView
items={items}
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="Controlled selection CardView Pokemon example"
selectionMode="multiple"
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
{...props}
>
{(item) => (
<Card
textValue={item
.name}
width={item
.width}
height={item
.height}
>
<Image
src={item
.src}
/>
<Heading>
{item.name}
</Heading>
<Text slot="detail">
PNG</Text>
<Content>
{item.type}
</Content>
</Card>
)}
</CardView>
);
}
Single selection#
To limit users to selecting only a single card at a time, selectionMode can be set to single.
// Using the same CardView as above
<PokemonCardView selectionMode="single" />
// Using the same CardView as above
<PokemonCardView selectionMode="single" />
// Using the same CardView as above
<PokemonCardView selectionMode="single" />
Disallow empty selection#
CardView also supports a disallowEmptySelection prop which forces the user to have at least one Card in the CardView selected at all times.
In this mode, if a single card is selected and the user presses it, it will not be deselected.
// Using the same CardView as above
<PokemonCardView disallowEmptySelection />
// Using the same CardView as above
<PokemonCardView disallowEmptySelection />
// Using the same CardView as above
<PokemonCardView
disallowEmptySelection
/>
Disabled cards#
You can disable specific card by providing an array of keys to CardView via the disabledKeys prop. This will prevent cards from being selectable as shown in the example below.
// Using the same CardView as above
<PokemonCardView selectionMode="multiple" disabledKeys={[3]} />
// Using the same CardView as above
<PokemonCardView
selectionMode="multiple"
disabledKeys={[3]}
/>
// Using the same CardView as above
<PokemonCardView
selectionMode="multiple"
disabledKeys={[3]}
/>
Card actions#
A common use case is to allow a user to perform actions on one or more cards. The example below illustrates how one could use ActionBar to trigger an action on the currently selected card.
import {ActionBar, ActionBarContainer, Item} from '@react-spectrum/actionbar';
import Copy from '@spectrum-icons/workflow/Copy';
import Delete from '@spectrum-icons/workflow/Delete';
import Edit from '@spectrum-icons/workflow/Edit';
let items = [
// Same items as in the pokemon examples...
];
function CardViewActions() {
let [selectedKeys, setSelectedKeys] = React.useState(new Set([2]));
return (
<ActionBarContainer height="500px">
<CardView
items={items}
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="CardView Pokemon with ActionBar example"
selectionMode="single"
>
{(item) => (
<Card textValue={item.name} width={item.width} height={item.height}>
<Image src={item.src} />
<Heading>
{item.name}
</Heading>
<Text slot="detail">
PNG
</Text>
<Content>{item.type}</Content>
</Card>
)}
</CardView>
<ActionBar
selectedItemCount={selectedKeys.size}
onClearSelection={() => {
setSelectedKeys(new Set());
}}
onAction={(key) =>
alert(
`Performing action on `
)}
>
<Item key="edit">
<Edit />
<Text>Edit</Text>
</Item>
<Item key="copy">
<Copy />
<Text>Copy</Text>
</Item>
<Item key="delete">
<Delete />
<Text>Delete</Text>
</Item>
</ActionBar>
</ActionBarContainer>
);
}
<CardViewActions />
import {
ActionBar,
ActionBarContainer,
Item
} from '@react-spectrum/actionbar';
import Copy from '@spectrum-icons/workflow/Copy';
import Delete from '@spectrum-icons/workflow/Delete';
import Edit from '@spectrum-icons/workflow/Edit';
let items = [
// Same items as in the pokemon examples...
];
function CardViewActions() {
let [selectedKeys, setSelectedKeys] = React.useState(
new Set([2])
);
return (
<ActionBarContainer height="500px">
<CardView
items={items}
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="CardView Pokemon with ActionBar example"
selectionMode="single"
>
{(item) => (
<Card
textValue={item.name}
width={item.width}
height={item.height}
>
<Image src={item.src} />
<Heading>{item.name}</Heading>
<Text slot="detail">PNG</Text>
<Content>{item.type}</Content>
</Card>
)}
</CardView>
<ActionBar
selectedItemCount={selectedKeys.size}
onClearSelection={() => {
setSelectedKeys(new Set());
}}
onAction={(key) =>
alert(
`Performing action on `
)}
>
<Item key="edit">
<Edit />
<Text>Edit</Text>
</Item>
<Item key="copy">
<Copy />
<Text>Copy</Text>
</Item>
<Item key="delete">
<Delete />
<Text>Delete</Text>
</Item>
</ActionBar>
</ActionBarContainer>
);
}
<CardViewActions />
import {
ActionBar,
ActionBarContainer,
Item
} from '@react-spectrum/actionbar';
import Copy from '@spectrum-icons/workflow/Copy';
import Delete from '@spectrum-icons/workflow/Delete';
import Edit from '@spectrum-icons/workflow/Edit';
let items = [
// Same items as in the pokemon examples...
];
function CardViewActions() {
let [
selectedKeys,
setSelectedKeys
] = React.useState(
new Set([2])
);
return (
<ActionBarContainer height="500px">
<CardView
items={items}
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
height="500px"
maxWidth="800px"
layout={GridLayout}
aria-label="CardView Pokemon with ActionBar example"
selectionMode="single"
>
{(item) => (
<Card
textValue={item
.name}
width={item
.width}
height={item
.height}
>
<Image
src={item
.src}
/>
<Heading>
{item.name}
</Heading>
<Text slot="detail">
PNG
</Text>
<Content>
{item.type}
</Content>
</Card>
)}
</CardView>
<ActionBar
selectedItemCount={selectedKeys
.size}
onClearSelection={() => {
setSelectedKeys(
new Set()
);
}}
onAction={(key) =>
alert(
`Performing action on `
)}
>
<Item key="edit">
<Edit />
<Text>Edit
</Text>
</Item>
<Item key="copy">
<Copy />
<Text>Copy
</Text>
</Item>
<Item key="delete">
<Delete />
<Text>Delete
</Text>
</Item>
</ActionBar>
</ActionBarContainer>
);
}
<CardViewActions />
Props#
CardView props#
| Name | Type | Default | Description |
layout | CardViewLayoutConstructor<T> | CardViewLayout<T> | — | The layout that the CardView should use. Determines the visual layout of the cards and contains information such as the collecion of items, loading state, keyboard delegate, etc. The available layouts include GridLayout, GalleryLayout, and WaterfallLayout. See the Layout section for more information. |
children | CollectionChildren<T> | — | The contents of the collection. |
cardOrientation | Orientation | — | The orientation of the cards within the CardView. |
isQuiet | boolean | — | Whether the cards in the CardView should be displayed with a quiet style. Note this option is only valid for the waterfall layout and doesn't affect the cards for the other layouts. |
renderEmptyState | () => JSX.Element | — | Sets what the CardView should render when there is no content to display. |
loadingState | LoadingState | — | The current loading state of the CardView. |
items | Iterable<T> | — | Item objects in the collection. |
disabledKeys | Iterable<Key> | — | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. |
selectionMode | SelectionMode | — | The type of selection that is allowed in the collection. |
disallowEmptySelection | boolean | — | Whether the collection allows empty selection. |
selectedKeys | 'all' | Iterable<Key> | — | The currently selected keys in the collection (controlled). |
defaultSelectedKeys | 'all' | Iterable<Key> | — | The initial selected keys in the collection (uncontrolled). |
Events
| Name | Type | Default | Description |
onSelectionChange | (
(keys: Selection
)) => any | — | Handler that is called when the selection changes. |
onLoadMore | () => any | — | Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. |
Layout
| Name | Type | Default | Description |
flex | Responsive<string
| number
| boolean> | — | When used in a flex layout, specifies how the element will grow or shrink to fit the space available. See MDN. |
flexGrow | Responsive<number> | — | When used in a flex layout, specifies how the element will grow to fit the space available. See MDN. |
flexShrink | Responsive<number> | — | When used in a flex layout, specifies how the element will shrink to fit the space available. See MDN. |
flexBasis | Responsive<number | string> | — | When used in a flex layout, specifies the initial main size of the element. See MDN. |
alignSelf | Responsive<'auto'
| 'normal'
| 'start'
| 'end'
| 'center'
| 'flex-start'
| 'flex-end'
| 'self-start'
| 'self-end'
| 'stretch'> | — | Overrides the alignItems property of a flex or grid container. See MDN. |
justifySelf | Responsive<'auto'
| 'normal'
| 'start'
| 'end'
| 'flex-start'
| 'flex-end'
| 'self-start'
| 'self-end'
| 'center'
| 'left'
| 'right'
| 'stretch'> | — | Specifies how the element is justified inside a flex or grid container. See MDN. |
order | Responsive<number> | — | The layout order for the element within a flex or grid container. See MDN. |
gridArea | Responsive<string> | — | When used in a grid layout, specifies the named grid area that the element should be placed in within the grid. See MDN. |
gridColumn | Responsive<string> | — | When used in a grid layout, specifies the column the element should be placed in within the grid. See MDN. |
gridRow | Responsive<string> | — | When used in a grid layout, specifies the row the element should be placed in within the grid. See MDN. |
gridColumnStart | Responsive<string> | — | When used in a grid layout, specifies the starting column to span within the grid. See MDN. |
gridColumnEnd | Responsive<string> | — | When used in a grid layout, specifies the ending column to span within the grid. See MDN. |
gridRowStart | Responsive<string> | — | When used in a grid layout, specifies the starting row to span within the grid. See MDN. |
gridRowEnd | Responsive<string> | — | When used in a grid layout, specifies the ending row to span within the grid. See MDN. |
Spacing
| Name | Type | Default | Description |
margin | Responsive<DimensionValue> | — | The margin for all four sides of the element. See MDN. |
marginTop | Responsive<DimensionValue> | — | The margin for the top side of the element. See MDN. |
marginBottom | Responsive<DimensionValue> | — | The margin for the bottom side of the element. See MDN. |
marginStart | Responsive<DimensionValue> | — | The margin for the logical start side of the element, depending on layout direction. See MDN. |
marginEnd | Responsive<DimensionValue> | — | The margin for the logical end side of an element, depending on layout direction. See MDN. |
marginX | Responsive<DimensionValue> | — | The margin for both the left and right sides of the element. See MDN. |
marginY | Responsive<DimensionValue> | — | The margin for both the top and bottom sides of the element. See MDN. |
Sizing
| Name | Type | Default | Description |
width | Responsive<DimensionValue> | — | The width of the element. See MDN. |
minWidth | Responsive<DimensionValue> | — | The minimum width of the element. See MDN. |
maxWidth | Responsive<DimensionValue> | — | The maximum width of the element. See MDN. |
height | Responsive<DimensionValue> | — | The height of the element. See MDN. |
minHeight | Responsive<DimensionValue> | — | The minimum height of the element. See MDN. |
maxHeight | Responsive<DimensionValue> | — | The maximum height of the element. See MDN. |
Positioning
| Name | Type | Default | Description |
position | Responsive<'static'
| 'relative'
| 'absolute'
| 'fixed'
| 'sticky'> | — | Specifies how the element is positioned. See MDN. |
top | Responsive<DimensionValue> | — | The top position for the element. See MDN. |
bottom | Responsive<DimensionValue> | — | The bottom position for the element. See MDN. |
left | Responsive<DimensionValue> | — | The left position for the element. See MDN. Consider using start instead for RTL support. |
right | Responsive<DimensionValue> | — | The right position for the element. See MDN. Consider using start instead for RTL support. |
start | Responsive<DimensionValue> | — | The logical start position for the element, depending on layout direction. See MDN. |
end | Responsive<DimensionValue> | — | The logical end position for the element, depending on layout direction. See MDN. |
zIndex | Responsive<number> | — | The stacking order for the element. See MDN. |
isHidden | Responsive<boolean> | — | Hides the element. |
Accessibility
| Name | Type | Default | Description |
id | string | — | The element's unique identifier. See MDN. |
aria-label | string | — | Defines a string value that labels the current element. |
aria-labelledby | string | — | Identifies the element (or elements) that labels the current element. |
aria-describedby | string | — | Identifies the element (or elements) that describes the object. |
aria-details | string | — | Identifies the element (or elements) that provide a detailed, extended description for the object. |
Advanced
| Name | Type | Default | Description |
UNSAFE_className | string | — | Sets the CSS className for the element. Only use as a last resort. Use style props instead. |
UNSAFE_style | CSSProperties | — | Sets inline style for the element. Only use as a last resort. Use style props instead. |
Card props#
| Name | Type | Default | Description |
children | ReactNode | — | Rendered contents of the card. Note that focusable elements are not allowed within the card in a CardView. |
textValue | string | — | A string representation of the cards's contents, used for features like typeahead. |
key | Key | — | A unique key for the card. |
Sizing
| Name | Type | Default | Description |
width | number | — | The raw width of the card's image. |
height | number | — | The raw height of the card's image. |
Accessibility
| Name | Type | Default | Description |
aria-label | string | — | An accessibility label for this card. |
Visual options#
Horizontal Cards#
GridLayout is the only layout that currently supports horizontal cards. To enable horizontal cards, provide cardOrientation="horizontal"
to the CardView and to your GridLayout instance if you are instantiating one.
// Same CardView from GridLayout example...
<CustomLayout
layout={GridLayout}
aria-label="CardView with horizontal cards"
height="400px"
cardOrientation="horizontal"
/>
// Same CardView from GridLayout example...
<CustomLayout
layout={GridLayout}
aria-label="CardView with horizontal cards"
height="400px"
cardOrientation="horizontal"
/>
// Same CardView from GridLayout example...
<CustomLayout
layout={GridLayout}
aria-label="CardView with horizontal cards"
height="400px"
cardOrientation="horizontal"
/>
Quiet#
WaterfallLayout is the only layout that currently supports quiet cards. To enable quiet cards, provide isQuiet
to the CardView.
// Same CardView from GridLayout example...
<CustomLayout
layout={WaterfallLayout}
aria-label="CardView with quiet cards"
isQuiet
/>
// Same CardView from GridLayout example...
<CustomLayout
layout={WaterfallLayout}
aria-label="CardView with quiet cards"
isQuiet
/>
// Same CardView from GridLayout example...
<CustomLayout
layout={WaterfallLayout}
aria-label="CardView with quiet cards"
isQuiet
/>
Empty state#
Use the renderEmptyState prop to customize what the CardView will display if there are no cards provided.
import {Content} from '@react-spectrum/view';
import {IllustratedMessage} from '@react-spectrum/illustratedmessage';
import NotFound from '@spectrum-icons/illustrations/NotFound';
import {Heading} from '@react-spectrum/text';
function renderEmptyState() {
return (
<IllustratedMessage>
<NotFound />
<Heading>No results</Heading>
<Content>No results found</Content>
</IllustratedMessage>
);
}
<CardView
aria-label="Example CardView for empty state"
height="size-3000"
layout={GridLayout}
renderEmptyState={renderEmptyState}
>
{[]}
</CardView>
import {Content} from '@react-spectrum/view';
import {IllustratedMessage} from '@react-spectrum/illustratedmessage';
import NotFound from '@spectrum-icons/illustrations/NotFound';
import {Heading} from '@react-spectrum/text';
function renderEmptyState() {
return (
<IllustratedMessage>
<NotFound />
<Heading>No results</Heading>
<Content>No results found</Content>
</IllustratedMessage>
);
}
<CardView
aria-label="Example CardView for empty state"
height="size-3000"
layout={GridLayout}
renderEmptyState={renderEmptyState}
>
{[]}
</CardView>
import {Content} from '@react-spectrum/view';
import {IllustratedMessage} from '@react-spectrum/illustratedmessage';
import NotFound from '@spectrum-icons/illustrations/NotFound';
import {Heading} from '@react-spectrum/text';
function renderEmptyState() {
return (
<IllustratedMessage>
<NotFound />
<Heading>
No results
</Heading>
<Content>
No results found
</Content>
</IllustratedMessage>
);
}
<CardView
aria-label="Example CardView for empty state"
height="size-3000"
layout={GridLayout}
renderEmptyState={renderEmptyState}
>
{[]}
</CardView>