Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions apps/docs/docs/components/cards/MessagingCard/_mobileExamples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ Use `mediaPlacement` to control the position of media content.

## Upsell Card Styles

MessagingCard with `type="upsell"` supports various background colors to match different promotional purposes. Use the `background` prop for semantic tokens, or `dangerouslySetBackground` for custom spectrum colors.
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** (`renderAsPressable={false}` or omitted): set the background via `styles.root` (e.g. `styles={{ root: { backgroundColor: 'rgb(...)' } }}`).
- **Interactive cards** (`renderAsPressable` with `onPress`): set the background via `blendStyles.background` (e.g. `blendStyles={{ background: 'rgb(...)' }}`) so press states are handled correctly.

### General Upsell

Expand Down Expand Up @@ -140,7 +145,7 @@ function FeatureUpsell() {
<MessagingCard
key={i}
type="upsell"
dangerouslySetBackground={card.bg}
styles={{ root: { backgroundColor: card.bg } }}
title={
<Text color="fgInverse" font="headline">
Up to 3.29% APR on ETH
Expand Down Expand Up @@ -182,7 +187,7 @@ function CommunityUpsell() {
<MessagingCard
key={i}
type="upsell"
dangerouslySetBackground={card.bg}
styles={{ root: { backgroundColor: card.bg } }}
title={
<Text color="fgInverse" font="headline">
Join the community
Expand Down Expand Up @@ -244,7 +249,7 @@ function ProductUpsell() {
<MessagingCard
key={card.title}
type="upsell"
dangerouslySetBackground={card.bg}
styles={{ root: { backgroundColor: card.bg } }}
title={
<Text color="fgInverse" font="headline">
{card.title}
Expand Down Expand Up @@ -291,7 +296,7 @@ function NewsUpsell() {
<MessagingCard
key={i}
type="upsell"
dangerouslySetBackground={card.bg}
styles={{ root: { backgroundColor: card.bg } }}
title={
<Text color="fgInverse" font="headline">
Help defend crypto in America
Expand Down Expand Up @@ -366,7 +371,7 @@ function DismissibleCards() {
<VStack gap={2}>
<MessagingCard
type="upsell"
dangerouslySetBackground={`rgb(${spectrum.teal70})`}
styles={{ root: { backgroundColor: `rgb(${spectrum.teal70})` } }}
title="Dismissible Upsell"
description="Upsell card with dismiss button"
width={320}
Expand Down Expand Up @@ -566,8 +571,10 @@ function DismissibleCardsList() {
<MessagingCard
key={card.id}
type={card.type}
dangerouslySetBackground={
card.type === 'upsell' ? `rgb(${spectrum.gray100})` : undefined
styles={
card.type === 'upsell'
? { root: { backgroundColor: `rgb(${spectrum.gray100})` } }
: undefined
}
title={card.title}
description={card.description}
Expand Down Expand Up @@ -617,7 +624,7 @@ function InteractiveCards() {
renderAsPressable
onPress={() => console.log('Card pressed!')}
type="upsell"
dangerouslySetBackground={`rgb(${spectrum.teal70})`}
blendStyles={{ background: `rgb(${spectrum.teal70})` }}
title="Interactive Upsell"
description="Tap to interact"
width={320}
Expand Down Expand Up @@ -745,7 +752,7 @@ function MultipleCards() {
renderAsPressable
onPress={() => console.log('clicked')}
type="upsell"
dangerouslySetBackground={`rgb(${spectrum.purple70})`}
blendStyles={{ background: `rgb(${spectrum.purple70})` }}
title="Card 3"
description="Card with onPress handler"
tag="Action"
Expand Down Expand Up @@ -820,7 +827,7 @@ When you need both `onDismissButtonPress` and want the entire card to be pressab

### Color Contrast

MessagingCard supports custom backgrounds via `background` and `dangerouslySetBackground` props. When using custom background colors, ensure sufficient color contrast between text and background:
MessagingCard supports custom backgrounds via the `background` prop and, for custom colors, `styles.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`)
Expand Down
33 changes: 21 additions & 12 deletions apps/docs/docs/components/cards/MessagingCard/_webExamples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ Use `mediaPlacement` to control the position of media content.

## Upsell Card Styles

MessagingCard with `type="upsell"` supports various background colors to match different promotional purposes. Use the `background` prop for semantic tokens, or `dangerouslySetBackground` for custom spectrum colors.
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

Expand Down Expand Up @@ -130,7 +135,7 @@ function FeatureUpsell() {
<MessagingCard
key={card.label}
type="upsell"
dangerouslySetBackground={card.bg}
styles={{ root: { backgroundColor: card.bg } }}
title={
<Text color="fgInverse" as="h3" font="headline">
Up to 3.29% APR on ETH
Expand Down Expand Up @@ -179,7 +184,7 @@ function CommunityUpsell() {
<MessagingCard
key={i}
type="upsell"
dangerouslySetBackground={card.bg}
styles={{ root: { backgroundColor: card.bg } }}
title={
<Text color="fgInverse" as="h3" font="headline">
Join the community
Expand Down Expand Up @@ -240,7 +245,7 @@ function ProductUpsell() {
<MessagingCard
key={card.title}
type="upsell"
dangerouslySetBackground={card.bg}
styles={{ root: { backgroundColor: card.bg } }}
title={
<Text color="fgInverse" as="h3" font="headline">
{card.title}
Expand Down Expand Up @@ -286,7 +291,7 @@ function NewsUpsell() {
<MessagingCard
key={i}
type="upsell"
dangerouslySetBackground={card.bg}
styles={{ root: { backgroundColor: card.bg } }}
title={
<Text color="fgInverse" as="h3" font="headline">
Help defend crypto in America
Expand Down Expand Up @@ -373,7 +378,7 @@ Use `onDismissButtonClick` to add a dismiss button.
mediaPlacement="end"
onDismissButtonClick={() => alert('Card dismissed!')}
dismissButtonAccessibilityLabel="Close card"
dangerouslySetBackground="rgb(var(--teal70))"
styles={{ root: { backgroundColor: 'rgb(var(--teal70))' } }}
/>
<MessagingCard
type="nudge"
Expand Down Expand Up @@ -555,7 +560,11 @@ function DismissibleCards() {
<MessagingCard
key={card.id}
type={card.type}
dangerouslySetBackground={card.type === 'upsell' ? 'rgb(var(--gray100))' : undefined}
styles={
card.type === 'upsell'
? { root: { backgroundColor: 'rgb(var(--gray100))' } }
: undefined
}
title={card.title}
description={card.description}
width={360}
Expand Down Expand Up @@ -600,7 +609,7 @@ MessagingCard supports polymorphic rendering with `as` and can be made interacti
<MessagingCard
as="article"
type="upsell"
dangerouslySetBackground="rgb(var(--teal70))"
styles={{ root: { backgroundColor: 'rgb(var(--teal70))' } }}
title="Title"
description="Description"
width={320}
Expand All @@ -621,7 +630,7 @@ MessagingCard supports polymorphic rendering with `as` and can be made interacti
href="https://www.coinbase.com"
target="_blank"
type="upsell"
dangerouslySetBackground="rgb(var(--purple70))"
blendStyles={{ background: 'rgb(var(--purple70))' }}
title="Interactive Upsell"
description="Clickable card with href"
width={320}
Expand Down Expand Up @@ -653,7 +662,7 @@ MessagingCard supports polymorphic rendering with `as` and can be made interacti
as="button"
onClick={() => alert('Card clicked!')}
type="upsell"
dangerouslySetBackground="rgb(var(--gray100))"
blendStyles={{ background: 'rgb(var(--gray100))' }}
title="Interactive Card"
description="Clickable card with onClick handler"
width={320}
Expand Down Expand Up @@ -771,7 +780,7 @@ Display multiple cards in a carousel.
as="button"
onClick={() => console.log('clicked')}
type="upsell"
dangerouslySetBackground="rgb(var(--purple70))"
blendStyles={{ background: 'rgb(var(--purple70))' }}
title="Card 3"
description="Card with onClick handler"
tag="Action"
Expand Down Expand Up @@ -850,7 +859,7 @@ When you need both `onDismissButtonClick` and want the entire card to be clickab

### Color Contrast

MessagingCard supports custom backgrounds via `background` and `dangerouslySetBackground` props. When using custom background colors, ensure sufficient color contrast between text and background:
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`)
Expand Down
24 changes: 24 additions & 0 deletions apps/docs/src/test-no-dangerously-set-background.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Temporary file to test @coinbase/cds/no-dangerously-set-background in the docs app.
* Run: yarn eslint apps/docs/src/test-no-dangerously-set-background.tsx
* You should see a warning on the Button line. Delete this file when done.
*/
import { Button } from '@coinbase/cds-web/buttons';
import { MessagingCard as MessagingCardComponent } from '@coinbase/cds-web/cards/MessagingCard';
import { Interactable } from '@coinbase/cds-web/system/Interactable';

export function TestComponent() {
return (
<>
<Interactable dangerouslySetBackground="red">This should trigger the rule</Interactable>
<MessagingCardComponent
renderAsPressable
dangerouslySetBackground="red"
mediaPlacement="end"
type="upsell"
>
This should trigger the rule
</MessagingCardComponent>
</>
);
}
11 changes: 11 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import eslintReactNativeA11y from 'eslint-plugin-react-native-a11y';
import eslintReactNative from 'eslint-plugin-react-native';
import eslintCodegen from 'eslint-plugin-codegen';
import internalPlugin from '@coinbase/eslint-plugin-internal';
import cdsPlugin from '@coinbase/eslint-plugin-cds';

const ignores = [
'*.md',
Expand Down Expand Up @@ -286,6 +287,16 @@ export default tseslint.config(
...packageProductionRules,
},
},
// CDS rule for docs app (no-dangerously-set-background)
{
files: ['apps/docs/**/*.{ts,tsx}'],
plugins: {
'@coinbase/cds': cdsPlugin,
},
rules: {
'@coinbase/cds/no-dangerously-set-background': 'warn',
},
},
{
files: ['**/*mobile*/**/*.{ts,tsx}'],
settings: sharedSettings,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"@babel/runtime": "^7.28.2",
"@babel/template": "^7.20.7",
"@babel/types": "^7.20.7",
"@coinbase/eslint-plugin-cds": "workspace:^",
"@coinbase/eslint-plugin-internal": "workspace:^",
"@figma/code-connect": "^1.3.13",
"@graphql-tools/jest-transform": "^2.0.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/common/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.

<!-- template-start -->

## 8.47.2 ((2/19/2026, 03:18 PM PST))

This is an artificial version bump with no new change.

## 8.47.1 ((2/19/2026, 01:18 PM PST))

This is an artificial version bump with no new change.
Expand Down
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-common",
"version": "8.47.1",
"version": "8.47.2",
"description": "Coinbase Design System - Common",
"repository": {
"type": "git",
Expand Down
6 changes: 6 additions & 0 deletions packages/eslint-plugin-cds/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.

<!-- template-start -->

## 3.3.0 (2/19/2026 PST)

#### 🚀 Updates

- Add lint rule to avoid dangerouslySetBackground on Interactable. [[#405](https://github.com/coinbase/cds/pull/405)]

## 3.2.1 (10/1/2025 PST)

#### 🐞 Fixes
Expand Down
10 changes: 9 additions & 1 deletion packages/eslint-plugin-cds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,15 @@ This rule also checks for other required a11y labels that need to be enforced ou

### Current CDS Best Practices Rules

TBD
#### no-dangerously-set-background (Web & Mobile)

**Rule Description**:

The `no-dangerously-set-background` rule warns when the `dangerouslySetBackground` prop is used on **Interactable**, **Pressable**, or **Card components** (MessagingCard, MediaCard, DataCard) when the card is interactive. For those card components, the rule only runs when `renderAsPressable` is explicitly set to `true` (or the shorthand `renderAsPressable`). Cards without `renderAsPressable` or with `renderAsPressable={false}` are ignored. Other components (e.g. Box, UpsellCard, CardRoot) are ignored.

**Why**: Background color applied via `dangerouslySetBackground` is not picked up by the color blending logic. As a result, the interactable does not display the correct background in its hovered, pressed, and disabled states. Use `blendStyles.background` so the blending logic applies and these states render correctly.

The rule is enabled by default in both `configs.web` and `configs.mobile` (and legacy configs) as `warn`.

## Development

Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-plugin-cds/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coinbase/eslint-plugin-cds",
"version": "3.2.1",
"version": "3.3.0",
"description": "ESLint plugin for CDS",
"repository": {
"type": "git",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin-cds/src/configs/mobile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function buildMobileConfig(plugin: Record<string, unknown>) {
rules: {
'react-native-a11y/has-accessibility-hint': 'off',
'@coinbase/cds/has-valid-accessibility-descriptors-extended': 'warn',
'@coinbase/cds/no-dangerously-set-background': 'warn',
},
};
}
Expand All @@ -32,5 +33,6 @@ export const legacyMobileConfig = {
rules: {
'react-native-a11y/has-accessibility-hint': 'off',
'@coinbase/cds/has-valid-accessibility-descriptors-extended': 'warn',
'@coinbase/cds/no-dangerously-set-background': 'warn',
},
};
2 changes: 2 additions & 0 deletions packages/eslint-plugin-cds/src/configs/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function buildWebConfig(plugin: Record<string, unknown>) {
},
rules: {
'@coinbase/cds/control-has-associated-label-extended': 'warn',
'@coinbase/cds/no-dangerously-set-background': 'warn',
'@coinbase/cds/no-v7-imports': 'warn',
'jsx-a11y/control-has-associated-label': [
'warn',
Expand All @@ -48,6 +49,7 @@ export const legacyWebConfig = {
plugins: ['jsx-a11y'],
rules: {
'@coinbase/cds/control-has-associated-label-extended': 'warn',
'@coinbase/cds/no-dangerously-set-background': 'warn',
'@coinbase/cds/no-v7-imports': 'warn',
},
overrides: [
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin-cds/src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import type { TSESLint } from '@typescript-eslint/utils';

import { controlHasAssociatedLabelExtended } from './rules/control-has-associated-label-extended';
import { hasValidA11yDescriptorsExtended } from './rules/has-valid-accessibility-descriptors-extended';
import { noDangerouslySetBackground } from './rules/no-dangerously-set-background';
import { noV7Imports } from './rules/no-v7-imports';

export const rules = {
'control-has-associated-label-extended': controlHasAssociatedLabelExtended,
'has-valid-accessibility-descriptors-extended': hasValidA11yDescriptorsExtended,
'no-dangerously-set-background': noDangerouslySetBackground,
'no-v7-imports': noV7Imports,
} as const satisfies {
[key: string]: TSESLint.RuleModule<string, []>;
Expand Down
Loading
Loading