import { ContentCard } from '@coinbase/cds-web/cards/ContentCard'
ContentCard is a flexible, composable card component built with ContentCardHeader, ContentCardBody, and ContentCardFooter sub-components. It can display various content layouts including text, media, and interactive elements.
Basic Examples
ContentCard uses sub-components for flexible layout. Combine ContentCardHeader, ContentCardBody, and ContentCardFooter to create your card structure.
function Example() { return ( <VStack gap={2} maxWidth={375}> <ContentCard> <ContentCardHeader thumbnail={assets.eth.imageUrl} title="CoinDesk" subtitle="News" actions={ <HStack gap={0}> <IconButton transparent accessibilityLabel="favorite" name="star" variant="secondary" /> <IconButton transparent accessibilityLabel="More options" name="more" variant="secondary" /> </HStack> } /> <ContentCardBody title="Ethereum Network Shatters Records With Hashrate Climbing to 464 EH/s" description="This is a description of the record-breaking hashrate milestone." label={ <HStack alignItems="flex-end" flexWrap="wrap" gap={0.5}> <Text as="p" color="fgMuted" font="label2" numberOfLines={1}> $3,081.01 </Text> <Text as="p" color="fgPositive" font="label2"> ↗ 6.37% </Text> </HStack> } /> <ContentCardFooter> <RemoteImageGroup shape="circle" size={32}> <RemoteImage src={assets.eth.imageUrl} /> <RemoteImage src={assets.btc.imageUrl} /> </RemoteImageGroup> <Button compact variant="secondary"> Share </Button> </ContentCardFooter> </ContentCard> </VStack> ); }
Media Placement
Use the mediaPlacement prop on ContentCardBody to control where media is positioned relative to the content.
function Example() { const exampleMedia = ( <RemoteImage alt="Ethereum background" aria-hidden="true" src={ethBackground} style={{ objectFit: 'cover', borderRadius: '24px' }} width="100%" /> ); return ( <VStack gap={2} maxWidth={375}> <Text as="h3" font="headline"> mediaPlacement: top (default) </Text> <ContentCard> <ContentCardHeader thumbnail={assets.eth.imageUrl} title="CoinDesk" subtitle="News" /> <ContentCardBody title="Media at top" description="The media appears above the text content." media={exampleMedia} mediaPlacement="top" /> </ContentCard> <Text as="h3" font="headline"> mediaPlacement: bottom </Text> <ContentCard> <ContentCardHeader thumbnail={assets.eth.imageUrl} title="CoinDesk" subtitle="News" /> <ContentCardBody title="Media at bottom" description="The media appears below the text content." media={exampleMedia} mediaPlacement="bottom" /> </ContentCard> <Text as="h3" font="headline"> mediaPlacement: end </Text> <ContentCard> <ContentCardHeader thumbnail={assets.eth.imageUrl} title="CoinDesk" subtitle="News" /> <ContentCardBody title="Media at end" description="The media appears to the right of the text content." media={exampleMedia} mediaPlacement="end" /> </ContentCard> <Text as="h3" font="headline"> mediaPlacement: start </Text> <ContentCard> <ContentCardHeader thumbnail={assets.eth.imageUrl} title="CoinDesk" subtitle="News" /> <ContentCardBody title="Media at start" description="The media appears to the left of the text content." media={exampleMedia} mediaPlacement="start" /> </ContentCard> </VStack> ); }
With Background
Apply a background color to the card using the background prop. When using a background, consider using variant="tertiary" on buttons.
function Example() { const exampleMedia = ( <RemoteImage alt="Ethereum background" aria-hidden="true" src={ethBackground} style={{ objectFit: 'cover', borderRadius: '24px' }} width="100%" /> ); return ( <VStack gap={2} maxWidth={375}> <ContentCard background="bgAlternate"> <ContentCardHeader thumbnail={assets.eth.imageUrl} title="CoinDesk" subtitle="News" /> <ContentCardBody title="Card with Background" description="This card has an alternate background color." media={exampleMedia} /> <ContentCardFooter> <RemoteImageGroup shape="circle" size={32}> <RemoteImage src={assets.eth.imageUrl} /> <RemoteImage src={assets.btc.imageUrl} /> </RemoteImageGroup> <Button compact variant="tertiary"> Share </Button> </ContentCardFooter> </ContentCard> <ContentCard background="bgAlternate"> <ContentCardHeader thumbnail={assets.eth.imageUrl} title="CoinDesk" subtitle="News" /> <ContentCardBody title="No Media with Background" description="This card has no media element." /> <ContentCardFooter gap={4} justifyContent="space-between" paddingTop={0.5}> <IconCounterButton count={99} icon="heart" /> <IconCounterButton count={4200} icon="comment" /> <IconCounterButton count={32} icon="arrowsHorizontal" /> </ContentCardFooter> </ContentCard> </VStack> ); }
Pressable Cards
ContentCard does not have a built-in pressable mode. To make the entire card pressable, wrap it in a Pressable component with background, borderRadius, and optionally width props.
Important: When wrapping ContentCard in Pressable, avoid placing additional interactive elements (buttons, links, etc.) inside the card. Nested interactive elements create accessibility issues. If you need actions inside the card, do not wrap the card in Pressable.
function Example() { const exampleMedia = ( <RemoteImage alt="Ethereum background" aria-hidden="true" src={ethBackground} style={{ objectFit: 'cover', borderRadius: '24px' }} width="100%" /> ); return ( <VStack gap={2} maxWidth={375}> <Text as="h3" font="headline"> Pressable card </Text> <Text as="p" color="fgMuted" font="body"> Note: No interactive elements (buttons, links) inside - the entire card is the action. </Text> <Pressable background="bg" borderRadius={500} onClick={() => alert('Card pressed!')} width="fit-content" > <ContentCard> <ContentCardHeader subtitle="News" thumbnail={assets.eth.imageUrl} title="CoinDesk" /> <ContentCardBody title="Clickable Card" description="Click anywhere on this card to trigger an action." media={exampleMedia} /> <ContentCardFooter alignItems="center"> <RemoteImageGroup shape="circle" size={32}> <RemoteImage src={assets.eth.imageUrl} /> <RemoteImage src={assets.btc.imageUrl} /> </RemoteImageGroup> <Text color="fgMuted" font="label2"> 2 assets </Text> </ContentCardFooter> </ContentCard> </Pressable> <Text as="h3" font="headline"> Pressable card with background </Text> <Pressable background="bgAlternate" borderRadius={500} onClick={() => alert('Card pressed!')} width="fit-content" > <ContentCard> <ContentCardHeader subtitle="News" thumbnail={assets.eth.imageUrl} title="CoinDesk" /> <ContentCardBody title="Clickable Card with Background" description="This pressable card has a background color." media={exampleMedia} /> <ContentCardFooter alignItems="center"> <RemoteImageGroup shape="circle" size={32}> <RemoteImage src={assets.eth.imageUrl} /> <RemoteImage src={assets.btc.imageUrl} /> </RemoteImageGroup> <Text color="fgMuted" font="label2"> 2 assets </Text> </ContentCardFooter> </ContentCard> </Pressable> <Text as="h3" font="headline"> Disabled pressable card </Text> <Pressable disabled background="bgAlternate" borderRadius={500} onClick={() => alert('Card pressed!')} width="fit-content" > <ContentCard> <ContentCardHeader subtitle="News" thumbnail={assets.eth.imageUrl} title="CoinDesk" /> <ContentCardBody title="Disabled Card" description="This card is disabled and cannot be clicked." /> <ContentCardFooter alignItems="center"> <RemoteImageGroup shape="circle" size={32}> <RemoteImage src={assets.eth.imageUrl} /> <RemoteImage src={assets.btc.imageUrl} /> </RemoteImageGroup> <Text color="fgMuted" font="label2"> 2 assets </Text> </ContentCardFooter> </ContentCard> </Pressable> </VStack> ); }
Rewards Card Example
Example showing a rewards-style content card with claim button.
function Example() { return ( <VStack gap={2} maxWidth={375}> <ContentCard gap={3}> <ContentCardBody title={ <Text as="p" font="body" paddingTop={0.5}> Bitcoin Network Shatters Records With Hashrate Climbing to 464 EH/s </Text> } label={ <HStack alignItems="flex-end" flexWrap="wrap" gap={0.5}> <Text as="p" color="fgMuted" font="label2" numberOfLines={1}> BTC </Text> <Text as="p" color="fgPositive" font="label2"> ↗ 5.12% </Text> </HStack> } media={ <RemoteImage alt="Rewards banner" aria-hidden="true" src={ethBackground} style={{ objectFit: 'cover', borderRadius: '24px' }} width="100%" /> } /> <ContentCardFooter> <HStack alignItems="center" gap={1}> <Avatar src={assets.btc.imageUrl} size="xl" /> <VStack> <TextLegal as="span" color="fgMuted"> Reward </TextLegal> <Text as="span" font="headline"> +$15 ACS </Text> </VStack> </HStack> <Button compact accessibilityLabel="Claim now" variant="secondary"> Claim Now </Button> </ContentCardFooter> </ContentCard> </VStack> ); }