diff --git a/.changeset/fair-oranges-leave.md b/.changeset/fair-oranges-leave.md
new file mode 100644
index 0000000000..43fd97f925
--- /dev/null
+++ b/.changeset/fair-oranges-leave.md
@@ -0,0 +1,5 @@
+---
+'@backstage/canon': minor
+---
+
+We added a new IconButton component with fixed sizes showcasing a single icon.
diff --git a/canon-docs/src/app/(docs)/components/icon-button/page.mdx b/canon-docs/src/app/(docs)/components/icon-button/page.mdx
new file mode 100644
index 0000000000..591ee321eb
--- /dev/null
+++ b/canon-docs/src/app/(docs)/components/icon-button/page.mdx
@@ -0,0 +1,111 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { Tabs } from '@/components/Tabs';
+import { CodeBlock } from '@/components/CodeBlock';
+import {
+ IconButtonPreview,
+ IconButtonSizes,
+ IconButtonDisabled,
+ IconButtonResponsive,
+} from '@/snippets/icon-button';
+import { iconButtonVariants } from '@/snippets/_snippets';
+import { iconButtonPropDefs } from '../../../../../../packages/canon/src/components/IconButton/IconButton.props';
+
+# Icon Button
+
+A button component with a single icon that can be used to trigger actions.
+
+}
+ code={`
+
+
+`}
+/>
+
+
+
+ Usage
+ Theming
+
+
+
+`}
+ />
+
+
+ We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with
+ your brand. For additional flexibility, you can use the provided class names for each element listed below.
+ `}
+ />
+
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Variants
+
+Here's a view when buttons have different variants.
+
+}
+ code={iconButtonVariants}
+/>
+
+### Sizes
+
+Here's a view when buttons have different sizes.
+
+}
+ code={`
+
+
+`}
+/>
+
+### Disabled
+
+Here's a view when buttons are disabled.
+
+}
+ code={``}
+/>
+
+### Responsive
+
+Here's a view when buttons are responsive.
+
+}
+ code={``}
+/>
diff --git a/canon-docs/src/components/PropsTable/PropsTable.tsx b/canon-docs/src/components/PropsTable/PropsTable.tsx
index aa6b9f296e..62917eaeed 100644
--- a/canon-docs/src/components/PropsTable/PropsTable.tsx
+++ b/canon-docs/src/components/PropsTable/PropsTable.tsx
@@ -19,10 +19,6 @@ export const PropsTable = >({
}) => {
const completeData = {
...data,
- children: {
- values: 'ReactNode',
- responsive: false,
- },
className: {
values: 'string',
responsive: false,
diff --git a/canon-docs/src/snippets/_snippets.ts b/canon-docs/src/snippets/_snippets.ts
index 75ff5056f4..00a9c1462d 100644
--- a/canon-docs/src/snippets/_snippets.ts
+++ b/canon-docs/src/snippets/_snippets.ts
@@ -38,7 +38,7 @@ export const grid = `import { Grid } from '@backstage/canon';
`;
-export const buttonVariants = `
+export const buttonVariants = `
@@ -48,7 +48,13 @@ export const buttonVariants = `
-
+
+`;
+
+export const iconButtonVariants = `
+
+
+
`;
export const flexFAQ1 = `
diff --git a/canon-docs/src/snippets/icon-button.tsx b/canon-docs/src/snippets/icon-button.tsx
new file mode 100644
index 0000000000..2079041456
--- /dev/null
+++ b/canon-docs/src/snippets/icon-button.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import { IconButton, Flex, ButtonProps, Text } from '../../../packages/canon';
+
+export const IconButtonPreview = () => {
+ return (
+
+
+
+
+ );
+};
+
+export const IconButtonSizes = () => {
+ return (
+
+
+
+
+ );
+};
+
+export const IconButtonDisabled = () => {
+ return ;
+};
+
+export const IconButtonResponsive = () => {
+ return (
+
+ );
+};
+
+export const IconButtonPlayground = () => {
+ const variants: string[] = ['primary', 'secondary'];
+
+ return (
+
+ {variants.map(variant => (
+
+ {variant}
+ {['small', 'medium'].map(size => (
+
+
+
+ ))}
+
+ ))}
+
+ );
+};
diff --git a/canon-docs/src/utils/data.ts b/canon-docs/src/utils/data.ts
index c454b76d71..6aa487d93a 100644
--- a/canon-docs/src/utils/data.ts
+++ b/canon-docs/src/utils/data.ts
@@ -87,6 +87,11 @@ export const components: Page[] = [
slug: 'icon',
status: 'alpha',
},
+ {
+ title: 'IconButton',
+ slug: 'icon-button',
+ status: 'alpha',
+ },
{
title: 'Input',
slug: 'input',
diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md
index ceb099f683..4d83c301e6 100644
--- a/packages/canon/report.api.md
+++ b/packages/canon/report.api.md
@@ -590,6 +590,40 @@ export type HeightProps = GetPropDefTypes;
// @public (undocumented)
export const Icon: (props: IconProps) => React_2.JSX.Element;
+// @public (undocumented)
+export const IconButton: React_2.ForwardRefExoticComponent<
+ IconButtonProps & React_2.RefAttributes
+>;
+
+// @public (undocumented)
+export type IconButtonOwnProps = GetPropDefTypes;
+
+// @public (undocumented)
+export const iconButtonPropDefs: {
+ variant: {
+ type: 'enum';
+ values: ('primary' | 'secondary')[];
+ className: string;
+ default: 'primary';
+ responsive: true;
+ };
+ size: {
+ type: 'enum';
+ values: ('small' | 'medium')[];
+ className: string;
+ default: 'medium';
+ responsive: true;
+ };
+};
+
+// @public
+export interface IconButtonProps
+ extends Omit, 'children'> {
+ icon: IconNames;
+ size?: IconButtonOwnProps['size'];
+ variant?: IconButtonOwnProps['variant'];
+}
+
// @public (undocumented)
export const IconContext: Context;
diff --git a/packages/canon/src/components/IconButton/IconButton.props.ts b/packages/canon/src/components/IconButton/IconButton.props.ts
new file mode 100644
index 0000000000..e584997ef5
--- /dev/null
+++ b/packages/canon/src/components/IconButton/IconButton.props.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { PropDef, GetPropDefTypes } from '../../props/prop-def';
+
+/** @public */
+export const iconButtonPropDefs = {
+ variant: {
+ type: 'enum',
+ values: ['primary', 'secondary'],
+ className: 'canon-Button--variant',
+ default: 'primary',
+ responsive: true,
+ },
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ className: 'canon-Button--size',
+ default: 'medium',
+ responsive: true,
+ },
+} satisfies {
+ variant: PropDef<'primary' | 'secondary'>;
+ size: PropDef<'small' | 'medium'>;
+};
+
+/** @public */
+export type IconButtonOwnProps = GetPropDefTypes;
diff --git a/packages/canon/src/components/IconButton/IconButton.stories.tsx b/packages/canon/src/components/IconButton/IconButton.stories.tsx
new file mode 100644
index 0000000000..74e058430c
--- /dev/null
+++ b/packages/canon/src/components/IconButton/IconButton.stories.tsx
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconButton } from './IconButton';
+import { Flex } from '../Flex';
+import { Text } from '../Text';
+import { IconButtonProps } from './types';
+
+const meta = {
+ title: 'Components/IconButton',
+ component: IconButton,
+ argTypes: {
+ size: {
+ control: 'select',
+ options: ['small', 'medium'],
+ },
+ variant: {
+ control: 'select',
+ options: ['primary', 'secondary'],
+ },
+ },
+ args: {
+ size: 'medium',
+ variant: 'primary',
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Variants: Story = {
+ args: {
+ icon: 'cloud',
+ },
+ parameters: {
+ argTypes: {
+ variant: {
+ control: false,
+ },
+ },
+ },
+ render: args => (
+
+
+
+
+ ),
+};
+
+export const Sizes: Story = {
+ args: {
+ icon: 'cloud',
+ },
+ render: args => (
+
+
+
+
+ ),
+};
+
+export const Disabled: Story = {
+ args: {
+ icon: 'cloud',
+ disabled: true,
+ },
+ render: args => (
+
+
+
+
+ ),
+};
+
+export const Responsive: Story = {
+ args: {
+ icon: 'cloud',
+ variant: {
+ initial: 'primary',
+ sm: 'secondary',
+ },
+ size: {
+ xs: 'small',
+ sm: 'medium',
+ },
+ },
+};
+
+const variants: string[] = ['primary', 'secondary'];
+
+export const Playground: Story = {
+ args: {
+ icon: 'cloud',
+ },
+ render: args => (
+
+ {variants.map(variant => (
+
+ {variant}
+ {['small', 'medium'].map(size => (
+
+
+
+
+
+ ))}
+
+ ))}
+
+ ),
+};
diff --git a/packages/canon/src/components/IconButton/IconButton.tsx b/packages/canon/src/components/IconButton/IconButton.tsx
new file mode 100644
index 0000000000..0f28ed963f
--- /dev/null
+++ b/packages/canon/src/components/IconButton/IconButton.tsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { forwardRef } from 'react';
+import { Icon } from '../Icon';
+import clsx from 'clsx';
+import { useResponsiveValue } from '../../hooks/useResponsiveValue';
+
+import type { IconButtonProps } from './types';
+
+/** @public */
+export const IconButton = forwardRef(
+ (props: IconButtonProps, ref) => {
+ const {
+ size = 'medium',
+ variant = 'primary',
+ icon,
+ className,
+ style,
+ ...rest
+ } = props;
+
+ const responsiveSize = useResponsiveValue(size);
+ const responsiveVariant = useResponsiveValue(variant);
+
+ return (
+
+ );
+ },
+);
+
+export default IconButton;
diff --git a/packages/canon/src/components/IconButton/index.tsx b/packages/canon/src/components/IconButton/index.tsx
new file mode 100644
index 0000000000..5f9943557e
--- /dev/null
+++ b/packages/canon/src/components/IconButton/index.tsx
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { IconButton } from './IconButton';
+export type { IconButtonProps } from './types';
+export { iconButtonPropDefs } from './IconButton.props';
+export type { IconButtonOwnProps } from './IconButton.props';
diff --git a/packages/canon/src/components/IconButton/styles.css b/packages/canon/src/components/IconButton/styles.css
new file mode 100644
index 0000000000..aa4c2d0523
--- /dev/null
+++ b/packages/canon/src/components/IconButton/styles.css
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.canon-IconButton {
+ border: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ user-select: none;
+ font-family: var(--canon-font-regular);
+ font-weight: var(--canon-font-weight-bold);
+ padding: 0;
+ cursor: pointer;
+ border-radius: var(--canon-radius-2);
+ gap: var(--canon-space-1_5);
+
+ &:disabled {
+ cursor: not-allowed;
+ }
+}
+
+.canon-IconButton--variant-primary {
+ background-color: var(--canon-bg-solid);
+ color: var(--canon-fg-solid);
+ transition: background-color 150ms ease, box-shadow 150ms ease;
+
+ &:hover {
+ background-color: var(--canon-bg-solid-hover);
+ }
+
+ &:active {
+ background-color: var(--canon-bg-solid-pressed);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--canon-ring);
+ outline-offset: 2px;
+ }
+
+ &:disabled {
+ background-color: var(--canon-bg-solid-disabled);
+ color: var(--canon-fg-solid-disabled);
+ }
+}
+
+.canon-IconButton--variant-secondary {
+ background-color: var(--canon-bg-surface-1);
+ box-shadow: inset 0 0 0 1px var(--canon-border);
+ color: var(--canon-fg-primary);
+ transition: box-shadow 150ms ease;
+
+ &:hover {
+ box-shadow: inset 0 0 0 1px var(--canon-border-hover);
+ }
+
+ &:active {
+ box-shadow: inset 0 0 0 1px var(--canon-border-pressed);
+ }
+
+ &:focus-visible {
+ outline: none;
+ transition: none;
+ box-shadow: inset 0 0 0 2px var(--canon-ring);
+ }
+
+ &:disabled {
+ box-shadow: inset 0 0 0 1px var(--canon-border-disabled);
+ color: var(--canon-fg-disabled);
+ }
+}
+
+.canon-IconButton--size-medium {
+ font-size: var(--canon-font-size-4);
+ height: 40px;
+ width: 40px;
+}
+
+.canon-IconButton--size-small {
+ font-size: var(--canon-font-size-3);
+ height: 32px;
+ width: 32px;
+}
+
+.canon-IconButton--size-small .canon-IconButton--icon {
+ width: 1rem;
+ height: 1rem;
+}
+
+.canon-IconButton--size-medium .canon-IconButton--icon {
+ width: 1.5rem;
+ height: 1.5rem;
+}
diff --git a/packages/canon/src/components/IconButton/types.ts b/packages/canon/src/components/IconButton/types.ts
new file mode 100644
index 0000000000..90505d3352
--- /dev/null
+++ b/packages/canon/src/components/IconButton/types.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { IconNames } from '../Icon';
+import type { IconButtonOwnProps } from './IconButton.props';
+
+/**
+ * Properties for {@link IconButton}
+ *
+ * @public
+ */
+export interface IconButtonProps
+ extends Omit, 'children'> {
+ /**
+ * The size of the button
+ * @defaultValue 'medium'
+ */
+ size?: IconButtonOwnProps['size'];
+
+ /**
+ * The visual variant of the button
+ * @defaultValue 'primary'
+ */
+ variant?: IconButtonOwnProps['variant'];
+
+ /**
+ * Icon to display at the start of the button
+ */
+ icon: IconNames;
+}
diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css
index e077a065f3..d50aa79177 100644
--- a/packages/canon/src/css/components.css
+++ b/packages/canon/src/css/components.css
@@ -24,6 +24,7 @@
@import '../components/Table/styles.css';
@import '../components/Text/styles.css';
@import '../components/Heading/styles.css';
+@import '../components/IconButton/styles.css';
@import '../components/Input/Input.styles.css';
@import '../components/Field/Field.styles.css';
@import '../components/Link/styles.css';
diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts
index 7c359e2d9b..adf833962c 100644
--- a/packages/canon/src/index.ts
+++ b/packages/canon/src/index.ts
@@ -34,6 +34,7 @@ export * from './components/Heading';
// UI components
export * from './components/Button';
export * from './components/Icon';
+export * from './components/IconButton';
export * from './components/Checkbox';
export * from './components/Table';
export * from './components/Input';