diff --git a/.changeset/public-nails-melt.md b/.changeset/public-nails-melt.md
new file mode 100644
index 0000000000..a8d916950a
--- /dev/null
+++ b/.changeset/public-nails-melt.md
@@ -0,0 +1,5 @@
+---
+'@backstage/canon': minor
+---
+
+**Breaking** We are removing the render prop on the Button component. If you want to use Button as a link, simply add the href prop and we will automaticlly use it as an anchor tag underneath.
diff --git a/canon-docs/src/content/components/button.mdx b/canon-docs/src/content/components/button.mdx
index aff74fe60b..dceabde154 100644
--- a/canon-docs/src/content/components/button.mdx
+++ b/canon-docs/src/content/components/button.mdx
@@ -11,6 +11,7 @@ import {
buttonFullWidthSnippet,
buttonDisabledSnippet,
buttonResponsiveSnippet,
+ buttonAsLinkSnippet,
} from './button.props';
import { ComponentInfos } from '@/components/ComponentInfos';
@@ -102,3 +103,15 @@ Here's a view when buttons are disabled.
Here's a view when buttons are responsive.
+
+### As Link
+
+Use the `href` prop to make a button act as a link.
+
+}
+ code={buttonAsLinkSnippet}
+/>
diff --git a/canon-docs/src/content/components/button.props.ts b/canon-docs/src/content/components/button.props.ts
index fc833a5f73..9cbfc4a80c 100644
--- a/canon-docs/src/content/components/button.props.ts
+++ b/canon-docs/src/content/components/button.props.ts
@@ -61,3 +61,7 @@ export const buttonDisabledSnippet = `
export const buttonResponsiveSnippet = ``;
+
+export const buttonAsLinkSnippet = ``;
diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md
index 4db3a81bbc..df15e5c20d 100644
--- a/packages/canon/report.api.md
+++ b/packages/canon/report.api.md
@@ -3,8 +3,10 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
+import { AnchorHTMLAttributes } from 'react';
import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar';
import { Breakpoint as Breakpoint_2 } from '@backstage/canon';
+import { ButtonHTMLAttributes } from 'react';
import { ChangeEvent } from 'react';
import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible';
import { ComponentProps } from 'react';
@@ -152,37 +154,36 @@ export const breakpoints: Breakpoint[];
// @public (undocumented)
export const Button: ForwardRefExoticComponent<
- Omit & RefAttributes
+ ButtonProps & RefAttributes
>;
// @public (undocumented)
-export type ButtonOwnProps = GetPropDefTypes;
+export type ButtonAnchorProps = ButtonCommonProps &
+ Omit, 'type' | 'onClick'> & {
+ href: string;
+ onClick?: React.MouseEventHandler;
+ };
// @public (undocumented)
-export const buttonPropDefs: {
- variant: {
- type: 'enum';
- values: ('primary' | 'secondary')[];
- className: string;
- default: 'primary';
- responsive: true;
- };
- size: {
- type: 'enum';
- values: ('small' | 'medium')[];
- className: string;
- default: 'medium';
- responsive: true;
- };
+export type ButtonCommonProps = {
+ size?: 'small' | 'medium';
+ variant?: 'primary' | 'secondary';
+ iconStart?: ReactElement;
+ iconEnd?: ReactElement;
+ className?: string;
+ style?: React.CSSProperties;
+ children?: React.ReactNode;
};
+// @public (undocumented)
+export type ButtonNativeProps = ButtonCommonProps &
+ Omit, 'href'> & {
+ href?: undefined;
+ onClick?: React.MouseEventHandler;
+ };
+
// @public
-export interface ButtonProps extends useRender.ComponentProps<'button'> {
- iconEnd?: ReactElement;
- iconStart?: ReactElement;
- size?: ButtonOwnProps['size'];
- variant?: ButtonOwnProps['variant'];
-}
+export type ButtonProps = ButtonAnchorProps | ButtonNativeProps;
// @public (undocumented)
export const Checkbox: ForwardRefExoticComponent<
diff --git a/packages/canon/src/components/Button/Button.props.ts b/packages/canon/src/components/Button/Button.props.ts
deleted file mode 100644
index e363360afa..0000000000
--- a/packages/canon/src/components/Button/Button.props.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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 buttonPropDefs = {
- 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 ButtonOwnProps = GetPropDefTypes;
diff --git a/packages/canon/src/components/Button/Button.stories.tsx b/packages/canon/src/components/Button/Button.stories.tsx
index 2d79da7e29..8c9cc521f0 100644
--- a/packages/canon/src/components/Button/Button.stories.tsx
+++ b/packages/canon/src/components/Button/Button.stories.tsx
@@ -134,13 +134,9 @@ export const Disabled: Story = {
export const AsLink: Story = {
args: {
children: 'I am a link',
+ href: 'https://canon.backstage.io',
+ target: '_blank',
},
- render: args => (
- }
- />
- ),
};
export const Responsive: Story = {
diff --git a/packages/canon/src/components/Button/Button.tsx b/packages/canon/src/components/Button/Button.tsx
index 9775f3f629..aae20c74e4 100644
--- a/packages/canon/src/components/Button/Button.tsx
+++ b/packages/canon/src/components/Button/Button.tsx
@@ -14,69 +14,91 @@
* limitations under the License.
*/
-import { forwardRef, useRef } from 'react';
+import { AnchorHTMLAttributes, ButtonHTMLAttributes, forwardRef } from 'react';
import clsx from 'clsx';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
-import { useRender } from '@base-ui-components/react/use-render';
-
import type { ButtonProps } from './types';
/** @public */
-export const Button = forwardRef(
- (props: ButtonProps, ref) => {
- const {
- size = 'small',
- variant = 'primary',
- iconStart,
- iconEnd,
- children,
- render = ,
- className,
- style,
- ...rest
- } = props;
+export const Button = forwardRef((props, ref) => {
+ const {
+ size = 'small',
+ variant = 'primary',
+ iconStart,
+ iconEnd,
+ children,
+ href,
+ className,
+ style,
+ ...rest
+ } = props;
- // Get the responsive value for the variant
- const responsiveSize = useResponsiveValue(size);
- const responsiveVariant = useResponsiveValue(variant);
- const internalRef = useRef(null);
+ function isAnchor(props: { href?: unknown }): props is { href: string } {
+ return typeof props.href === 'string';
+ }
- const { renderElement } = useRender({
- render,
- props: {
- className: clsx('canon-Button', className),
- ['data-variant']: responsiveVariant,
- ['data-size']: responsiveSize,
- ...rest,
- children: (
- <>
- {iconStart && (
-
- {iconStart}
-
- )}
- {children}
- {iconEnd && (
-
- {iconEnd}
-
- )}
- >
- ),
- },
- refs: [ref, internalRef],
- });
+ const responsiveSize = useResponsiveValue(size);
+ const responsiveVariant = useResponsiveValue(variant);
- return renderElement();
- },
-);
+ const content = (
+ <>
+ {iconStart && (
+
+ {iconStart}
+
+ )}
+ {children}
+ {iconEnd && (
+
+ {iconEnd}
+
+ )}
+ >
+ );
+
+ if (isAnchor(props)) {
+ const { onClick, ...anchorRest } =
+ rest as AnchorHTMLAttributes;
+ return (
+ }
+ onClick={onClick}
+ {...anchorRest}
+ >
+ {content}
+
+ );
+ } else {
+ const { onClick, ...buttonRest } =
+ rest as ButtonHTMLAttributes;
+ return (
+
+ );
+ }
+});
export default Button;
diff --git a/packages/canon/src/components/Button/index.tsx b/packages/canon/src/components/Button/index.ts
similarity index 76%
rename from packages/canon/src/components/Button/index.tsx
rename to packages/canon/src/components/Button/index.ts
index 9051f1145e..b9c497ae1f 100644
--- a/packages/canon/src/components/Button/index.tsx
+++ b/packages/canon/src/components/Button/index.ts
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-export { Button } from './Button';
-export type { ButtonProps } from './types';
-export { buttonPropDefs } from './Button.props';
-export type { ButtonOwnProps } from './Button.props';
+
+export * from './Button';
+export * from './types';
diff --git a/packages/canon/src/components/Button/types.ts b/packages/canon/src/components/Button/types.ts
index e91538299e..c7ac2b88b7 100644
--- a/packages/canon/src/components/Button/types.ts
+++ b/packages/canon/src/components/Button/types.ts
@@ -14,35 +14,40 @@
* limitations under the License.
*/
-import type { ButtonOwnProps } from './Button.props';
-import { ReactElement } from 'react';
-import type { useRender } from '@base-ui-components/react/use-render';
+import {
+ ReactElement,
+ AnchorHTMLAttributes,
+ ButtonHTMLAttributes,
+} from 'react';
+
+/** @public */
+export type ButtonCommonProps = {
+ size?: 'small' | 'medium';
+ variant?: 'primary' | 'secondary';
+ iconStart?: ReactElement;
+ iconEnd?: ReactElement;
+ className?: string;
+ style?: React.CSSProperties;
+ children?: React.ReactNode;
+};
+
+/** @public */
+export type ButtonAnchorProps = ButtonCommonProps &
+ Omit, 'type' | 'onClick'> & {
+ href: string;
+ onClick?: React.MouseEventHandler;
+ };
+
+/** @public */
+export type ButtonNativeProps = ButtonCommonProps &
+ Omit, 'href'> & {
+ href?: undefined;
+ onClick?: React.MouseEventHandler;
+ };
/**
* Properties for {@link Button}
*
* @public
*/
-export interface ButtonProps extends useRender.ComponentProps<'button'> {
- /**
- * The size of the button
- * @defaultValue 'medium'
- */
- size?: ButtonOwnProps['size'];
-
- /**
- * The visual variant of the button
- * @defaultValue 'primary'
- */
- variant?: ButtonOwnProps['variant'];
-
- /**
- * Optional icon to display at the start of the button
- */
- iconStart?: ReactElement;
-
- /**
- * Optional icon to display at the end of the button
- */
- iconEnd?: ReactElement;
-}
+export type ButtonProps = ButtonAnchorProps | ButtonNativeProps;