diff --git a/apps/docs/docs/components/charts/Legend/_webExamples.mdx b/apps/docs/docs/components/charts/Legend/_webExamples.mdx
index def80130fa..ce6b3e867a 100644
--- a/apps/docs/docs/components/charts/Legend/_webExamples.mdx
+++ b/apps/docs/docs/components/charts/Legend/_webExamples.mdx
@@ -710,7 +710,7 @@ function InteractiveLegend() {
compact
aria-label={`${isEmphasized ? 'Remove emphasis from' : 'Emphasize'} ${label} series`}
aria-pressed={isEmphasized}
- inverted={isEmphasized}
+ active={isEmphasized}
onClick={() => handleToggle(seriesId)}
style={{
backgroundColor: `rgb(var(${baseColor}10))`,
diff --git a/apps/docs/docs/components/inputs/Chip/_mobileExamples.mdx b/apps/docs/docs/components/inputs/Chip/_mobileExamples.mdx
index c956da4432..f12f97df15 100644
--- a/apps/docs/docs/components/inputs/Chip/_mobileExamples.mdx
+++ b/apps/docs/docs/components/inputs/Chip/_mobileExamples.mdx
@@ -50,21 +50,41 @@ function Example() {
## Styling
-### Color
+### Active Color
-Use `invertColorScheme` to invert foreground and background for emphasis.
+Use `active` for high-contrast emphasis. When `active` is true, the Chip inverts the color scheme for everything inside its borders — `background`, `color`, icons, and other token-based colors are all resolved against the opposite light/dark palette. Set `background` and `color` to control the inactive appearance; those same tokens flip automatically when active.
```tsx
function Example() {
return (
Default
- Inverted
+ Active
+
+ Custom tokens
+
);
}
```
+To break from the inverted convention, set `activeBackground` and/or `activeColor` while `active` is true. The Chip paints those tokens directly instead of inverting. **Any `start`, `end`, or `children` ReactNodes need explicit `color` props** — they are not updated automatically.
+
+```tsx
+function Example() {
+ return (
+ }
+ >
+ Positive active
+
+ );
+}
+```
+
### Sizes
`size` accepts `xs` and `s`. Defaults to `s`.
diff --git a/apps/docs/docs/components/inputs/Chip/_webExamples.mdx b/apps/docs/docs/components/inputs/Chip/_webExamples.mdx
index 79748c0252..77f687e616 100644
--- a/apps/docs/docs/components/inputs/Chip/_webExamples.mdx
+++ b/apps/docs/docs/components/inputs/Chip/_webExamples.mdx
@@ -50,21 +50,41 @@ function Example() {
## Styling
-### Color
+### Active Color
-Use `invertColorScheme` to invert foreground and background for emphasis.
+Use `active` for high-contrast emphasis. When `active` is true, the Chip inverts the color scheme for everything inside its borders — `background`, `color`, icons, and other token-based colors are all resolved against the opposite light/dark palette. Set `background` and `color` to control the inactive appearance; those same tokens flip automatically when active.
```tsx live
function Example() {
return (
Default
- Inverted
+ Active
+
+ Custom tokens
+
);
}
```
+To break from the inverted convention, set `activeBackground` and/or `activeColor` while `active` is true. The Chip paints those tokens directly instead of inverting. **Any `start`, `end`, or `children` ReactNodes need explicit `color` props** — they are not updated automatically.
+
+```tsx live
+function Example() {
+ return (
+ }
+ >
+ Positive active
+
+ );
+}
+```
+
### Sizes
`size` accepts `xs` and `s`. Defaults to `s`.
diff --git a/apps/docs/docs/components/inputs/InputChip/_mobileExamples.mdx b/apps/docs/docs/components/inputs/InputChip/_mobileExamples.mdx
index 84ba7ff5f2..b6bddab821 100644
--- a/apps/docs/docs/components/inputs/InputChip/_mobileExamples.mdx
+++ b/apps/docs/docs/components/inputs/InputChip/_mobileExamples.mdx
@@ -79,20 +79,16 @@ function Example() {
```
-### Invert color scheme
+### Active
-Use `invertColorScheme` to emphasize removable values.
+InputChip is active by default. Set `active={false}` for a less emphasized appearance.
```tsx
function Example() {
return (
- console.log('Remove Default')} value="Default" />
- console.log('Remove Inverted')}
- value="Inverted"
- />
+ console.log('Remove Active')} value="Active (default)" />
+ console.log('Remove Inactive')} value="Inactive" />
);
}
diff --git a/apps/docs/docs/components/inputs/InputChip/_webExamples.mdx b/apps/docs/docs/components/inputs/InputChip/_webExamples.mdx
index 23f5a09924..4f93dbc634 100644
--- a/apps/docs/docs/components/inputs/InputChip/_webExamples.mdx
+++ b/apps/docs/docs/components/inputs/InputChip/_webExamples.mdx
@@ -86,20 +86,16 @@ function Example() {
}
```
-### Invert color scheme
+### Active
-Use `invertColorScheme` to emphasize removable values.
+InputChip is active by default. Set `active={false}` for a less emphasized appearance.
```tsx live
function Example() {
return (
- console.log('Remove Default')} value="Default" />
- console.log('Remove Inverted')}
- value="Inverted"
- />
+ console.log('Remove Active')} value="Active (default)" />
+ console.log('Remove Inactive')} value="Inactive" />
);
}
diff --git a/apps/docs/docs/components/inputs/MediaChip/_mobileExamples.mdx b/apps/docs/docs/components/inputs/MediaChip/_mobileExamples.mdx
index 88abca1c95..cf36e9a3c7 100644
--- a/apps/docs/docs/components/inputs/MediaChip/_mobileExamples.mdx
+++ b/apps/docs/docs/components/inputs/MediaChip/_mobileExamples.mdx
@@ -76,15 +76,15 @@ MediaChip supports all six content combinations automatically.
```
-### Invert color scheme
+### Active
-Use `invertColorScheme` to emphasize the chip with inverted colors.
+Use `active` to emphasize the chip with high-contrast colors.
```tsx
- Selected
+ Selected
}
start={}
>
diff --git a/apps/docs/docs/components/inputs/MediaChip/_webExamples.mdx b/apps/docs/docs/components/inputs/MediaChip/_webExamples.mdx
index 489edd08ff..1d26b3ecc9 100644
--- a/apps/docs/docs/components/inputs/MediaChip/_webExamples.mdx
+++ b/apps/docs/docs/components/inputs/MediaChip/_webExamples.mdx
@@ -84,15 +84,15 @@ MediaChip supports all six content combinations automatically.
```
-### Invert color scheme
+### Active
-Use `invertColorScheme` to emphasize the chip with inverted colors.
+Use `active` to emphasize the chip with high-contrast colors.
```tsx live
- Selected
+ Selected
}
start={}
>
diff --git a/apps/docs/docs/components/inputs/SelectChipAlpha/_mobileExamples.mdx b/apps/docs/docs/components/inputs/SelectChipAlpha/_mobileExamples.mdx
index c08c7edaf0..ac68a076b3 100644
--- a/apps/docs/docs/components/inputs/SelectChipAlpha/_mobileExamples.mdx
+++ b/apps/docs/docs/components/inputs/SelectChipAlpha/_mobileExamples.mdx
@@ -4,6 +4,12 @@ SelectChip is a chip-styled control built on top of the [Alpha Select](/componen
Avoid using options with duplicate values. Each option's `value` should be unique within the options array to ensure proper selection behavior.
:::
+## Active state
+
+SelectChip uses the same `active` semantics as [Chip](/components/inputs/Chip/): when `active` is true, the control inverts the color scheme for everything inside the chip borders.
+
+By default, SelectChip sets `active` to `true` whenever one or more values are selected and `false` when the control is empty. You do not need to wire `active` to `value` manually. Pass `active` explicitly to force the emphasized state even when nothing is selected, or set `active={false}` to keep the chip visually inactive despite a selection.
+
## Basics
### Basic usage
@@ -256,32 +262,6 @@ function ExampleSizes() {
}
```
-### Inverted
-
-```tsx
-function ExampleInverted() {
- const exampleOptions = [
- { value: null, label: 'Clear selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- ];
- const [value, setValue] = useState(null);
- const hasValue = value !== null;
- return (
-
- );
-}
-```
-
### Start and end nodes
```tsx
diff --git a/apps/docs/docs/components/inputs/SelectChipAlpha/_webExamples.mdx b/apps/docs/docs/components/inputs/SelectChipAlpha/_webExamples.mdx
index 246be706a2..4a7018fc6a 100644
--- a/apps/docs/docs/components/inputs/SelectChipAlpha/_webExamples.mdx
+++ b/apps/docs/docs/components/inputs/SelectChipAlpha/_webExamples.mdx
@@ -4,6 +4,12 @@ SelectChip is a chip-styled control built on top of the [Alpha Select](/componen
Avoid using options with duplicate values. Each option's `value` should be unique within the options array to ensure proper selection behavior.
:::
+## Active state
+
+SelectChip uses the same `active` semantics as [Chip](/components/inputs/Chip/): when `active` is true, the control inverts the color scheme for everything inside the chip borders.
+
+By default, SelectChip sets `active` to `true` whenever one or more values are selected and `false` when the control is empty. You do not need to wire `active` to `value` manually. Pass `active` explicitly to force the emphasized state even when nothing is selected, or set `active={false}` to keep the chip visually inactive despite a selection.
+
## Basics
### Basic usage
@@ -277,32 +283,6 @@ function ExampleSizes() {
}
```
-### Inverted
-
-```jsx live
-function ExampleInverted() {
- const exampleOptions = [
- { value: null, label: 'Clear selection' },
- { value: '1', label: 'Option 1' },
- { value: '2', label: 'Option 2' },
- { value: '3', label: 'Option 3' },
- { value: '4', label: 'Option 4' },
- ];
- const [value, setValue] = useState(null);
- const hasValue = value !== null;
- return (
-
- );
-}
-```
-
### Start and end nodes
```jsx live
diff --git a/apps/expo-app/src/playground/CustomerComponentConfigScreen.tsx b/apps/expo-app/src/playground/CustomerComponentConfigScreen.tsx
index 9f704e3c4c..641b80ceca 100644
--- a/apps/expo-app/src/playground/CustomerComponentConfigScreen.tsx
+++ b/apps/expo-app/src/playground/CustomerComponentConfigScreen.tsx
@@ -4,6 +4,11 @@ import type { TabValue } from '@coinbase/cds-common/tabs/useTabs';
import { Accordion } from '@coinbase/cds-mobile/accordion/Accordion';
import { AccordionItem } from '@coinbase/cds-mobile/accordion/AccordionItem';
import { Select } from '@coinbase/cds-mobile/alpha/select/Select';
+import { SelectChip } from '@coinbase/cds-mobile/alpha/select-chip/SelectChip';
+import { TabbedChips } from '@coinbase/cds-mobile/alpha/tabbed-chips/TabbedChips';
+import { InputChip } from '@coinbase/cds-mobile/chips/InputChip';
+import { CheckboxCell } from '@coinbase/cds-mobile/controls/CheckboxCell';
+import { RadioCell } from '@coinbase/cds-mobile/controls/RadioCell';
import { Switch } from '@coinbase/cds-mobile/controls/Switch';
import { TextInput } from '@coinbase/cds-mobile/controls/TextInput';
import { DateInput } from '@coinbase/cds-mobile/dates/DateInput';
@@ -18,10 +23,13 @@ import { SegmentedTabs } from '@coinbase/cds-mobile/tabs/SegmentedTabs';
import { Tabs } from '@coinbase/cds-mobile/tabs/Tabs';
import { Text } from '@coinbase/cds-mobile/typography/Text';
+import type { ComponentConfig } from '@coinbase/cds-mobile/core/componentConfig';
+
import { ComponentConfigComparison } from './customerComponentConfig/ComponentConfigComparison';
import { customerComponentConfig } from './customerComponentConfig/customerComponentConfig';
import { retailCDSTheme } from './customerComponentConfig/retailCDSTheme';
+const emptyConfig: ComponentConfig = {};
const scrollContentContainerStyle = { flexGrow: 1 };
const tabsExampleData: TabValue[] = [
@@ -54,16 +62,130 @@ const SegmentedTabsExample = memo(() => {
);
});
-const comparisonKeys = ['Accordion', 'Inputs', 'Tabs', 'SegmentedTabs'] as const;
+const tabbedChipsExampleData: TabValue[] = [
+ { id: 'overview', label: 'Overview' },
+ { id: 'activity', label: 'Activity' },
+ { id: 'settings', label: 'Settings' },
+];
+
+/** Self-contained Alpha TabbedChips example; TabComponent comes from customer component config. */
+const TabbedChipsExample = memo(() => {
+ const [activeTab, setActiveTab] = useState(tabbedChipsExampleData[0]);
+ return (
+
+ );
+});
+
+const chipExampleLabel = 'Basic Chip';
+
+const InputChipExample = memo(() => (
+
+ {}}>
+ {chipExampleLabel}
+
+ {}}>{chipExampleLabel}
+
+));
+
+const selectChipOptions = [
+ { value: '1', label: 'Option 1' },
+ { value: '2', label: 'Option 2' },
+ { value: '3', label: 'Option 3' },
+];
+
+const SelectChipExample = memo(() => {
+ const [value, setValue] = useState(null);
+
+ return (
+
+ );
+});
+
+const CheckboxCellExample = memo(() => {
+ const [checked, setChecked] = useState(false);
+
+ return (
+
+ setChecked(!!nextChecked)}
+ title="Checkbox cell"
+ value="checkbox-cell"
+ />
+ {}}
+ title="Selected and disabled"
+ value="checkbox-cell-disabled"
+ />
+
+ );
+});
+
+const RadioCellExample = memo(() => {
+ const [value, setValue] = useState('option-1');
+
+ return (
+
+ {
+ if (next) setValue(next);
+ }}
+ title="Option 1"
+ value="option-1"
+ />
+ {
+ if (next) setValue(next);
+ }}
+ title="Option 2"
+ value="option-2"
+ />
+
+ );
+});
+
+const comparisonKeys = [
+ 'Accordion',
+ 'Inputs',
+ 'InputChip',
+ 'SelectChip',
+ 'Tabs',
+ 'SegmentedTabs',
+ 'TabbedChips',
+ 'CheckboxCell',
+ 'RadioCell',
+] as const;
type ComparisonKey = (typeof comparisonKeys)[number];
const initialConfiguredState: Record = {
Accordion: false,
Inputs: false,
+ InputChip: false,
+ SelectChip: false,
Tabs: false,
SegmentedTabs: false,
+ TabbedChips: false,
+ CheckboxCell: false,
+ RadioCell: false,
};
+const resolveSwitchChecked = (currentChecked: boolean, nextChecked?: boolean) =>
+ typeof nextChecked === 'boolean' ? nextChecked : !currentChecked;
+
/**
* Playground screen for iterating on a customer's CDS component config.
* Wrap examples in {@link ComponentConfigComparison} to toggle stock CDS vs configured.
@@ -73,41 +195,30 @@ export const CustomerComponentConfigScreen = memo(() => {
const [useRetailTheme, setUseRetailTheme] = useState(false);
const [configuredState, setConfiguredState] = useState(initialConfiguredState);
- const handleToggleRetailTheme = useCallback((_: string | undefined, checked?: boolean) => {
- setUseRetailTheme(Boolean(checked));
+ const handleToggleRetailTheme = useCallback((_: string | undefined, nextChecked?: boolean) => {
+ setUseRetailTheme((prev) => resolveSwitchChecked(prev, nextChecked));
}, []);
- const setConfigured = useCallback((key: ComparisonKey, checked: boolean) => {
+ const handleConfiguredChange = useCallback((key: ComparisonKey, checked: boolean) => {
setConfiguredState((prev) => ({ ...prev, [key]: checked }));
}, []);
- const handleAccordionChange = useCallback(
- (checked: boolean) => setConfigured('Accordion', checked),
- [setConfigured],
- );
- const handleInputsChange = useCallback(
- (checked: boolean) => setConfigured('Inputs', checked),
- [setConfigured],
- );
- const handleTabsChange = useCallback(
- (checked: boolean) => setConfigured('Tabs', checked),
- [setConfigured],
- );
- const handleSegmentedTabsChange = useCallback(
- (checked: boolean) => setConfigured('SegmentedTabs', checked),
- [setConfigured],
- );
- const allConfigured = comparisonKeys.every((key) => configuredState[key]);
+ const someConfigured = comparisonKeys.some((key) => configuredState[key]);
- const handleToggleAllConfigured = useCallback((_: string | undefined, checked?: boolean) => {
- const nextChecked = Boolean(checked);
- setConfiguredState(
- comparisonKeys.reduce(
- (next, key) => ({ ...next, [key]: nextChecked }),
- {} as Record,
- ),
- );
- }, []);
+ const handleToggleAllConfigured = useCallback(
+ (_: string | undefined, switchChecked?: boolean) => {
+ setConfiguredState((prev) => {
+ const someOn = comparisonKeys.some((key) => prev[key]);
+ // Any configured -> turn all off; none configured -> turn all on.
+ const nextAllConfigured = typeof switchChecked === 'boolean' ? switchChecked : !someOn;
+ return comparisonKeys.reduce(
+ (next, key) => ({ ...next, [key]: nextAllConfigured }),
+ {} as Record,
+ );
+ });
+ },
+ [],
+ );
return (
@@ -137,18 +248,20 @@ export const CustomerComponentConfigScreen = memo(() => {
-
- Toggle all
-
+
+
+ Toggle all
+
+
@@ -156,7 +269,7 @@ export const CustomerComponentConfigScreen = memo(() => {
handleConfiguredChange('Accordion', checked)}
>
{() => (
@@ -172,7 +285,7 @@ export const CustomerComponentConfigScreen = memo(() => {
handleConfiguredChange('Inputs', checked)}
>
{() => (
@@ -201,20 +314,55 @@ export const CustomerComponentConfigScreen = memo(() => {
)}
+ handleConfiguredChange('InputChip', checked)}
+ >
+ {() => }
+
+ handleConfiguredChange('SelectChip', checked)}
+ >
+ {() => }
+
handleConfiguredChange('Tabs', checked)}
>
{() => }
handleConfiguredChange('SegmentedTabs', checked)}
>
{() => }
+ handleConfiguredChange('TabbedChips', checked)}
+ >
+ {() => }
+
+ handleConfiguredChange('CheckboxCell', checked)}
+ >
+ {() => }
+
+ handleConfiguredChange('RadioCell', checked)}
+ >
+ {() => }
+
diff --git a/apps/expo-app/src/playground/customerComponentConfig/ComponentConfigComparison.tsx b/apps/expo-app/src/playground/customerComponentConfig/ComponentConfigComparison.tsx
index 97b15badc6..d84589bd21 100644
--- a/apps/expo-app/src/playground/customerComponentConfig/ComponentConfigComparison.tsx
+++ b/apps/expo-app/src/playground/customerComponentConfig/ComponentConfigComparison.tsx
@@ -21,6 +21,10 @@ export type ComponentConfigComparisonProps = {
children: () => React.ReactNode;
};
+/** Resolve the next checked value from Control's onChange, falling back to toggling local state. */
+const resolveSwitchChecked = (currentChecked: boolean, nextChecked?: boolean) =>
+ typeof nextChecked === 'boolean' ? nextChecked : !currentChecked;
+
/**
* Toggle orchestration for customer component config work.
* Place inside a screen-level {@link ComponentConfigProvider} with the customer config;
@@ -30,9 +34,9 @@ export const ComponentConfigComparison = memo(
({ componentName, checked, onChange, children }: ComponentConfigComparisonProps) => {
const handleToggle = useCallback(
(_: string | undefined, nextChecked?: boolean) => {
- onChange(Boolean(nextChecked));
+ onChange(resolveSwitchChecked(checked, nextChecked));
},
- [onChange],
+ [checked, onChange],
);
return (
diff --git a/apps/expo-app/src/playground/customerComponentConfig/customTabComponent.tsx b/apps/expo-app/src/playground/customerComponentConfig/customTabComponent.tsx
new file mode 100644
index 0000000000..77c5f57466
--- /dev/null
+++ b/apps/expo-app/src/playground/customerComponentConfig/customTabComponent.tsx
@@ -0,0 +1,38 @@
+import React, { memo, useCallback, useMemo } from 'react';
+import { useTabsContext } from '@coinbase/cds-common/tabs/TabsContext';
+import type { TabbedChipProps } from '@coinbase/cds-mobile/alpha/tabbed-chips/TabbedChips';
+import { Chip } from '@coinbase/cds-mobile/chips/Chip';
+
+/**
+ * TabbedChips tab renderer stub — wired through {@link customerComponentConfig}.
+ * Uses tab context for active state and selection; customize chip styling/behavior below.
+ */
+export const CustomTabComponent = memo(
+ ({
+ label = '',
+ id,
+ Component: _Component,
+ size,
+ ...chipRenderProps
+ }: TabbedChipProps) => {
+ const { activeTab, updateActiveTab } = useTabsContext();
+ const isActive = useMemo(() => activeTab?.id === id, [activeTab, id]);
+ const handlePress = useCallback(() => updateActiveTab(id), [id, updateActiveTab]);
+
+ return (
+
+ {label}
+
+ );
+ },
+);
+
+CustomTabComponent.displayName = 'CustomTabComponent';
diff --git a/apps/expo-app/src/playground/customerComponentConfig/customerComponentConfig.ts b/apps/expo-app/src/playground/customerComponentConfig/customerComponentConfig.ts
index efcde05f1d..4b6d6e1b2d 100644
--- a/apps/expo-app/src/playground/customerComponentConfig/customerComponentConfig.ts
+++ b/apps/expo-app/src/playground/customerComponentConfig/customerComponentConfig.ts
@@ -1,4 +1,37 @@
-import type { ComponentConfig } from '@coinbase/cds-mobile/core/componentConfig';
+import type { SelectChipBaseProps } from '@coinbase/cds-mobile/alpha/select-chip/SelectChip';
+import type { TabbedChipsBaseProps } from '@coinbase/cds-mobile/alpha/tabbed-chips/TabbedChips';
+import type { InputChipBaseProps } from '@coinbase/cds-mobile/chips/ChipProps';
+import type { CheckboxCellBaseProps } from '@coinbase/cds-mobile/controls/CheckboxCell';
+import type { RadioCellBaseProps } from '@coinbase/cds-mobile/controls/RadioCell';
+import type { ComponentConfig, ConfigResolver } from '@coinbase/cds-mobile/core/componentConfig';
+
+import { CustomTabComponent } from './customTabComponent';
+
+const inputChipStyleConfigResolver: ConfigResolver = (props) =>
+ props.active
+ ? {
+ activeBackground: 'bgSecondary',
+ borderWidth: 100,
+ borderColor: 'bgSecondary',
+ }
+ : {
+ background: 'bg',
+ borderWidth: 100,
+ borderColor: 'bgLine',
+ };
+
+const selectChipStyleConfigResolver: ConfigResolver = (props) =>
+ props.active
+ ? {
+ activeBackground: 'bgSecondary',
+ borderWidth: 100,
+ borderColor: 'bgSecondary',
+ }
+ : {
+ background: 'bg',
+ borderWidth: 100,
+ borderColor: 'bgLine',
+ };
/**
* Customer component config under test.
@@ -15,6 +48,7 @@ export const customerComponentConfig: ComponentConfig = {
paddingY: 1,
borderRadius: 400,
},
+ InputChip: inputChipStyleConfigResolver,
TextInput: {
borderRadius: 400,
},
@@ -32,7 +66,17 @@ export const customerComponentConfig: ComponentConfig = {
Select: {
borderRadius: 400,
},
+ SelectChip: selectChipStyleConfigResolver,
+ TabbedChips: {
+ TabComponent: CustomTabComponent,
+ } satisfies ConfigResolver,
DateInput: {
borderRadius: 400,
},
+ CheckboxCell: {
+ borderRadius: 400,
+ },
+ RadioCell: {
+ borderRadius: 400,
+ },
};
diff --git a/apps/expo-app/src/playground/customerComponentConfig/retailCDSTheme.ts b/apps/expo-app/src/playground/customerComponentConfig/retailCDSTheme.ts
index d5b3e85ff6..3735be69d5 100644
--- a/apps/expo-app/src/playground/customerComponentConfig/retailCDSTheme.ts
+++ b/apps/expo-app/src/playground/customerComponentConfig/retailCDSTheme.ts
@@ -370,7 +370,7 @@ export const retailCDSTheme: ThemeConfig = {
bgPositiveWash: `rgb(${darkSpectrum.green0})`,
bgWarning: `rgb(${darkSpectrum.orange60})`,
bgWarningWash: `rgb(${darkSpectrum.orange0})`,
- bgLine: 'rgba(138,145,158,0.1)',
+ bgLine: 'rgba(138,145,158,0.2)',
bgLineHeavy: 'rgba(138,145,158,0.66)',
bgLineInverse: `rgb(${darkSpectrum.gray0})`,
bgLinePrimary: `rgb(${darkSpectrum.blue70})`,
diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md
index bbbefd1fcb..cfc18c80cb 100644
--- a/packages/common/CHANGELOG.md
+++ b/packages/common/CHANGELOG.md
@@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.
+## 9.16.0 (8/13/2026 PST)
+
+#### 🚀 Updates
+
+- Add utility for SelectChip to share across packages. [[#843](https://github.com/coinbase/cds/pull/843)]
+
## 9.15.0 ((8/11/2026, 12:23 PM PST))
This is an artificial version bump with no new change.
diff --git a/packages/common/package.json b/packages/common/package.json
index 69f4cdb9c6..ca8a6356d4 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-common",
- "version": "9.15.0",
+ "version": "9.16.0",
"description": "Coinbase Design System - Common",
"repository": {
"type": "git",
diff --git a/packages/common/src/chips/__tests__/getSelectChipActive.test.ts b/packages/common/src/chips/__tests__/getSelectChipActive.test.ts
new file mode 100644
index 0000000000..faab67169c
--- /dev/null
+++ b/packages/common/src/chips/__tests__/getSelectChipActive.test.ts
@@ -0,0 +1,32 @@
+import { getSelectChipActive, getSelectChipHasValue } from '../getSelectChipActive';
+
+describe('getSelectChipHasValue', () => {
+ it('returns false for null, undefined, and empty arrays', () => {
+ expect(getSelectChipHasValue(null)).toBe(false);
+ expect(getSelectChipHasValue(undefined)).toBe(false);
+ expect(getSelectChipHasValue([])).toBe(false);
+ });
+
+ it('returns true for selected single and multi values', () => {
+ expect(getSelectChipHasValue('option1')).toBe(true);
+ expect(getSelectChipHasValue(['option1'])).toBe(true);
+ });
+});
+
+describe('getSelectChipActive', () => {
+ it('defaults to selection state when active is omitted and legacy invert props are unset', () => {
+ expect(getSelectChipActive(undefined, null)).toBe(false);
+ expect(getSelectChipActive(undefined, 'option1')).toBe(true);
+ });
+
+ it('respects explicit active overrides', () => {
+ expect(getSelectChipActive(false, 'option1')).toBe(false);
+ expect(getSelectChipActive(true, null)).toBe(true);
+ });
+
+ it('does not default active from selection when legacy invert props are set', () => {
+ expect(getSelectChipActive(undefined, 'option1', false)).toBe(false);
+ expect(getSelectChipActive(undefined, 'option1', true)).toBe(false);
+ expect(getSelectChipActive(undefined, null, true)).toBe(false);
+ });
+});
diff --git a/packages/common/src/chips/getSelectChipActive.ts b/packages/common/src/chips/getSelectChipActive.ts
new file mode 100644
index 0000000000..d476589869
--- /dev/null
+++ b/packages/common/src/chips/getSelectChipActive.ts
@@ -0,0 +1,30 @@
+/**
+ * SelectChip selection: a single option, a multi-select list, or empty.
+ */
+type SelectChipValue = string | readonly string[] | null | undefined;
+
+/**
+ * Whether a SelectChip value represents a current selection.
+ */
+export function getSelectChipHasValue(value: SelectChipValue): boolean {
+ return value != null && !(Array.isArray(value) && value.length === 0);
+}
+
+/**
+ * Resolves SelectChip `active` from an explicit prop, legacy invert props, or the current selection.
+ * Explicit `active={false}` stays inactive even when a value is selected.
+ * When `active` is omitted and neither legacy invert prop is set, defaults to whether a value is selected.
+ */
+export function getSelectChipActive(
+ active: boolean | undefined,
+ value: SelectChipValue,
+ invertColorScheme?: boolean,
+ inverted?: boolean,
+): boolean {
+ return (
+ active ??
+ (invertColorScheme === undefined && inverted === undefined
+ ? getSelectChipHasValue(value)
+ : false)
+ );
+}
diff --git a/packages/mcp-server/CHANGELOG.md b/packages/mcp-server/CHANGELOG.md
index ad839782c3..5e0207de6d 100644
--- a/packages/mcp-server/CHANGELOG.md
+++ b/packages/mcp-server/CHANGELOG.md
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
+## 9.16.0 ((8/13/2026, 01:04 PM PST))
+
+This is an artificial version bump with no new change.
+
## 9.15.0 ((8/11/2026, 12:23 PM PST))
This is an artificial version bump with no new change.
diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json
index b6fa61a208..760dc51d2c 100644
--- a/packages/mcp-server/package.json
+++ b/packages/mcp-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-mcp-server",
- "version": "9.15.0",
+ "version": "9.16.0",
"description": "Coinbase Design System - MCP Server",
"repository": {
"type": "git",
diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md
index 425e065004..c929a03377 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.
+## 9.16.0 (8/13/2026 PST)
+
+#### 🚀 Updates
+
+- Deprecate invertColorScheme in favor of new, canoncical active prop for the Chip family of props. [[#843](https://github.com/coinbase/cds/pull/843)]
+
## 9.15.0 (8/11/2026 PST)
#### 🚀 Updates
diff --git a/packages/mobile/package.json b/packages/mobile/package.json
index b37bbeab7c..4a76537454 100644
--- a/packages/mobile/package.json
+++ b/packages/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cds-mobile",
- "version": "9.15.0",
+ "version": "9.16.0",
"description": "Coinbase Design System - Mobile",
"repository": {
"type": "git",
diff --git a/packages/mobile/src/alpha/select-chip/SelectChip.tsx b/packages/mobile/src/alpha/select-chip/SelectChip.tsx
index d72928581f..112a0a714d 100644
--- a/packages/mobile/src/alpha/select-chip/SelectChip.tsx
+++ b/packages/mobile/src/alpha/select-chip/SelectChip.tsx
@@ -1,23 +1,19 @@
-import React, { memo, useCallback } from 'react';
+import React, { memo, useMemo } from 'react';
+import { getSelectChipActive } from '@coinbase/cds-common/chips/getSelectChipActive';
-import type { ChipBaseProps } from '../../chips/ChipProps';
import { useComponentConfig } from '../../hooks/useComponentConfig';
import { Select, type SelectRef } from '../select/Select';
import type { SelectControlProps, SelectProps, SelectType } from '../select/types';
-import { SelectChipControl } from './SelectChipControl';
+import {
+ SelectChipControl,
+ type SelectChipControlChipProps,
+ type SelectChipControlProps,
+} from './SelectChipControl';
-export type SelectChipBaseProps = Pick<
- ChipBaseProps,
- 'invertColorScheme' | 'numberOfLines' | 'maxWidth' | 'size' | 'compact'
-> & {
- /**
- * Override the displayed value in the chip control.
- * Useful for avoiding truncation, especially in multi-select scenarios where multiple option labels might be too long to display.
- * When provided, this value takes precedence over the default label generation.
- */
- displayValue?: React.ReactNode;
-};
+// TODO(CDS-2544): SelectChipBaseProps should also compose SelectBaseProps.
+// https://linear.app/coinbase/issue/CDS-2544/selectchipbaseprops-should-compose-selectbaseprops
+export type SelectChipBaseProps = Pick;
export type SelectChipProps<
Type extends SelectType = 'single',
@@ -25,15 +21,39 @@ export type SelectChipProps<
> = SelectChipBaseProps &
Omit<
SelectProps,
- | 'SelectControlComponent'
- | 'helperText'
- | 'labelVariant'
- | 'variant'
- | 'maxWidth'
- | 'size'
- | 'compact'
+ | 'SelectControlComponent' // fixed to SelectChipControl
+ | 'helperText' // not supported
+ | 'labelVariant' // not supported
+ | 'variant' // not supported
+ | 'maxWidth' // chip-owned
+ | 'size' // ChipSize, not SelectSize
+ | 'compact' // chip-owned
+ | 'bordered' // chip-owned
+ | 'borderWidth' // chip-owned
+ | 'borderColor' // chip-owned
+ | 'borderRadius' // chip-owned
>;
+/**
+ * Creates a wrapper component that injects chip-specific props into SelectChipControl.
+ * Select only forwards generic control props to `SelectControlComponent`; chip styling
+ * and `active` are owned by SelectChip and must be closed over here.
+ */
+function createSelectChipControlWrapper<
+ Type extends SelectType,
+ SelectOptionValue extends string = string,
+>(chipProps: SelectChipBaseProps): React.FC> {
+ return memo((controlProps: SelectControlProps) => {
+ // Chip props are spread last so they win at runtime; the cast bridges overlapping keys
+ // (`size`, borders) where SelectControlProps and SelectChipBaseProps use different types.
+ return (
+ )}
+ />
+ );
+ });
+}
+
/**
* Chip-styled Select control built on top of the Alpha Select.
* Supports both single and multi selection via Select's `type` prop.
@@ -45,34 +65,85 @@ const SelectChipComponent = memo(
}: SelectChipProps & {
ref?: React.Ref;
}) => {
- const mergedProps = useComponentConfig('SelectChip', _props);
- const { invertColorScheme, numberOfLines, maxWidth, displayValue, size, compact, ...props } =
- mergedProps;
- // Select doesn't pass the chip-specific props (size/compact/displayValue/etc.) down to the
- // control, so they're injected here. They're listed AFTER the `controlProps` spread so the
- // chip-level values win — matching the web SelectChip wrapper.
- const SelectChipControlComponent = useCallback(
- (controlProps: SelectControlProps) => {
- return (
-
- );
- },
- [displayValue, invertColorScheme, maxWidth, numberOfLines, size, compact],
+ // Resolve `active` from instance props before config merge so state-aware resolvers
+ // (active vs inactive border styling) see the final selection state. `value` and
+ // legacy invert props are pulled out with `active` and passed back explicitly because
+ // they feed that resolution and must remain available to the config resolver input.
+ const { active: activeProp, invertColorScheme, inverted, value, ...restProps } = _props;
+ const resolvedActive = getSelectChipActive(activeProp, value, invertColorScheme, inverted);
+ const mergedProps = useComponentConfig('SelectChip', {
+ ...restProps,
+ value,
+ active: resolvedActive,
+ invertColorScheme,
+ inverted,
+ });
+ // SelectChip composes Select + MediaChip; peel chip props off merged props so the rest
+ // can flow to `