alpha

CardView

CardViews display a collection of items in a variety of layouts, providing users with a visual representation of the collection's contents.

installyarn add @react-spectrum/card
version3.0.0-alpha.1
usageimport {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="380px"
  width="100%"
  layout={GridLayout}
  aria-label="Static CardView example"
>
  <Card key="1" width={1024} height={683} textValue="Shaggy Horse">
    <Image src="https://lfdanlu.github.io/docPhotos/photos/DSC00527.jpeg" />
    <Heading>Shaggy Horse</Heading>
    <Text slot="detail">JPEG</Text>
    <Content>Size: 366 KB</Content>
  </Card>
  <Card key="2" width={1024} height={683} textValue="Lighthouse">
    <Image src="https://lfdanlu.github.io/docPhotos/photos/DSC00591.jpeg" />
    <Heading>Lighthouse</Heading>
    <Text slot="detail">JPEG</Text>
    <Content>Size: 182 KB</Content>
  </Card>
  <Card key="3" width={1024} height={683} textValue="Waterfall">
    <Image src="https://lfdanlu.github.io/docPhotos/photos/DSC00827.jpeg" />
    <Heading>Waterfall</Heading>
    <Text slot="detail">JPEG</Text>
    <Content>Size: 182 KB</Content>
  </Card>
</CardView>
import {Content} from '@react-spectrum/view';
import {Heading, Text} from '@react-spectrum/text';
import {Image} from '@react-spectrum/image';

<CardView
  height="380px"
  width="100%"
  layout={GridLayout}
  aria-label="Static CardView example"
>
  <Card
    key="1"
    width={1024}
    height={683}
    textValue="Shaggy Horse"
  >
    <Image src="https://lfdanlu.github.io/docPhotos/photos/DSC00527.jpeg" />
    <Heading>Shaggy Horse</Heading>
    <Text slot="detail">JPEG</Text>
    <Content>Size: 366 KB</Content>
  </Card>
  <Card
    key="2"
    width={1024}
    height={683}
    textValue="Lighthouse"
  >
    <Image src="https://lfdanlu.github.io/docPhotos/photos/DSC00591.jpeg" />
    <Heading>Lighthouse</Heading>
    <Text slot="detail">JPEG</Text>
    <Content>Size: 182 KB</Content>
  </Card>
  <Card
    key="3"
    width={1024}
    height={683}
    textValue="Waterfall"
  >
    <Image src="https://lfdanlu.github.io/docPhotos/photos/DSC00827.jpeg" />
    <Heading>Waterfall</Heading>
    <Text slot="detail">JPEG</Text>
    <Content>Size: 182 KB</Content>
  </Card>
</CardView>
import {Content} from '@react-spectrum/view';
import {
  Heading,
  Text
} from '@react-spectrum/text';
import {Image} from '@react-spectrum/image';

<CardView
  height="380px"
  width="100%"
  layout={GridLayout}
  aria-label="Static CardView example"
>
  <Card
    key="1"
    width={1024}
    height={683}
    textValue="Shaggy Horse"
  >
    <Image src="https://lfdanlu.github.io/docPhotos/photos/DSC00527.jpeg" />
    <Heading>
      Shaggy Horse
    </Heading>
    <Text slot="detail">
      JPEG
    </Text>
    <Content>
      Size: 366 KB
    </Content>
  </Card>
  <Card
    key="2"
    width={1024}
    height={683}
    textValue="Lighthouse"
  >
    <Image src="https://lfdanlu.github.io/docPhotos/photos/DSC00591.jpeg" />
    <Heading>
      Lighthouse
    </Heading>
    <Text slot="detail">
      JPEG
    </Text>
    <Content>
      Size: 182 KB
    </Content>
  </Card>
  <Card
    key="3"
    width={1024}
    height={683}
    textValue="Waterfall"
  >
    <Image src="https://lfdanlu.github.io/docPhotos/photos/DSC00827.jpeg" />
    <Heading>
      Waterfall
    </Heading>
    <Text slot="detail">
      JPEG
    </Text>
    <Content>
      Size: 182 KB
    </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 comes from an external data source such as an API call, or update over time. Providing the data in this way allows CardView to automatically cache the rendering of each item, which dramatically improves performance.

Each card has a unique key defined by the data. In the example below, the key of each card is implicitly defined by the id property of the item object. See collections to learn more keys in dynamic collections.

let items = [
  {
    id: 1,
    name: 'Shaggy Horse',
    size: '366 KB',
    width: 1024,
    height: 683,
    src: 'https://lfdanlu.github.io/docPhotos/photos/DSC00527.jpeg'
  },
  {
    id: 2,
    name: 'Lighthouse',
    size: '182 KB',
    width: 1024,
    height: 683,
    src: 'https://lfdanlu.github.io/docPhotos/photos/DSC00591.jpeg'
  },
  {
    id: 3,
    name: 'Waterfall',
    size: '182 KB',
    width: 1024,
    height: 683,
    src: 'https://lfdanlu.github.io/docPhotos/photos/DSC00827.jpeg'
  }
];

<CardView
  items={items}
  height="380px"
  width="100%"
  layout={GridLayout}
  aria-label="Dynamic CardView example"
>
  {(item) => (
    <Card textValue={item.name} width={item.width} height={item.height}>
      <Image src={item.src} />
      <Heading>
        {item.name}
      </Heading>
      <Text slot="detail">
        JPEG
      </Text>
      <Content>Size: {item.size}</Content>
    </Card>
  )}
</CardView>
let items = [
  {
    id: 1,
    name: 'Shaggy Horse',
    size: '366 KB',
    width: 1024,
    height: 683,
    src:
      'https://lfdanlu.github.io/docPhotos/photos/DSC00527.jpeg'
  },
  {
    id: 2,
    name: 'Lighthouse',
    size: '182 KB',
    width: 1024,
    height: 683,
    src:
      'https://lfdanlu.github.io/docPhotos/photos/DSC00591.jpeg'
  },
  {
    id: 3,
    name: 'Waterfall',
    size: '182 KB',
    width: 1024,
    height: 683,
    src:
      'https://lfdanlu.github.io/docPhotos/photos/DSC00827.jpeg'
  }
];

<CardView
  items={items}
  height="380px"
  width="100%"
  layout={GridLayout}
  aria-label="Dynamic CardView example"
>
  {(item) => (
    <Card
      textValue={item.name}
      width={item.width}
      height={item.height}
    >
      <Image src={item.src} />
      <Heading>{item.name}</Heading>
      <Text slot="detail">JPEG</Text>
      <Content>Size: {item.size}</Content>
    </Card>
  )}
</CardView>
let items = [
  {
    id: 1,
    name: 'Shaggy Horse',
    size: '366 KB',
    width: 1024,
    height: 683,
    src:
      'https://lfdanlu.github.io/docPhotos/photos/DSC00527.jpeg'
  },
  {
    id: 2,
    name: 'Lighthouse',
    size: '182 KB',
    width: 1024,
    height: 683,
    src:
      'https://lfdanlu.github.io/docPhotos/photos/DSC00591.jpeg'
  },
  {
    id: 3,
    name: 'Waterfall',
    size: '182 KB',
    width: 1024,
    height: 683,
    src:
      'https://lfdanlu.github.io/docPhotos/photos/DSC00827.jpeg'
  }
];

<CardView
  items={items}
  height="380px"
  width="100%"
  layout={GridLayout}
  aria-label="Dynamic CardView example"
>
  {(item) => (
    <Card
      textValue={item
        .name}
      width={item
        .width}
      height={item
        .height}
    >
      <Image
        src={item.src}
      />
      <Heading>
      {item.name}
      </Heading>
      <Text slot="detail">
      JPEG</Text>
      <Content>
        Size:{' '}
        {item.size}
      </Content>
    </Card>
  )}
</CardView>

Cards#

Title, detailOptional metadata included here.Card title DETAILFramePreview

A standard Card consists of a preview image and a title, and allows for an 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 an 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 card, 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 the layout class with its default options. Alternatively, you can modify the layout's various options (e.g. margins, item sizes, etc) by constructing your own instance of the class and providing that to the CardView instead.

The example below demonstrates the differences between each of the layouts as well as showing how you could instantiate a layout instance.

import {ActionGroup, Item} from '@react-spectrum/actiongroup';
import {Flex} from '@react-spectrum/layout';
import {useProvider} from '@react-spectrum/provider';

function LayoutDemo() {
  let items = [
    {
      width: 1001,
      height: 381,
      src: 'https://i.imgur.com/Z7AzH2c.jpg',
      title: 'Temple Roof',
      ext: 'JPG'
    }
    // ...
  ];

  let { scale } = useProvider();
  let gridLayout = React.useMemo(() => new GridLayout({ scale }), [scale]);
  let galleryLayout = React.useMemo(() => new GalleryLayout({ scale }), [
    scale
  ]);
  let waterfallLayout = React.useMemo(() => new WaterfallLayout({ scale }), [
    scale
  ]);
  let [layout, setLayout] = React.useState(gridLayout);
  let onAction = (key) => {
    if (key === 'grid') {
      setLayout(gridLayout);
    } else if (key === 'gallery') {
      setLayout(galleryLayout);
    } else {
      setLayout(waterfallLayout);
    }
  };

  return (
    <Flex direction="column" height="600px" width="100%">
      <ActionGroup
        defaultSelectedKeys={['grid']}
        selectionMode="single"
        disallowEmptySelection
        onAction={onAction}
        aria-label="CardView layout selection"
      >
        <Item key="grid">Grid layout</Item>
        <Item key="gallery">Gallery layout</Item>
        <Item key="waterfall">Waterfall layout</Item>
      </ActionGroup>
      <CardView
        items={items}
        layout={layout}
        width="100%"
        height="100%"
        aria-label="CardView with changeable layout"
      >
        {(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>
    </Flex>
  );
}
import {
  ActionGroup,
  Item
} from '@react-spectrum/actiongroup';
import {Flex} from '@react-spectrum/layout';
import {useProvider} from '@react-spectrum/provider';

function LayoutDemo() {
  let items = [
    {
      width: 1001,
      height: 381,
      src: 'https://i.imgur.com/Z7AzH2c.jpg',
      title: 'Temple Roof',
      ext: 'JPG'
    }
    // ...
  ];

  let { scale } = useProvider();
  let gridLayout = React.useMemo(
    () => new GridLayout({ scale }),
    [scale]
  );
  let galleryLayout = React.useMemo(
    () => new GalleryLayout({ scale }),
    [scale]
  );
  let waterfallLayout = React.useMemo(
    () => new WaterfallLayout({ scale }),
    [scale]
  );
  let [layout, setLayout] = React.useState(gridLayout);
  let onAction = (key) => {
    if (key === 'grid') {
      setLayout(gridLayout);
    } else if (key === 'gallery') {
      setLayout(galleryLayout);
    } else {
      setLayout(waterfallLayout);
    }
  };

  return (
    <Flex direction="column" height="600px" width="100%">
      <ActionGroup
        defaultSelectedKeys={['grid']}
        selectionMode="single"
        disallowEmptySelection
        onAction={onAction}
        aria-label="CardView layout selection"
      >
        <Item key="grid">
          Grid layout
        </Item>
        <Item key="gallery">
          Gallery layout
        </Item>
        <Item key="waterfall">Waterfall layout</Item>
      </ActionGroup>
      <CardView
        items={items}
        layout={layout}
        width="100%"
        height="100%"
        aria-label="CardView with changeable layout"
      >
        {(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>
    </Flex>
  );
}
import {
  ActionGroup,
  Item
} from '@react-spectrum/actiongroup';
import {Flex} from '@react-spectrum/layout';
import {useProvider} from '@react-spectrum/provider';

function LayoutDemo() {
  let items = [
    {
      width: 1001,
      height: 381,
      src:
        'https://i.imgur.com/Z7AzH2c.jpg',
      title:
        'Temple Roof',
      ext: 'JPG'
    }
    // ...
  ];

  let { scale } =
    useProvider();
  let gridLayout = React
    .useMemo(
      () =>
        new GridLayout({
          scale
        }),
      [scale]
    );
  let galleryLayout =
    React.useMemo(
      () =>
        new GalleryLayout(
          { scale }
        ),
      [scale]
    );
  let waterfallLayout =
    React.useMemo(
      () =>
        new WaterfallLayout(
          { scale }
        ),
      [scale]
    );
  let [
    layout,
    setLayout
  ] = React.useState(
    gridLayout
  );
  let onAction = (
    key
  ) => {
    if (key === 'grid') {
      setLayout(
        gridLayout
      );
    } else if (
      key === 'gallery'
    ) {
      setLayout(
        galleryLayout
      );
    } else {
      setLayout(
        waterfallLayout
      );
    }
  };

  return (
    <Flex
      direction="column"
      height="600px"
      width="100%"
    >
      <ActionGroup
        defaultSelectedKeys={[
          'grid'
        ]}
        selectionMode="single"
        disallowEmptySelection
        onAction={onAction}
        aria-label="CardView layout selection"
      >
        <Item key="grid">
          Grid layout
        </Item>
        <Item key="gallery">
          Gallery layout
        </Item>
        <Item key="waterfall">
          Waterfall
          layout
        </Item>
      </ActionGroup>
      <CardView
        items={items}
        layout={layout}
        width="100%"
        height="100%"
        aria-label="CardView with changeable layout"
      >
        {(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>
    </Flex>
  );
}

GridLayout#

As can be seen above, the GridLayout renders each card with a uniform height and width regardless of the image size. The customizable options specific to the GridLayout are as follows:

  • maxItemSize
  • minSpace
  • maxColumns
  • cardOrientation

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:

  • idealRowHeight
  • itemSpacing
  • threshold

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 specific to the WaterfallLayout are as follows:

  • maxItemSize
  • minSpace
  • maxColumns

Shared layout options#

In addition to the layout specific options mentioned above, each of the layouts possess following shared options:

  • minItemSize
  • itemPadding
  • scale
  • margin

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

async function getImageData(page) {
  // Mock function for getting paged data...
}

function AsyncLoadingCardView() {
  let list = useAsyncList({
    async load({ cursor }) {
      let res = await getImageData(cursor || 1);
      return {
        items: res.result,
        cursor: res.cursor
      };
    }
  });

  return (
    <CardView
      items={list.items}
      onLoadMore={list.loadMore}
      loadingState={list.loadingState}
      layout={GridLayout}
      width="100%"
      height="500px"
      aria-label="Async loading CardView"
    >
      {(item) => (
        <Card key={item.title} textValue={item.title}>
          <Image src={item.src} />
          <Heading>{item.name}</Heading>
          <Text slot="detail">{item.ext}</Text>
          <Content>Size: {item.size}</Content>
        </Card>
      )}
    </CardView>
  );
}

<AsyncLoadingCardView />
import {useAsyncList} from '@react-stately/data';

async function getImageData(page) {
  // Mock function for getting paged data...
}

function AsyncLoadingCardView() {
  let list = useAsyncList({
    async load({ cursor }) {
      let res = await getImageData(cursor || 1);
      return {
        items: res.result,
        cursor: res.cursor
      };
    }
  });

  return (
    <CardView
      items={list.items}
      onLoadMore={list.loadMore}
      loadingState={list.loadingState}
      layout={GridLayout}
      width="100%"
      height="500px"
      aria-label="Async loading CardView"
    >
      {(item) => (
        <Card key={item.title} textValue={item.title}>
          <Image src={item.src} />
          <Heading>{item.name}</Heading>
          <Text slot="detail">{item.ext}</Text>
          <Content>Size: {item.size}</Content>
        </Card>
      )}
    </CardView>
  );
}

<AsyncLoadingCardView />
import {useAsyncList} from '@react-stately/data';

async function getImageData(
  page
) {
  // Mock function for getting paged data...
}

function AsyncLoadingCardView() {
  let list =
    useAsyncList({
      async load(
        { cursor }
      ) {
        let res =
          await getImageData(
            cursor || 1
          );
        return {
          items:
            res.result,
          cursor:
            res.cursor
        };
      }
    });

  return (
    <CardView
      items={list.items}
      onLoadMore={list
        .loadMore}
      loadingState={list
        .loadingState}
      layout={GridLayout}
      width="100%"
      height="500px"
      aria-label="Async loading CardView"
    >
      {(item) => (
        <Card
          key={item
            .title}
          textValue={item
            .title}
        >
          <Image
            src={item
              .src}
          />
          <Heading>
          {item.name}
          </Heading>
          <Text slot="detail">
          {item.ext}
          </Text>
          <Content>Size:
          {' '}
          {item.size}
          </Content>
        </Card>
      )}
    </CardView>
  );
}

<AsyncLoadingCardView />

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: 'Icy Peak',
    size: '206 KB',
    width: 670,
    height: 1005,
    src: 'https://lfdanlu.github.io/docPhotos/photos/DSC00888.jpeg'
  },
  {
    id: 2,
    name: 'Grassy Plain',
    size: '233 KB',
    width: 1024,
    height: 683,
    src: 'https://lfdanlu.github.io/docPhotos/photos/DSC01051.jpeg'
  }
  // ...
];

<CardView
  items={items}
  height="500px"
  width="100%"
  layout={GridLayout}
  aria-label="Default selection CardView 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">
        JPEG
      </Text>
      <Content>Size: {item.size}</Content>
    </Card>
  )}
</CardView>
let items = [
  {
    id: 1,
    name: 'Icy Peak',
    size: '206 KB',
    width: 670,
    height: 1005,
    src:
      'https://lfdanlu.github.io/docPhotos/photos/DSC00888.jpeg'
  },
  {
    id: 2,
    name: 'Grassy Plain',
    size: '233 KB',
    width: 1024,
    height: 683,
    src:
      'https://lfdanlu.github.io/docPhotos/photos/DSC01051.jpeg'
  }
  // ...
];

<CardView
  items={items}
  height="500px"
  width="100%"
  layout={GridLayout}
  aria-label="Default selection CardView 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">JPEG</Text>
      <Content>Size: {item.size}</Content>
    </Card>
  )}
</CardView>
let items = [
  {
    id: 1,
    name: 'Icy Peak',
    size: '206 KB',
    width: 670,
    height: 1005,
    src:
      'https://lfdanlu.github.io/docPhotos/photos/DSC00888.jpeg'
  },
  {
    id: 2,
    name: 'Grassy Plain',
    size: '233 KB',
    width: 1024,
    height: 683,
    src:
      'https://lfdanlu.github.io/docPhotos/photos/DSC01051.jpeg'
  }
  // ...
];

<CardView
  items={items}
  height="500px"
  width="100%"
  layout={GridLayout}
  aria-label="Default selection CardView 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">
      JPEG</Text>
      <Content>
        Size:{' '}
        {item.size}
      </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 SelectionCardView(props) {
  let items = [
    // Same items as in the previous...
  ];
  let [selectedKeys, setSelectedKeys] = React.useState(new Set([2]));

  return (
    <CardView
      items={items}
      height="500px"
      width="100%"
      layout={GridLayout}
      aria-label="Controlled selection CardView 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">
            JPEG
          </Text>
          <Content>Size: {item.size}</Content>
        </Card>
      )}
    </CardView>
  );
}
function SelectionCardView(props) {
  let items = [
    // Same items as in the previous...
  ];
  let [selectedKeys, setSelectedKeys] = React.useState(
    new Set([2])
  );

  return (
    <CardView
      items={items}
      height="500px"
      width="100%"
      layout={GridLayout}
      aria-label="Controlled selection CardView 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">JPEG</Text>
          <Content>Size: {item.size}</Content>
        </Card>
      )}
    </CardView>
  );
}
function SelectionCardView(
  props
) {
  let items = [
    // Same items as in the previous...
  ];
  let [
    selectedKeys,
    setSelectedKeys
  ] = React.useState(
    new Set([2])
  );

  return (
    <CardView
      items={items}
      height="500px"
      width="100%"
      layout={GridLayout}
      aria-label="Controlled selection CardView 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">
          JPEG</Text>
          <Content>
            Size:{' '}
            {item.size}
          </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
<SelectionCardView selectionMode="single" />
// Using the same CardView as above
<SelectionCardView selectionMode="single" />
// Using the same CardView as above
<SelectionCardView 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
<SelectionCardView disallowEmptySelection />
// Using the same CardView as above
<SelectionCardView disallowEmptySelection />
// Using the same CardView as above
<SelectionCardView
  disallowEmptySelection
/>

Disabled cards#

You can disable specific cards 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.

function Folder(props) {
  // Custom Folder asset illustration implementation...
}

function File(props) {
  // Custom File asset illustration implementation...
}

function CardViewDisabledKeys() {
  let items = [
    {
      id: 1,
      name: 'Vacation ideas',
      type: 'file',
      size: '100 KB',
      format: 'DOCX'
    },
    { id: 2, name: 'Saved Photos 2022', type: 'folder', size: '1.3 GB' }
    // ...
  ];

  return (
    <CardView
      items={items}
      height="500px"
      width="100%"
      layout={GridLayout}
      aria-label="Disabled selection CardView example"
      selectionMode="multiple"
      disabledKeys={[2, 4]}
    >
      {(item) => (
        <Card textValue={item.name}>
          {item.type === 'folder'
            ? <Folder alt="folder" slot="illustration" />
            : <File alt="file" slot="illustration" />}
          <Heading>{item.name}</Heading>
          {item.format && <Text slot="detail">{item.format}</Text>}
          <Content>Size: {item.size}</Content>
        </Card>
      )}
    </CardView>
  );
}

<CardViewDisabledKeys />
function Folder(props) {
  // Custom Folder asset illustration implementation...
}

function File(props) {
  // Custom File asset illustration implementation...
}

function CardViewDisabledKeys() {
  let items = [
    {
      id: 1,
      name: 'Vacation ideas',
      type: 'file',
      size: '100 KB',
      format: 'DOCX'
    },
    {
      id: 2,
      name: 'Saved Photos 2022',
      type: 'folder',
      size: '1.3 GB'
    }
    // ...
  ];

  return (
    <CardView
      items={items}
      height="500px"
      width="100%"
      layout={GridLayout}
      aria-label="Disabled selection CardView example"
      selectionMode="multiple"
      disabledKeys={[2, 4]}
    >
      {(item) => (
        <Card textValue={item.name}>
          {item.type === 'folder'
            ? <Folder alt="folder" slot="illustration" />
            : <File alt="file" slot="illustration" />}
          <Heading>{item.name}</Heading>
          {item.format &&
            <Text slot="detail">{item.format}</Text>}
          <Content>Size: {item.size}</Content>
        </Card>
      )}
    </CardView>
  );
}

<CardViewDisabledKeys />
function Folder(props) {
  // Custom Folder asset illustration implementation...
}

function File(props) {
  // Custom File asset illustration implementation...
}

function CardViewDisabledKeys() {
  let items = [
    {
      id: 1,
      name:
        'Vacation ideas',
      type: 'file',
      size: '100 KB',
      format: 'DOCX'
    },
    {
      id: 2,
      name:
        'Saved Photos 2022',
      type: 'folder',
      size: '1.3 GB'
    }
    // ...
  ];

  return (
    <CardView
      items={items}
      height="500px"
      width="100%"
      layout={GridLayout}
      aria-label="Disabled selection CardView example"
      selectionMode="multiple"
      disabledKeys={[
        2,
        4
      ]}
    >
      {(item) => (
        <Card
          textValue={item
            .name}
        >
          {item.type ===
              'folder'
            ? (
              <Folder
                alt="folder"
                slot="illustration"
              />
            )
            : (
              <File
                alt="file"
                slot="illustration"
              />
            )}
          <Heading>
            {item.name}
          </Heading>
          {item.format &&
            (
              <Text slot="detail">
                {item
                  .format}
              </Text>
            )}
          <Content>
            Size:{' '}
            {item.size}
          </Content>
        </Card>
      )}
    </CardView>
  );
}

<CardViewDisabledKeys />

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 selection examples...
];

function CardViewActions() {
  let [selectedKeys, setSelectedKeys] = React.useState(new Set([2]));

  return (
    <ActionBarContainer height="500px">
      <CardView
        items={items}
        selectedKeys={selectedKeys}
        onSelectionChange={setSelectedKeys}
        height="500px"
        width="100%"
        layout={GridLayout}
        aria-label="CardView with ActionBar example"
        selectionMode="multiple"
      >
        {(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
        isEmphasized
        selectedItemCount={selectedKeys.size}
        onClearSelection={() => {
          setSelectedKeys(new Set());
        }}
        onAction={(key) =>
          alert(`Performing ${key} action on ${selectedKeys.size} cards.`)}
      >
        <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 selection examples...
];

function CardViewActions() {
  let [selectedKeys, setSelectedKeys] = React.useState(
    new Set([2])
  );

  return (
    <ActionBarContainer height="500px">
      <CardView
        items={items}
        selectedKeys={selectedKeys}
        onSelectionChange={setSelectedKeys}
        height="500px"
        width="100%"
        layout={GridLayout}
        aria-label="CardView with ActionBar example"
        selectionMode="multiple"
      >
        {(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
        isEmphasized
        selectedItemCount={selectedKeys.size}
        onClearSelection={() => {
          setSelectedKeys(new Set());
        }}
        onAction={(key) =>
          alert(
            `Performing ${key} action on ${selectedKeys.size} cards.`
          )}
      >
        <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 selection examples...
];

function CardViewActions() {
  let [
    selectedKeys,
    setSelectedKeys
  ] = React.useState(
    new Set([2])
  );

  return (
    <ActionBarContainer height="500px">
      <CardView
        items={items}
        selectedKeys={selectedKeys}
        onSelectionChange={setSelectedKeys}
        height="500px"
        width="100%"
        layout={GridLayout}
        aria-label="CardView with ActionBar example"
        selectionMode="multiple"
      >
        {(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
        isEmphasized
        selectedItemCount={selectedKeys
          .size}
        onClearSelection={() => {
          setSelectedKeys(
            new Set()
          );
        }}
        onAction={(key) =>
          alert(
            `Performing ${key} action on ${selectedKeys.size} cards.`
          )}
      >
        <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#

NameTypeDefaultDescription
layoutCardViewLayoutConstructor<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.
childrenCollectionChildren<T>The contents of the collection.
cardOrientationOrientationThe orientation of the cards within the CardView.
isQuietbooleanWhether 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.ElementSets what the CardView should render when there is no content to display.
loadingStateLoadingStateThe current loading state of the CardView.
itemsIterable<T>Item objects in the collection.
disabledKeysIterable<Key>The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
selectionModeSelectionModeThe type of selection that is allowed in the collection.
disallowEmptySelectionbooleanWhether 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
NameTypeDefaultDescription
onSelectionChange( (keys: Selection )) => anyHandler that is called when the selection changes.
onLoadMore() => anyHandler that is called when more items should be loaded, e.g. while scrolling near the bottom.
Layout
NameTypeDefaultDescription
flexResponsive<stringnumberboolean>When used in a flex layout, specifies how the element will grow or shrink to fit the space available. See MDN.
flexGrowResponsive<number>When used in a flex layout, specifies how the element will grow to fit the space available. See MDN.
flexShrinkResponsive<number>When used in a flex layout, specifies how the element will shrink to fit the space available. See MDN.
flexBasisResponsive<numberstring>When used in a flex layout, specifies the initial main size of the element. See MDN.
alignSelfResponsive<'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.
justifySelfResponsive<'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.
orderResponsive<number>The layout order for the element within a flex or grid container. See MDN.
gridAreaResponsive<string>When used in a grid layout, specifies the named grid area that the element should be placed in within the grid. See MDN.
gridColumnResponsive<string>When used in a grid layout, specifies the column the element should be placed in within the grid. See MDN.
gridRowResponsive<string>When used in a grid layout, specifies the row the element should be placed in within the grid. See MDN.
gridColumnStartResponsive<string>When used in a grid layout, specifies the starting column to span within the grid. See MDN.
gridColumnEndResponsive<string>When used in a grid layout, specifies the ending column to span within the grid. See MDN.
gridRowStartResponsive<string>When used in a grid layout, specifies the starting row to span within the grid. See MDN.
gridRowEndResponsive<string>When used in a grid layout, specifies the ending row to span within the grid. See MDN.
Spacing
NameTypeDefaultDescription
marginResponsive<DimensionValue>The margin for all four sides of the element. See MDN.
marginTopResponsive<DimensionValue>The margin for the top side of the element. See MDN.
marginBottomResponsive<DimensionValue>The margin for the bottom side of the element. See MDN.
marginStartResponsive<DimensionValue>The margin for the logical start side of the element, depending on layout direction. See MDN.
marginEndResponsive<DimensionValue>The margin for the logical end side of an element, depending on layout direction. See MDN.
marginXResponsive<DimensionValue>The margin for both the left and right sides of the element. See MDN.
marginYResponsive<DimensionValue>The margin for both the top and bottom sides of the element. See MDN.
Sizing
NameTypeDefaultDescription
widthResponsive<DimensionValue>The width of the element. See MDN.
minWidthResponsive<DimensionValue>The minimum width of the element. See MDN.
maxWidthResponsive<DimensionValue>The maximum width of the element. See MDN.
heightResponsive<DimensionValue>The height of the element. See MDN.
minHeightResponsive<DimensionValue>The minimum height of the element. See MDN.
maxHeightResponsive<DimensionValue>The maximum height of the element. See MDN.
Positioning
NameTypeDefaultDescription
positionResponsive<'static''relative''absolute''fixed''sticky'>Specifies how the element is positioned. See MDN.
topResponsive<DimensionValue>The top position for the element. See MDN.
bottomResponsive<DimensionValue>The bottom position for the element. See MDN.
leftResponsive<DimensionValue>The left position for the element. See MDN. Consider using start instead for RTL support.
rightResponsive<DimensionValue>The right position for the element. See MDN. Consider using start instead for RTL support.
startResponsive<DimensionValue>The logical start position for the element, depending on layout direction. See MDN.
endResponsive<DimensionValue>The logical end position for the element, depending on layout direction. See MDN.
zIndexResponsive<number>The stacking order for the element. See MDN.
isHiddenResponsive<boolean>Hides the element.
Accessibility
NameTypeDefaultDescription
idstringThe element's unique identifier. See MDN.
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.
Advanced
NameTypeDefaultDescription
UNSAFE_classNamestringSets the CSS className for the element. Only use as a last resort. Use style props instead.
UNSAFE_styleCSSPropertiesSets inline style for the element. Only use as a last resort. Use style props instead.

Card props#

NameTypeDefaultDescription
childrenReactNodeRendered contents of the card. Note that focusable elements are not allowed within the card in a CardView.
textValuestringA string representation of the cards's contents, used for features like typeahead.
keyKeyA unique key for the card.
Sizing
NameTypeDefaultDescription
widthnumberThe raw width of the card's image.
heightnumberThe raw height of the card's image.
Accessibility
NameTypeDefaultDescription
aria-labelstringAn 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 controlled selection example...
<SelectionCardView
  layout={GridLayout}
  aria-label="CardView with horizontal cards"
  height="300px"
  cardOrientation="horizontal"
/>
// Same CardView from controlled selection example...
<SelectionCardView
  layout={GridLayout}
  aria-label="CardView with horizontal cards"
  height="300px"
  cardOrientation="horizontal"
/>
// Same CardView from controlled selection example...
<SelectionCardView
  layout={GridLayout}
  aria-label="CardView with horizontal cards"
  height="300px"
  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 controlled selection example...
<SelectionCardView
  layout={WaterfallLayout}
  aria-label="CardView with quiet cards"
  isQuiet
/>
// Same CardView from controlled selection example...
<SelectionCardView
  layout={WaterfallLayout}
  aria-label="CardView with quiet cards"
  isQuiet
/>
// Same CardView from controlled selection example...
<SelectionCardView
  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>