diff --git a/apps/docs/content/docs/components/actions/button/index.mdx b/apps/docs/content/docs/components/actions/button/index.mdx
index 1f635eda..ce85f857 100644
--- a/apps/docs/content/docs/components/actions/button/index.mdx
+++ b/apps/docs/content/docs/components/actions/button/index.mdx
@@ -20,7 +20,7 @@ Keep related controls at the same size.
## Appearance
@@ -58,9 +58,10 @@ primary action at the end of a narrow form or panel.
## Icons
-Use `startIcon` or `endIcon` with a visible label. `Button` supplies the icon size, so the nested
-`Icon` does not need its own `size` prop. Use [`IconButton`](/components/actions/icon-button) only
-when the icon is familiar without a text label.
+Use `startIcon` or `endIcon` with a visible label. `Button` sizes nested icons at 16px for both
+`small` and `medium`, so the `Icon` needs no `size` prop of its own — pass one only to override it
+deliberately. Use [`IconButton`](/components/actions/icon-button) only when the icon is familiar
+without a text label.
- }>Add item
- }>
+ }>Add item
+ }>
Continue
diff --git a/apps/docs/src/examples/button/sizes.tsx b/apps/docs/src/examples/button/sizes.tsx
index cb19da75..953ca638 100644
--- a/apps/docs/src/examples/button/sizes.tsx
+++ b/apps/docs/src/examples/button/sizes.tsx
@@ -1,11 +1,16 @@
import { Box } from '@luke-ui/react/box';
import { Button } from '@luke-ui/react/button';
+import { Icon } from '@luke-ui/react/icon';
export default function Sizes() {
return (
-
-
+ }>
+ Edit profile
+
+ }>
+ Edit profile
+
);
}
diff --git a/packages/@luke-ui/react/src/button/button.browser.test.tsx b/packages/@luke-ui/react/src/button/button.browser.test.tsx
new file mode 100644
index 00000000..8e32b2c4
--- /dev/null
+++ b/packages/@luke-ui/react/src/button/button.browser.test.tsx
@@ -0,0 +1,96 @@
+import '../../dist/themes/tactile.css';
+import '../stylesheet.css.js';
+import { act } from 'react';
+import type { ReactNode } from 'react';
+import type { Root } from 'react-dom/client';
+import { createRoot } from 'react-dom/client';
+import { afterEach, expect, test } from 'vite-plus/test';
+import spritesheetHref from '../../dist/spritesheet.svg?url';
+import { IconButton } from '../icon-button/index.js';
+import { Icon, IconSpritesheetProvider } from '../icon/index.js';
+import { themeRootClassName } from '../theme/index.js';
+import { tactileThemeClassName } from '../themes/index.js';
+import { Button } from './index.js';
+
+const mounted: Array<{ container: HTMLElement; root: Root }> = [];
+
+afterEach(() => {
+ for (const { container, root } of mounted) {
+ act(() => root.unmount());
+ container.remove();
+ }
+ mounted.length = 0;
+});
+
+test('renders a 16px start icon in a medium button', () => {
+ const { container } = mountFixture(
+ }>
+ Add
+ ,
+ );
+
+ const icon = container.querySelector('svg');
+ if (!(icon instanceof SVGElement)) throw new Error('Expected icon.');
+
+ const styles = getComputedStyle(icon);
+ expect(styles.inlineSize).toBe('16px');
+ expect(styles.blockSize).toBe('16px');
+});
+
+test('renders a 16px start icon in a small button', () => {
+ const { container } = mountFixture(
+ }>
+ Add
+ ,
+ );
+
+ const icon = container.querySelector('svg');
+ if (!(icon instanceof SVGElement)) throw new Error('Expected icon.');
+
+ const styles = getComputedStyle(icon);
+ expect(styles.inlineSize).toBe('16px');
+ expect(styles.blockSize).toBe('16px');
+});
+
+test('renders a 16px icon in a medium icon button while the button itself stays 40px', () => {
+ const { container } = mountFixture();
+
+ const button = container.firstElementChild;
+ if (!(button instanceof HTMLElement)) throw new Error('Expected button.');
+ expect(getComputedStyle(button).inlineSize).toBe('40px');
+
+ const icon = button.querySelector('svg');
+ if (!(icon instanceof SVGElement)) throw new Error('Expected icon.');
+
+ const styles = getComputedStyle(icon);
+ expect(styles.inlineSize).toBe('16px');
+ expect(styles.blockSize).toBe('16px');
+});
+
+test('an explicit icon size overrides the button-provided icon size', () => {
+ const { container } = mountFixture(
+ }>
+ Add
+ ,
+ );
+
+ const icon = container.querySelector('svg');
+ if (!(icon instanceof SVGElement)) throw new Error('Expected icon.');
+
+ const styles = getComputedStyle(icon);
+ expect(styles.inlineSize).toBe('32px');
+ expect(styles.blockSize).toBe('32px');
+});
+
+function mountFixture(node: ReactNode) {
+ const container = document.body.appendChild(document.createElement('div'));
+ container.className = `${themeRootClassName} ${tactileThemeClassName}`;
+ const root = createRoot(container);
+ mounted.push({ container, root });
+
+ act(() => {
+ root.render({node});
+ });
+
+ return { container };
+}
diff --git a/packages/@luke-ui/react/src/button/primitive/index.tsx b/packages/@luke-ui/react/src/button/primitive/index.tsx
index 35e6aff2..6c377344 100644
--- a/packages/@luke-ui/react/src/button/primitive/index.tsx
+++ b/packages/@luke-ui/react/src/button/primitive/index.tsx
@@ -55,10 +55,9 @@ export function Button(props: ButtonProps): JSX.Element {
tone = 'neutral',
...restProps
} = props;
- const iconSize = BUTTON_ICON_SIZE[size];
return (
-
+
{
diff --git a/packages/@luke-ui/react/src/sizing/button-sizing.ts b/packages/@luke-ui/react/src/sizing/button-sizing.ts
index 3bb72f70..26bc0256 100644
--- a/packages/@luke-ui/react/src/sizing/button-sizing.ts
+++ b/packages/@luke-ui/react/src/sizing/button-sizing.ts
@@ -1,7 +1,4 @@
import type { IconSize } from '../types/icon-size.js';
-/** Maps button size to the appropriate icon/spinner size. */
-export const BUTTON_ICON_SIZE: Record<'medium' | 'small', IconSize> = {
- medium: 'medium',
- small: 'xsmall',
-};
+/** Icon/spinner size used inside buttons. Both button sizes take a 16px icon. */
+export const BUTTON_ICON_SIZE: IconSize = 'xsmall';