Beta Preview

CardView

A CardView displays a group of related objects, with support for selection and bulk actions.

layout 
selectionMode 
selectionStyle 
variant 
size 
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})}>
</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'};

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>
  );
}

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.

selectionMode 
selectionStyle 
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';

<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:

selectionMode 
selectionStyle 
disabledBehavior 
disallowEmptySelection 
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>
          );
        }}>
</CardView> <p>Current selection: {selected === 'all' ? 'all' : [...selected].join(', ')}</p> </> ); }

API

<CardView>
  <Card />
  <SkeletonCollection />
</CardView>

CardView

NameTypeDefault
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.
stylesDefault:
Spectrum-defined styles, returned by the style() macro.
renderActionBar(selectedKeys: 'all'Set<Key>) => ReactElementDefault:
Provides the ActionBar to render when cards are selected in the CardView.
disallowTypeAheadbooleanDefault: false
Whether typeahead navigation is disabled.
dragAndDropHooks<NoInfer<T>>Default:
The drag and drop hooks returned by useDragAndDrop used to enable drag and drop behavior for the GridList.
childrenReactNode(item: T) => ReactNodeDefault:
The contents of the collection.
itemsIterable<T>Default:
Item objects in the collection.
loadingStateDefault:
The loading state of the CardView.
onLoadMore() => voidDefault:
Handler that is called when more items should be loaded, e.g. while scrolling near the bottom.
renderEmptyState(props: ) => ReactNodeDefault:
Provides content to display when there are no items in the list.
dependenciesReadonlyArray<any>Default:
Values that should invalidate the item cache when using dynamic collections.
selectionModeDefault:
The type of selection that is allowed in the collection.
selectedKeys'all'Iterable<Key>Default:
The currently selected keys in the collection (controlled).
defaultSelectedKeys'all'Iterable<Key>Default:
The initial selected keys in the collection (uncontrolled).
onSelectionChange(keys: ) => voidDefault:
Handler that is called when the selection changes.
disabledKeysIterable<Key>Default:
The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
disabledBehaviorDefault: "all"
Whether disabledKeys applies to all interactions, or only selection.
disallowEmptySelectionbooleanDefault:
Whether the collection allows empty selection.
shouldSelectOnPressUpbooleanDefault:
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.