From e1950b9c2fafaee5edea194f9b6bbc365d6f094b Mon Sep 17 00:00:00 2001 From: Stephen Vergara Date: Thu, 9 Apr 2026 14:06:01 -0700 Subject: [PATCH 1/5] feat(IconButton): expose icon glyph styles via styles and classNames props Adds `styles.icon` (and `classNames.icon` on web) to IconButton on both mobile and web, forwarding them to the inner Icon component so consumers can customize icon glyph appearance (e.g. color, font size). Fixes CDS-1134 Co-Authored-By: Claude Sonnet 4.6 --- packages/mobile/src/buttons/IconButton.tsx | 15 ++++++++++++- .../__stories__/IconButton.stories.tsx | 11 ++++++++++ .../src/buttons/__tests__/IconButton.test.tsx | 13 +++++++++++ packages/web/src/buttons/IconButton.tsx | 21 +++++++++++++++++- .../__stories__/IconButton.stories.tsx | 19 ++++++++++++++++ .../src/buttons/__tests__/IconButton.test.tsx | 22 +++++++++++++++++++ 6 files changed, 99 insertions(+), 2 deletions(-) diff --git a/packages/mobile/src/buttons/IconButton.tsx b/packages/mobile/src/buttons/IconButton.tsx index df85653647..d15a201f2f 100644 --- a/packages/mobile/src/buttons/IconButton.tsx +++ b/packages/mobile/src/buttons/IconButton.tsx @@ -1,5 +1,11 @@ import { forwardRef, memo, useCallback, useMemo } from 'react'; -import { type PressableStateCallbackType, type View, type ViewStyle } from 'react-native'; +import { + type PressableStateCallbackType, + type StyleProp, + type TextStyle, + type View, + type ViewStyle, +} from 'react-native'; import { transparentVariants, variants } from '@coinbase/cds-common/tokens/button'; import { interactableHeight } from '@coinbase/cds-common/tokens/interactableHeight'; import type { @@ -39,6 +45,11 @@ export type IconButtonBaseProps = SharedProps & * @default primary */ variant?: IconButtonVariant; + /** Custom styles for individual elements of the IconButton component */ + styles?: { + /** Inner icon glyph Text element */ + icon?: StyleProp; + }; }; export type IconButtonProps = IconButtonBaseProps; @@ -65,6 +76,7 @@ export const IconButton = memo( loading, progressCircleSize, style, + styles, accessibilityHint, accessibilityLabel, ...props @@ -134,6 +146,7 @@ export const IconButton = memo( name={name} size={iconSize} style={sizingStyle} + styles={{ icon: styles?.icon }} /> )} diff --git a/packages/mobile/src/buttons/__stories__/IconButton.stories.tsx b/packages/mobile/src/buttons/__stories__/IconButton.stories.tsx index d54c910e73..09afbb4781 100644 --- a/packages/mobile/src/buttons/__stories__/IconButton.stories.tsx +++ b/packages/mobile/src/buttons/__stories__/IconButton.stories.tsx @@ -129,6 +129,17 @@ const IconButtonScreen = () => { + + + + Custom color via styles.icon + + + diff --git a/packages/mobile/src/buttons/__tests__/IconButton.test.tsx b/packages/mobile/src/buttons/__tests__/IconButton.test.tsx index 37f6e53346..5c95ffc362 100644 --- a/packages/mobile/src/buttons/__tests__/IconButton.test.tsx +++ b/packages/mobile/src/buttons/__tests__/IconButton.test.tsx @@ -134,4 +134,17 @@ describe('IconButton', () => { // Should be "loading" when no accessibility label is provided expect(button.props.accessibilityLabel).toBe(', loading'); }); + + it('applies styles.icon to the inner icon glyph', () => { + const customIconStyle = { fontSize: 99 }; + const { UNSAFE_getAllByType } = render( + + + , + ); + + const [iconText] = UNSAFE_getAllByType(Text); + // Mobile Icon builds iconStyle as [baseStyles, styles?.icon] + expect(iconText.props.style[1]).toEqual(customIconStyle); + }); }); diff --git a/packages/web/src/buttons/IconButton.tsx b/packages/web/src/buttons/IconButton.tsx index 81f54021db..f8a51c3192 100644 --- a/packages/web/src/buttons/IconButton.tsx +++ b/packages/web/src/buttons/IconButton.tsx @@ -36,6 +36,16 @@ export type IconButtonBaseProps = Polymorphic.ExtendableProps< * @default primary */ variant?: IconButtonVariant; + /** Custom inline styles for individual elements of the IconButton component */ + styles?: { + /** Inner icon glyph element */ + icon?: React.CSSProperties; + }; + /** Custom class names for individual elements of the IconButton component */ + classNames?: { + /** Inner icon glyph element */ + icon?: string; + }; } >; @@ -94,6 +104,8 @@ export const IconButton: IconButtonComponent = memo( progressCircleSize, accessibilityLabel, accessibilityHint, + styles, + classNames, ...props } = mergedProps; const Component = (as ?? iconButtonDefaultElement) satisfies React.ElementType; @@ -148,7 +160,14 @@ export const IconButton: IconButtonComponent = memo( weight="thin" /> ) : ( - + )} ); diff --git a/packages/web/src/buttons/__stories__/IconButton.stories.tsx b/packages/web/src/buttons/__stories__/IconButton.stories.tsx index ecf888ac70..c5c818f449 100644 --- a/packages/web/src/buttons/__stories__/IconButton.stories.tsx +++ b/packages/web/src/buttons/__stories__/IconButton.stories.tsx @@ -91,6 +91,25 @@ export const Default = () => ( style={{ backgroundColor: 'red', transform: 'scale(0.5)' }} /> + + Icon Glyph Styles + + + Custom color via styles.icon + + + + Custom class via classNames.icon + + Variants {variants.map((variant, index) => ( diff --git a/packages/web/src/buttons/__tests__/IconButton.test.tsx b/packages/web/src/buttons/__tests__/IconButton.test.tsx index 4b6992441a..96f2371f46 100644 --- a/packages/web/src/buttons/__tests__/IconButton.test.tsx +++ b/packages/web/src/buttons/__tests__/IconButton.test.tsx @@ -223,4 +223,26 @@ describe('IconButton', () => { expect(button).toHaveAttribute('data-variant', 'secondary'); expect(button).toHaveAttribute('data-compact', 'true'); }); + + describe('styles and classNames', () => { + it('applies styles.icon to the inner icon glyph element', () => { + render( + + + , + ); + + expect(screen.getByTestId('icon-base-glyph')).toHaveStyle({ fontSize: '99px' }); + }); + + it('applies classNames.icon to the inner icon glyph element', () => { + render( + + + , + ); + + expect(screen.getByTestId('icon-base-glyph')).toHaveClass('custom-icon-class'); + }); + }); }); From 4e53b1b96580a3e0cac6511868d46d22fd60718d Mon Sep 17 00:00:00 2001 From: Stephen Vergara Date: Thu, 9 Apr 2026 14:36:30 -0700 Subject: [PATCH 2/5] feat(IconButton): demonstrate classNames.icon with real Linaria css class in Storybook Co-Authored-By: Claude Sonnet 4.6 --- .../src/buttons/__stories__/IconButton.stories.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/web/src/buttons/__stories__/IconButton.stories.tsx b/packages/web/src/buttons/__stories__/IconButton.stories.tsx index c5c818f449..81289d64fd 100644 --- a/packages/web/src/buttons/__stories__/IconButton.stories.tsx +++ b/packages/web/src/buttons/__stories__/IconButton.stories.tsx @@ -1,10 +1,16 @@ import React from 'react'; import { names } from '@coinbase/cds-icons/names'; +import { css } from '@linaria/core'; import { HStack, VStack } from '../../layout'; import { Text } from '../../typography/Text'; import { IconButton, type IconButtonBaseProps } from '../IconButton'; +const rotatedIconCss = css` + transform: rotate(45deg); + transition: transform 200ms ease-in-out; +`; + const iconName = 'arrowsHorizontal'; const accessibilityLabel = 'Horizontal arrows'; @@ -103,11 +109,11 @@ export const Default = () => ( - Custom class via classNames.icon + Rotated icon via classNames.icon From d26f66b939f8f5e470acc78c69af73a1aeb089af Mon Sep 17 00:00:00 2001 From: Stephen Vergara Date: Thu, 9 Apr 2026 14:53:22 -0700 Subject: [PATCH 3/5] feat(IconButton): add static classNames, StylesAndClassNames type, and docs styles tab - Introduce iconButtonClassNames with root and icon selectors - Replace manual styles/classNames types with StylesAndClassNames utility - Apply static class names to Pressable root and Icon glyph - Add static classNames test - Add _webStyles.mdx and _mobileStyles.mdx doc pages - Wire styles tabs into index.mdx Co-Authored-By: Claude Sonnet 4.6 --- .../inputs/IconButton/_mobileStyles.mdx | 7 +++++ .../inputs/IconButton/_webStyles.mdx | 21 +++++++++++++ .../components/inputs/IconButton/index.mdx | 8 ++++- packages/web/src/buttons/IconButton.tsx | 30 ++++++++++--------- .../src/buttons/__tests__/IconButton.test.tsx | 16 +++++++++- 5 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 apps/docs/docs/components/inputs/IconButton/_mobileStyles.mdx create mode 100644 apps/docs/docs/components/inputs/IconButton/_webStyles.mdx diff --git a/apps/docs/docs/components/inputs/IconButton/_mobileStyles.mdx b/apps/docs/docs/components/inputs/IconButton/_mobileStyles.mdx new file mode 100644 index 0000000000..bad1aed150 --- /dev/null +++ b/apps/docs/docs/components/inputs/IconButton/_mobileStyles.mdx @@ -0,0 +1,7 @@ +import { ComponentStylesTable } from '@site/src/components/page/ComponentStylesTable'; + +import mobileStylesData from ':docgen/mobile/buttons/IconButton/styles-data'; + +## Selectors + + diff --git a/apps/docs/docs/components/inputs/IconButton/_webStyles.mdx b/apps/docs/docs/components/inputs/IconButton/_webStyles.mdx new file mode 100644 index 0000000000..448a432203 --- /dev/null +++ b/apps/docs/docs/components/inputs/IconButton/_webStyles.mdx @@ -0,0 +1,21 @@ +import { ComponentStylesTable } from '@site/src/components/page/ComponentStylesTable'; +import { StylesExplorer } from '@site/src/components/page/StylesExplorer'; +import { IconButton } from '@coinbase/cds-web/buttons'; + +import webStylesData from ':docgen/web/buttons/IconButton/styles-data'; + +## Explorer + + + {(classNames) => ( + + )} + + +## Selectors + + diff --git a/apps/docs/docs/components/inputs/IconButton/index.mdx b/apps/docs/docs/components/inputs/IconButton/index.mdx index af5a7562fd..3970a9d62a 100644 --- a/apps/docs/docs/components/inputs/IconButton/index.mdx +++ b/apps/docs/docs/components/inputs/IconButton/index.mdx @@ -16,6 +16,8 @@ import mobilePropsToc from ':docgen/mobile/buttons/IconButton/toc-props'; import WebPropsTable from './_webPropsTable.mdx'; import MobilePropsTable from './_mobilePropsTable.mdx'; +import WebStyles, { toc as webStylesToc } from './_webStyles.mdx'; +import MobileStyles, { toc as mobileStylesToc } from './_mobileStyles.mdx'; import MobileExamples, { toc as mobileExamplesToc } from './_mobileExamples.mdx'; import WebExamples, { toc as webExamplesToc } from './_webExamples.mdx'; @@ -27,18 +29,22 @@ import mobileMetadata from './mobileMetadata.json'; title="IconButton" description="A button that renders an official icon as content instead of text." webMetadata={webMetadata} - mobileMetadata={mobileMetadata} + mobileMetadata={mobileMetadata} banner={} /> } + webStyles={} webExamples={} mobilePropsTable={} + mobileStyles={} mobileExamples={} webExamplesToc={webExamplesToc} mobileExamplesToc={mobileExamplesToc} webPropsToc={webPropsToc} + webStylesToc={webStylesToc} mobilePropsToc={mobilePropsToc} + mobileStylesToc={mobileStylesToc} /> diff --git a/packages/web/src/buttons/IconButton.tsx b/packages/web/src/buttons/IconButton.tsx index f8a51c3192..7cd47f6894 100644 --- a/packages/web/src/buttons/IconButton.tsx +++ b/packages/web/src/buttons/IconButton.tsx @@ -6,6 +6,7 @@ import { css } from '@linaria/core'; import type { Polymorphic } from '../core/polymorphism'; import { cx } from '../cx'; import { useComponentConfig } from '../hooks/useComponentConfig'; +import type { StylesAndClassNames } from '../types'; import { useTheme } from '../hooks/useTheme'; import { Icon } from '../icons/Icon'; import { Pressable, type PressableBaseProps } from '../system/Pressable'; @@ -13,7 +14,16 @@ import { ProgressCircle } from '../visualizations/ProgressCircle'; import { type ButtonBaseProps } from './Button'; -const COMPONENT_STATIC_CLASSNAME = 'cds-IconButton'; +/** + * Static class names for IconButton component parts. + * Use these selectors to target specific elements with CSS. + */ +export const iconButtonClassNames = { + /** Root button element */ + root: 'cds-IconButton', + /** Inner icon glyph element */ + icon: 'cds-IconButton-icon', +} as const; export const iconButtonDefaultElement = 'button'; @@ -36,23 +46,14 @@ export type IconButtonBaseProps = Polymorphic.ExtendableProps< * @default primary */ variant?: IconButtonVariant; - /** Custom inline styles for individual elements of the IconButton component */ - styles?: { - /** Inner icon glyph element */ - icon?: React.CSSProperties; - }; - /** Custom class names for individual elements of the IconButton component */ - classNames?: { - /** Inner icon glyph element */ - icon?: string; - }; } >; export type IconButtonProps = Polymorphic.Props< AsComponent, IconButtonBaseProps ->; +> & + StylesAndClassNames; type IconButtonComponent = (( props: IconButtonProps, @@ -132,10 +133,11 @@ export const IconButton: IconButtonComponent = memo( borderRadius={borderRadius} borderWidth={borderWidth} className={cx( - COMPONENT_STATIC_CLASSNAME, + iconButtonClassNames.root, flush && flushSpaceCss, flush === 'start' && flushStartCss, flush === 'end' && flushEndCss, + classNames?.root, className, )} color={colorValue} @@ -162,7 +164,7 @@ export const IconButton: IconButtonComponent = memo( ) : ( { expect(button).toHaveAttribute('data-compact', 'true'); }); + describe('static classNames', () => { + it('applies static class names to component elements', () => { + render( + + + , + ); + + const button = screen.getByRole('button'); + expect(button).toHaveClass(iconButtonClassNames.root); + expect(screen.getByTestId('icon-base-glyph')).toHaveClass(iconButtonClassNames.icon); + }); + }); + describe('styles and classNames', () => { it('applies styles.icon to the inner icon glyph element', () => { render( From a36022daf37f8371f47f779c5e7641c68ce944d6 Mon Sep 17 00:00:00 2001 From: Stephen Vergara Date: Fri, 10 Apr 2026 11:48:27 -0700 Subject: [PATCH 4/5] feat(IconButton): add rotate transform example to Icon Glyph Styles story Co-Authored-By: Claude Sonnet 4.6 --- .../__stories__/IconButton.stories.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/mobile/src/buttons/__stories__/IconButton.stories.tsx b/packages/mobile/src/buttons/__stories__/IconButton.stories.tsx index 09afbb4781..1864759b37 100644 --- a/packages/mobile/src/buttons/__stories__/IconButton.stories.tsx +++ b/packages/mobile/src/buttons/__stories__/IconButton.stories.tsx @@ -130,14 +130,24 @@ const IconButtonScreen = () => { - - - Custom color via styles.icon - + + + + Custom color via styles.icon + + + + Rotated icon via styles.icon + + From 49f3d14b26c61c23d972a8d0306000f32d76d798 Mon Sep 17 00:00:00 2001 From: Stephen Vergara Date: Thu, 16 Apr 2026 14:16:07 -0700 Subject: [PATCH 5/5] fix lint --- packages/web/src/buttons/IconButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/buttons/IconButton.tsx b/packages/web/src/buttons/IconButton.tsx index 7cd47f6894..f2ebc855e1 100644 --- a/packages/web/src/buttons/IconButton.tsx +++ b/packages/web/src/buttons/IconButton.tsx @@ -6,10 +6,10 @@ import { css } from '@linaria/core'; import type { Polymorphic } from '../core/polymorphism'; import { cx } from '../cx'; import { useComponentConfig } from '../hooks/useComponentConfig'; -import type { StylesAndClassNames } from '../types'; import { useTheme } from '../hooks/useTheme'; import { Icon } from '../icons/Icon'; import { Pressable, type PressableBaseProps } from '../system/Pressable'; +import type { StylesAndClassNames } from '../types'; import { ProgressCircle } from '../visualizations/ProgressCircle'; import { type ButtonBaseProps } from './Button';