MessagingCard provides two card types for promotional and informational content.
Migrating from NudgeCard or UpsellCard?See the Migration Guide at the end of this page.
Basic Types
Use type to set the card variant:
upsell: Primary background, used for promoting features or products. Use variant="secondary" buttons.
nudge: Alternate background, used for encouraging user actions. Use variant="tertiary" (transparent) buttons for a less intrusive appearance.
<VStack gap={2}>
<MessagingCard
type="upsell"
title="Upsell Card"
description="This is an upsell card with primary background"
width={320}
action="Get started"
onActionButtonClick={() => alert('Action clicked!')}
media={
<RemoteImage
alt="Feature promotional image"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/feature.png"
/>
}
mediaPlacement="end"
/>
<MessagingCard
type="nudge"
title="Nudge Card"
description="This is a nudge card with alternate background"
width={320}
action="Learn more"
onActionButtonClick={() => alert('Action clicked!')}
media={<Pictogram dimension="64x64" name="addToWatchlist" />}
mediaPlacement="end"
/>
</VStack>
Nudge Button StyleUse transparent buttons (variant="tertiary" or transparent prop) for nudge cards. They provide a gentle reminder without being intrusive, blending more seamlessly with the card's alternate background.
Use mediaPlacement to control the position of media content.
<VStack gap={2}>
<MessagingCard
type="nudge"
title="Media End"
description="Media placed at the end (right)"
width={320}
media={<Pictogram dimension="48x48" name="addToWatchlist" />}
mediaPlacement="end"
/>
<MessagingCard
type="nudge"
title="Media Start"
description="Media placed at the start (left)"
width={320}
media={<Pictogram dimension="48x48" name="addToWatchlist" />}
mediaPlacement="start"
/>
</VStack>
Upsell Card Styles
MessagingCard with type="upsell" supports various background colors to match different promotional purposes. Use the background prop for semantic tokens.
For custom background colors, use the recommended approach:
- Non-interactive cards (default
as="article" or renderAsPressable={false}): set the background via styles.root or classNames.root (e.g. styles={{ root: { backgroundColor: 'rgb(var(--blue80))' } }}).
- Interactive cards (
renderAsPressable with as="a" or as="button"): set the background via blendStyles.background (e.g. blendStyles={{ background: 'rgb(var(--blue80))' }}) so press states are handled correctly.
General Upsell
Utilize the default background for general information and non-urgent promotions. Its versatile design is perfect for a broad range of content, providing a subtle yet effective approach to engage users. It's also the most suitable style for Pictogram illustrations.
<MessagingCard
type="upsell"
background="bgPrimaryWash"
title={
<Text as="h3" color="fg" font="headline">
Recurring Buy
</Text>
}
description={
<Text as="p" color="fg" font="label2">
Want to add funds to your card every week or month?
</Text>
}
width={360}
action={
<Button compact variant="secondary">
Get started
</Button>
}
media={
<Box paddingEnd={3}>
<Pictogram name="recurringPurchases" dimension="64x64" />
</Box>
}
mediaPlacement="end"
onDismissButtonClick={() => {}}
dismissButtonAccessibilityLabel="Dismiss"
/>
Feature Upsell
Ideal for highlighting Coinbase tools, innovative features, and unique functionalities. Choose from our palette of distinct colors to make your Feature Upsell stand out. Each color is carefully selected to grab attention while aligning with the specific nature of the feature being promoted.
function FeatureUpsell() {
const cards = [
{ bg: 'rgb(var(--purple70))', label: 'Purple' },
{ bg: 'rgb(var(--teal50))', label: 'Teal' },
{ bg: 'rgb(var(--blue80))', label: 'Blue' },
{ bg: 'rgb(var(--indigo70))', label: 'Indigo' },
];
return (
<VStack gap={2}>
{cards.map((card) => (
<MessagingCard
key={card.label}
type="upsell"
styles={{ root: { backgroundColor: card.bg } }}
title={
<Text color="fgInverse" as="h3" font="headline">
Up to 3.29% APR on ETH
</Text>
}
description={
<Text as="p" font="label2" numberOfLines={3} color="fgInverse">
Earn staking rewards on ETH by holding it on Coinbase
</Text>
}
width={360}
action="Start earning"
onActionButtonClick={() => alert('Action clicked!')}
media={
<RemoteImage
alt="Feature illustration"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/feature.png"
/>
}
mediaPlacement="end"
onDismissButtonClick={() => {}}
dismissButtonAccessibilityLabel="Dismiss"
/>
))}
</VStack>
);
}
Designed for community-focused messaging. Vibrant colors spark enthusiasm and encourage active participation, fostering a sense of community engagement.
function CommunityUpsell() {
const cards = [
{ bg: 'rgb(var(--teal70))', image: '/img/community.png' },
{ bg: 'rgb(var(--purple70))', image: '/img/radial.png' },
];
return (
<VStack gap={2}>
{cards.map((card, i) => (
<MessagingCard
key={i}
type="upsell"
styles={{ root: { backgroundColor: card.bg } }}
title={
<Text color="fgInverse" as="h3" font="headline">
Join the community
</Text>
}
description={
<Text as="p" font="label2" numberOfLines={3} color="fgInverse">
Chat with other devs in our Discord community
</Text>
}
width={360}
action="Join now"
onActionButtonClick={() => alert('Action clicked!')}
media={
<RemoteImage
alt="Community illustration"
height={160}
resizeMode="cover"
shape="rectangle"
source={card.image}
/>
}
mediaPlacement="end"
onDismissButtonClick={() => {}}
dismissButtonAccessibilityLabel="Dismiss"
/>
))}
</VStack>
);
}
Product Upsell
Optimal for business products, security features, and functionalities that emphasize trust and reliability, such as Coinbase One and Coinbase Card. Blue and dark backgrounds symbolize stability, trustworthiness, and professionalism.
function ProductUpsell() {
const cards = [
{
title: 'Coinbase One offer',
description: 'Use code NOV60 when you sign up for Coinbase One',
action: 'Get 60 days free',
bg: 'rgb(var(--blue80))',
image: '/img/marketing.png',
},
{
title: 'Coinbase Card',
description: 'Spend USDC to get rewards with our Visa® debit card',
action: 'Get started',
bg: 'rgb(var(--gray100))',
image: '/img/object.png',
},
];
return (
<VStack gap={2}>
{cards.map((card) => (
<MessagingCard
key={card.title}
type="upsell"
styles={{ root: { backgroundColor: card.bg } }}
title={
<Text color="fgInverse" as="h3" font="headline">
{card.title}
</Text>
}
description={
<Text as="p" font="label2" numberOfLines={3} color="fgInverse">
{card.description}
</Text>
}
width={360}
action={card.action}
onActionButtonClick={() => alert('Action clicked!')}
media={
<RemoteImage
alt="Product illustration"
height={160}
resizeMode="cover"
shape="rectangle"
source={card.image}
/>
}
mediaPlacement="end"
onDismissButtonClick={() => {}}
dismissButtonAccessibilityLabel="Dismiss"
/>
))}
</VStack>
);
}
News Upsell
Specifically tailored for company announcements and policy updates. Its design ensures that important information is conveyed clearly and prominently, ensuring users stay well-informed about the latest developments.
function NewsUpsell() {
const cards = [{ bg: 'rgb(var(--gray100))' }, { bg: 'rgb(var(--indigo70))' }];
return (
<VStack gap={2}>
{cards.map((card, i) => (
<MessagingCard
key={i}
type="upsell"
styles={{ root: { backgroundColor: card.bg } }}
title={
<Text color="fgInverse" as="h3" font="headline">
Help defend crypto in America
</Text>
}
description={
<Text as="p" font="label2" numberOfLines={3} color="fgInverse">
Help us keep crypto in America with a single click
</Text>
}
width={360}
action="Join the fight"
onActionButtonClick={() => alert('Action clicked!')}
media={
<RemoteImage
alt="Place illustration"
height={180}
resizeMode="cover"
shape="rectangle"
source="/img/place.png"
/>
}
mediaPlacement="end"
onDismissButtonClick={() => {}}
dismissButtonAccessibilityLabel="Dismiss"
/>
))}
</VStack>
);
}
Nudge Card Style
Use type="nudge" for gentle reminders or secondary options. Nudge cards use the alternate background and blend more seamlessly with the page. Pair them with Pictogram illustrations and transparent buttons.
<VStack gap={2}>
<MessagingCard
type="nudge"
title="Earn more crypto"
description="You've got unstaked crypto. Stake it now to earn more."
width={360}
action="Start earning"
onActionButtonClick={() => alert('Action clicked!')}
media={<Pictogram dimension="64x64" name="key" />}
mediaPlacement="end"
onDismissButtonClick={() => {}}
dismissButtonAccessibilityLabel="Dismiss"
/>
<MessagingCard
type="nudge"
title="Derivatives Trading"
description="Derivative Exchange is available for all users"
width={360}
media={<Pictogram dimension="48x48" name="derivativesNavigation" />}
mediaPlacement="end"
/>
</VStack>
Features
Dismissible Cards
Use onDismissButtonClick to add a dismiss button.
<VStack gap={2}>
<MessagingCard
type="upsell"
title="Dismissible Upsell"
description="Upsell card with dismiss button"
width={320}
media={
<RemoteImage
alt="Community illustration"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/community.png"
/>
}
mediaPlacement="end"
onDismissButtonClick={() => alert('Card dismissed!')}
dismissButtonAccessibilityLabel="Close card"
styles={{ root: { backgroundColor: 'rgb(var(--teal70))' } }}
/>
<MessagingCard
type="nudge"
title="Dismissible Nudge"
description="Nudge card with dismiss button"
width={320}
media={<Pictogram dimension="48x48" name="baseStar" />}
mediaPlacement="end"
onDismissButtonClick={() => alert('Card dismissed!')}
dismissButtonAccessibilityLabel="Close card"
/>
</VStack>
Use tag to add a label badge.
<VStack gap={2}>
<MessagingCard
type="upsell"
title="Tagged Upsell"
description="Upsell card with a tag"
width={320}
tag="New"
media={
<RemoteImage
alt="Place illustration"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/place.png"
/>
}
mediaPlacement="end"
/>
<MessagingCard
type="nudge"
title="Tagged Nudge"
description="Nudge card with a tag"
width={320}
tag="New"
media={<Pictogram dimension="48x48" name="key" />}
mediaPlacement="end"
/>
</VStack>
Actions
Use the action prop to add an action button. Pass a string to render a default button with onActionButtonClick, or pass a custom React element.
<VStack gap={2}>
<MessagingCard
type="upsell"
title="Upsell with Action"
description="Upsell card with action button"
width={320}
action="Action"
onActionButtonClick={() => alert('Action clicked!')}
media={
<RemoteImage
alt="Feature illustration"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/feature.png"
/>
}
mediaPlacement="end"
/>
<MessagingCard
type="nudge"
title="Nudge with Action"
description="Nudge card with action button"
width={320}
action="Learn More"
onActionButtonClick={() => alert('Action clicked!')}
media={<Pictogram dimension="64x64" name="wallet" />}
mediaPlacement="end"
/>
</VStack>
Complete Example
Combine all features in a complete card.
<VStack gap={2}>
<MessagingCard
type="upsell"
title="Complete Upsell Card"
description="Complete upsell card with all features"
width={360}
tag="New"
action="Get Started"
onActionButtonClick={() => alert('Action clicked!')}
onDismissButtonClick={() => alert('Dismissed')}
dismissButtonAccessibilityLabel="Dismiss"
media={
<RemoteImage
alt="Marketing illustration"
height={184}
resizeMode="cover"
shape="rectangle"
source="/img/marketing.png"
/>
}
mediaPlacement="end"
/>
<MessagingCard
type="nudge"
title="Complete Nudge Card"
description="Complete nudge card with all features"
width={360}
tag="New"
action="Learn More"
onActionButtonClick={() => alert('Action clicked!')}
onDismissButtonClick={() => alert('Dismissed')}
dismissButtonAccessibilityLabel="Dismiss"
media={<Pictogram dimension="64x64" name="giftbox" />}
mediaPlacement="end"
/>
</VStack>
Interactive Dismissible List
This example shows a list of cards that can be dismissed interactively. Click the dismiss button to remove cards from the list.
function DismissibleCards() {
const cards = [
{
id: '1',
title: 'Welcome to Coinbase',
description: 'Get started with your crypto journey',
type: 'upsell',
},
{
id: '2',
title: 'Complete your profile',
description: 'Add your details to unlock more features',
type: 'nudge',
},
{
id: '3',
title: 'Enable notifications',
description: 'Stay updated on market movements',
type: 'upsell',
},
{
id: '4',
title: 'Invite friends',
description: 'Earn rewards when friends join',
type: 'nudge',
},
];
const [dismissedIds, setDismissedIds] = React.useState(new Set());
const handleDismiss = (id) => {
setDismissedIds((prev) => new Set(prev).add(id));
};
const handleReset = () => {
setDismissedIds(new Set());
};
const visibleCards = cards.filter((card) => !dismissedIds.has(card.id));
return (
<VStack gap={2}>
<HStack gap={2} flexWrap="wrap">
{visibleCards.map((card) => (
<MessagingCard
key={card.id}
type={card.type}
styles={
card.type === 'upsell'
? { root: { backgroundColor: 'rgb(var(--gray100))' } }
: undefined
}
title={card.title}
description={card.description}
width={360}
media={
card.type === 'upsell' ? (
<RemoteImage
alt="Promotional illustration"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/object.png"
/>
) : (
<Pictogram dimension="48x48" name="addToWatchlist" />
)
}
mediaPlacement="end"
onDismissButtonClick={() => handleDismiss(card.id)}
dismissButtonAccessibilityLabel={`Dismiss ${card.title}`}
/>
))}
{visibleCards.length === 0 && (
<Text color="fgNegative" font="label1">
All cards dismissed!
</Text>
)}
</HStack>
<Button onClick={handleReset} variant="tertiary">
Reset Cards
</Button>
</VStack>
);
}
Polymorphic and Interactive
MessagingCard supports polymorphic rendering with as and can be made interactive with renderAsPressable.
<VStack gap={2}>
<MessagingCard
as="article"
type="upsell"
styles={{ root: { backgroundColor: 'rgb(var(--teal70))' } }}
title="Title"
description="Description"
width={320}
media={
<RemoteImage
alt="Community illustration"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/community.png"
/>
}
mediaPlacement="end"
/>
<MessagingCard
renderAsPressable
as="a"
href="https://www.coinbase.com"
target="_blank"
type="upsell"
blendStyles={{ background: 'rgb(var(--purple70))' }}
title="Interactive Upsell"
description="Clickable card with href"
width={320}
media={
<RemoteImage
alt="Radial design"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/radial.png"
/>
}
mediaPlacement="end"
/>
<MessagingCard
renderAsPressable
as="a"
href="https://www.coinbase.com"
target="_blank"
type="nudge"
title="Interactive Nudge"
description="Clickable nudge with href"
width={320}
media={<Pictogram dimension="48x48" name="baseRocket" />}
mediaPlacement="end"
/>
<MessagingCard
renderAsPressable
as="button"
onClick={() => alert('Card clicked!')}
type="upsell"
blendStyles={{ background: 'rgb(var(--gray100))' }}
title="Interactive Card"
description="Clickable card with onClick handler"
width={320}
media={
<RemoteImage
alt="Object illustration"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/object.png"
/>
}
mediaPlacement="end"
/>
</VStack>
Custom Content
Use React nodes for custom styled content.
<VStack gap={2}>
<MessagingCard
type="upsell"
title="This is a very long title text that demonstrates text wrapping"
description="This is a very long description text that demonstrates how the card handles longer content and wraps appropriately within the card layout"
width={320}
media={
<RemoteImage
alt="Place illustration"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/place.png"
/>
}
mediaPlacement="end"
/>
<MessagingCard
type="upsell"
width={320}
height={160}
title={
<Text color="fgInverse" font="title3">
Custom Title
</Text>
}
tag={
<Text color="fgInverse" font="label2">
Custom Tag
</Text>
}
description={
<Text color="fgInverse" font="label2" numberOfLines={3}>
Custom description with <strong>bold text</strong> and <em>italic text</em>
</Text>
}
media={
<RemoteImage
alt="Collection illustration"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/collection.png"
/>
}
mediaPlacement="end"
/>
</VStack>
Multiple Cards
Display multiple cards in a carousel.
<Carousel styles={{ carousel: { gap: 16 } }}>
<CarouselItem id="card1">
<MessagingCard
as="article"
type="upsell"
title="Card 1"
description="Non-interactive card"
width={320}
media={
<RemoteImage
alt="Marketing illustration"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/marketing.png"
/>
}
mediaPlacement="end"
/>
</CarouselItem>
<CarouselItem id="card2">
<MessagingCard
renderAsPressable
as="a"
href="https://www.coinbase.com"
target="_blank"
type="nudge"
title="Card 2"
description="Clickable card with href"
tag="Link"
media={<Pictogram dimension="64x64" name="addToWatchlist" />}
mediaPlacement="end"
/>
</CarouselItem>
<CarouselItem id="card3">
<MessagingCard
renderAsPressable
as="button"
onClick={() => console.log('clicked')}
type="upsell"
blendStyles={{ background: 'rgb(var(--purple70))' }}
title="Card 3"
description="Card with onClick handler"
tag="Action"
media={
<RemoteImage
alt="Radial design"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/radial.png"
/>
}
mediaPlacement="end"
/>
</CarouselItem>
</Carousel>
Accessibility
When you need both onDismissButtonClick and want the entire card to be clickable, you should handle accessibility carefully to avoid nested interactive elements.
The Problem: If you use renderAsPressable with onClick and also have onDismissButtonClick, the card becomes a button containing another button (the dismiss button). This creates accessibility issues for screen reader users.
The Solution: Mark the card as non-accessible and add a separate action button inside the card with the same action. This allows:
- Regular users to click anywhere on the card
- Screen reader users to focus on individual interactive elements (action button + dismiss button)
<MessagingCard
renderAsPressable
tabIndex={-1}
as="div"
onClick={() => alert('Card clicked - navigating...')}
type="upsell"
title="Accessible Interactive Card"
description="Card with both dismiss and card-level action"
width={360}
action={
<Button
compact
variant="secondary"
onClick={(event) => {
event.stopPropagation();
alert('Button clicked - navigating...');
}}
>
Learn More
</Button>
}
background="accentBoldPurple"
onDismissButtonClick={() => alert('Dismissed')}
dismissButtonAccessibilityLabel="Dismiss promotion"
media={
<RemoteImage
alt="Feature illustration"
height={160}
resizeMode="cover"
shape="rectangle"
source="/img/feature.png"
/>
}
mediaPlacement="end"
/>
Key points:
- Use
as="div" to avoid rendering as a semantic button
- When using
as="div" with renderAsPressable, the card remains keyboard focusable. Set tabIndex={-1} to remove it from the tab order if needed
- Call
event.stopPropagation() at the beginning of the event handler method passed into the onClick prop for action buttons. This will prevent two click events from firing if the user directly clicks the action button.
- Use
actionButtonAccessibilityLabel and dismissButtonAccessibilityLabel to add or override the aria-label for the action and dismiss buttons
Color Contrast
MessagingCard supports custom backgrounds via the background prop and, for custom colors, styles.root / classNames.root (non-interactive) or blendStyles.background (interactive). When using custom background colors, ensure sufficient color contrast between text and background:
- Use
fgInverse text color with dark backgrounds (e.g., accentBoldPurple, bgInverse)
- Use
fg text color with light backgrounds (e.g., bgPrimaryWash, bgAlternate)
- Use the WebAIM Contrast Checker to verify your color combinations meet WCAG AA guidelines (4.5:1 for normal text)
Migration from Deprecated Components
Migrating from NudgeCard
Replace NudgeCard with MessagingCard using type="nudge".
<NudgeCard
title="Title"
description="Description"
pictogram="addToWatchlist"
action="Learn more"
onActionPress={handleAction}
onDismissPress={handleDismiss}
/>
<MessagingCard
type="nudge"
title="Title"
description="Description"
media={<Pictogram dimension="64x64" name="addToWatchlist" />}
action="Learn more"
onActionButtonClick={handleAction}
onDismissButtonClick={handleDismiss}
mediaPlacement="end"
/>
Migrating from UpsellCard
Replace UpsellCard with MessagingCard using type="upsell".
<UpsellCard
title="Title"
description="Description"
media={<RemoteImage ... />}
action="Get Started"
onActionPress={handleAction}
onDismissPress={handleDismiss}
/>
<MessagingCard
type="upsell"
title="Title"
description="Description"
media={<RemoteImage ... />}
action="Get Started"
onActionButtonClick={handleAction}
onDismissButtonClick={handleDismiss}
mediaPlacement="end"
/>