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/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..1864759b37 100644
--- a/packages/mobile/src/buttons/__stories__/IconButton.stories.tsx
+++ b/packages/mobile/src/buttons/__stories__/IconButton.stories.tsx
@@ -129,6 +129,27 @@ const IconButtonScreen = () => {
+
+
+
+
+ Custom color via styles.icon
+
+
+
+ Rotated icon via styles.icon
+
+
+
+
diff --git a/packages/mobile/src/buttons/__tests__/IconButton.test.tsx b/packages/mobile/src/buttons/__tests__/IconButton.test.tsx
index 8ed8bf4abf..9ac869fe85 100644
--- a/packages/mobile/src/buttons/__tests__/IconButton.test.tsx
+++ b/packages/mobile/src/buttons/__tests__/IconButton.test.tsx
@@ -142,4 +142,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..f2ebc855e1 100644
--- a/packages/web/src/buttons/IconButton.tsx
+++ b/packages/web/src/buttons/IconButton.tsx
@@ -9,11 +9,21 @@ import { useComponentConfig } from '../hooks/useComponentConfig';
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';
-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';
@@ -42,7 +52,8 @@ export type IconButtonBaseProps = Polymorphic.ExtendableProps<
export type IconButtonProps = Polymorphic.Props<
AsComponent,
IconButtonBaseProps
->;
+> &
+ StylesAndClassNames;
type IconButtonComponent = ((
props: IconButtonProps,
@@ -94,6 +105,8 @@ export const IconButton: IconButtonComponent = memo(
progressCircleSize,
accessibilityLabel,
accessibilityHint,
+ styles,
+ classNames,
...props
} = mergedProps;
const Component = (as ?? iconButtonDefaultElement) satisfies React.ElementType;
@@ -120,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}
@@ -148,7 +162,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..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';
@@ -91,6 +97,25 @@ export const Default = () => (
style={{ backgroundColor: 'red', transform: 'scale(0.5)' }}
/>
+
+ Icon Glyph Styles
+
+
+ Custom color via styles.icon
+
+
+
+ Rotated icon 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..3c26a7d0c7 100644
--- a/packages/web/src/buttons/__tests__/IconButton.test.tsx
+++ b/packages/web/src/buttons/__tests__/IconButton.test.tsx
@@ -4,7 +4,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { defaultTheme } from '../../themes/defaultTheme';
import { DefaultThemeProvider } from '../../utils/test';
-import { IconButton } from '../IconButton';
+import { IconButton, iconButtonClassNames } from '../IconButton';
const name = 'arrowsHorizontal';
@@ -223,4 +223,40 @@ describe('IconButton', () => {
expect(button).toHaveAttribute('data-variant', 'secondary');
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(
+
+
+ ,
+ );
+
+ 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');
+ });
+ });
});