Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

### Enhancements

- Add `LinkButton` component ([#78944](https://github.com/WordPress/gutenberg/pull/78944)).
- `Tooltip.Provider`: Widen the types to accept all props of the equivalent `Tooltip.Provider` from `@base-ui/react` (types-only change) ([#78642](https://github.com/WordPress/gutenberg/pull/78642)).

## 0.14.0 (2026-05-27)
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import defenseStyles from '../utils/css/global-css-defense.module.css';

/**
* A versatile button component with multiple variants, tones, and sizes.
*
* @see {@link https://wordpress.github.io/gutenberg/?path=/docs/design-system-components-button-usage-guidelines--docs When to use Button, Link, or LinkButton}
*/
export const Button = forwardRef< HTMLButtonElement, ButtonProps >(
function Button(
Expand Down
6 changes: 6 additions & 0 deletions packages/ui/src/button/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ const meta: Meta< typeof Button > = {
whereUsed: 'global',
notes: 'Not yet recommended for use alongside components from `@wordpress/components`, pending review of style consistency with `@wordpress/components` and text overflow behavior. See [WordPress/gutenberg#76135](https://github.com/WordPress/gutenberg/issues/76135).',
},
docs: {
description: {
component:
'See [Usage Guidelines](?path=/docs/design-system-components-button-usage-guidelines--docs) for when to use `Button`, `Link`, or `LinkButton`.',
},
},
Comment on lines +23 to +28

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, this kind of description should be consolidated with the main component's JSDoc description so it's reusable everywhere including IntelliSense, not just Storybook.

},
};
export default meta;
Expand Down
97 changes: 97 additions & 0 deletions packages/ui/src/button/stories/usage-guidelines.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Canvas, Markdown, Meta } from '@storybook/addon-docs/blocks';
import * as UsageGuidelinesStories from './usage-guidelines.story';

<Meta of={ UsageGuidelinesStories } />

# Button, Link, and LinkButton

Choose the component based on **what happens when the user activates it**, not
only on how it should look.
Comment on lines +8 to +9

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I don't think we normally break the line in markdown files for formatting reasons?

Suggested change
Choose the component based on **what happens when the user activates it**, not
only on how it should look.
Choose the component based on **what happens when the user activates it**, not only on how it should look.

Here and everywhere else applicable in the PR


## Quick reference

<Markdown>{`
| Goal | Component | Element | Examples |
| --- | --- | --- | --- |
| Change something on the current page | [Button](?path=/docs/design-system-components-button--docs) | \`<button>\` | Save, Delete, Open dialog, Toggle |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| Change something on the current page | [Button](?path=/docs/design-system-components-button--docs) | \`<button>\` | Save, Delete, Open dialog, Toggle |
| Performs an action on the current page. | [Button](?path=/docs/design-system-components-button--docs) | \`<button>\` | Save, Delete, Submit, Open dialog, Toggle |

| Navigate to another page or URL | [Link](?path=/docs/design-system-components-link--docs) | \`<a>\` | "Learn more", docs links, external references |
| Navigate with button styling | [LinkButton](?path=/docs/design-system-components-link-button--docs) | \`<a>\` | Standalone CTAs, card actions |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL generated by Storybook doesn't have the hyphen

Suggested change
| Navigate with button styling | [LinkButton](?path=/docs/design-system-components-link-button--docs) | \`<a>\` | Standalone CTAs, card actions |
| Navigate with button styling | [LinkButton](?path=/docs/design-system-components-linkbutton--docs) | \`<a>\` | Standalone CTAs, card actions |

`}</Markdown>

## Use `Button` for actions

Use `Button` when activation should **not** navigate away as a link would:

- Submitting or resetting a form (`type="submit"` / `type="reset"`)
- Opening overlays ([Dialog](?path=/docs/design-system-components-dialog--docs), [Popover](?path=/docs/design-system-components-popover--docs), etc.)
- Running JavaScript on the current page (`onClick` without a meaningful `href`)
- Toggle controls (`aria-pressed` on neutral minimal buttons)
- Showing a **loading** state while an async action completes

`Button` uses button semantics from Base UI. Do not use it as a stand-in for
navigation when a real URL is the primary outcome.

<Canvas of={ UsageGuidelinesStories.UseButtonForActions } />

See [Button](?path=/docs/design-system-components-button--docs) for API details
and examples.

## Use `Link` for navigation

Use `Link` when activation should take the user to another page or URL. This is
the default choice for navigation — not just inline references in running text,
but any case where the outcome is “go somewhere else.”

`Link` styling communicates navigation more clearly than a button-shaped control.
Users can see at a glance that they are following a link, not triggering an
action on the current page.

Typical uses:

- Inline references inside [Text](?path=/docs/design-system-components-text--docs) or paragraphs
- Standalone “Learn more”, “View documentation”, or “Open site” actions
- External destinations with `openInNewTab` (adds `target="_blank"` and an accessible new-tab indicator)

Prefer composing `Text` as the host and `Link` via `render` when you need
typography from `Text` on a standalone link.

<Canvas of={ UsageGuidelinesStories.UseLinkForInlineNavigation } />

See [Link](?path=/docs/design-system-components-link--docs) for API details and
examples.

## Use `LinkButton` for navigation with button styling

Use `LinkButton` when the destination is a **URL** and the control should
**look like a `Button`** — for example, standalone calls to action, card
footers, or notice actions where button weight fits the layout.

`LinkButton` shares `Button` visual props (`variant`, `tone`, `size`)
but renders an `<a>` like `Link`. It does **not** support `disabled`, `loading`,
`focusableWhenDisabled`, `nativeButton`, or toggle `aria-pressed`.

Do **not** use `LinkButton` for form submit, dialogs, or other non-navigation
actions — use `Button` instead.

Do **not** use `Button` with `render={ <a href="…" /> }` and `nativeButton={
false }` for new code when `LinkButton` fits; `LinkButton` is the supported
pattern for button-styled links.

<Canvas of={ UsageGuidelinesStories.UseLinkButtonForNavigation } />

> **Note:** Prefer `Link` for navigation when possible. Its underline and link
> styling communicate where the user is going more clearly than a button-shaped
> control. Reach for `LinkButton` only when you have considered `Button` and
> `Link` and still need button prominence.

See [LinkButton](?path=/docs/design-system-components-link-button--docs) for API

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL generated by Storybook doesn't have the hyphen

Suggested change
See [LinkButton](?path=/docs/design-system-components-link-button--docs) for API
See [LinkButton](?path=/docs/design-system-components-linkbutton--docs) for API

details and examples.

## Common mistakes

- **`Button` + `onClick` for navigation** — Prefer `Link` with a real `href`
so keyboard, assistive technology, and browser link affordances stay correct.
- **`LinkButton` when `Link` would work** — Default to `Link` for navigation;
button styling can obscure that the control goes elsewhere.
- **`LinkButton` for submit or toggle actions** — Use `Button`; only `Button`
supports loading and pressed toggle styling.
100 changes: 100 additions & 0 deletions packages/ui/src/button/stories/usage-guidelines.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { createInterpolateElement } from '@wordpress/element';
import { Button } from '../index';
import { Link } from '../../link';
import { LinkButton } from '../../link-button';
import { Stack } from '../../stack';
import { Text } from '../../text';

const meta: Meta = {
title: 'Design System/Components/Button/Usage Guidelines',
parameters: {
controls: { disable: true },
componentStatus: {
status: 'use-with-caution',
whereUsed: 'global',
notes: 'Not yet recommended for use alongside components from `@wordpress/components`, pending review of style consistency with `@wordpress/components` and text overflow behavior. See [WordPress/gutenberg#76135](https://github.com/WordPress/gutenberg/issues/76135).',
Comment on lines +13 to +16

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this duplication necessary?

},
},
tags: [ '!dev' ],
};
export default meta;

type Story = StoryObj;

/**
* Use `Button` for actions on the current view: submitting a form, opening a
* dialog, toggling UI, or running JavaScript. It renders a `<button>` and
* supports loading and pressed states.
*/
export const UseButtonForActions: Story = {
render: () => (
<Stack direction="row" gap="sm" wrap="wrap">
<Button type="submit">Save changes</Button>
<Button variant="outline" onClick={ () => {} }>
Open settings
</Button>
</Stack>
),
};

/**
* Use `Link` for navigation. Its underline and link styling communicate where
* the user is going more clearly than a button-shaped control.
*/
export const UseLinkForInlineNavigation: Story = {
render: () => (
<Text variant="body-md" render={ <p /> }>
{ createInterpolateElement(
'Read the <DocumentationLink /> for more details, or <ExternalLink />.',
{
DocumentationLink: (
<Link href="https://wordpress.org/documentation/">
documentation
</Link>
),
ExternalLink: (
<Link href="https://make.wordpress.org/" openInNewTab>
open an external reference
</Link>
),
}
) }
</Text>
),
};

/**
* Use `LinkButton` when navigation should look like a `Button`, such as
* standalone calls to action that require an `href`.
*
* Note: Prefer `Link` for navigation when possible. Its underline and link
* styling set clearer expectations than a button-shaped control.
*/
export const UseLinkButtonForNavigation: Story = {
render: () => (
<Stack direction="column" gap="md">
<Text variant="body-md" render={ <p /> }>
Standalone navigation calls to action can use `LinkButton` when
button styling matches the surrounding UI.
</Text>
<div>
<LinkButton href="https://make.wordpress.org/">
Get started
</LinkButton>
</div>
<Text variant="body-md" render={ <p /> }>
{ createInterpolateElement(
'Note: Prefer <LinkComponent /> for navigation when possible — its underline and link styling communicate where the user is going more clearly than a button-shaped control.',
{
LinkComponent: (
<Link href="https://wordpress.org/documentation/">
Link
</Link>
),
}
) }
</Text>
</Stack>
),
};
1 change: 1 addition & 0 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from './form';
export * from './icon';
export * from './icon-button';
export * from './link';
export * from './link-button';
export * as Notice from './notice';
export * as Popover from './popover';
export * from './stack';
Expand Down
18 changes: 18 additions & 0 deletions packages/ui/src/link-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { LinkButton as _LinkButton } from './link-button';
import { ButtonIcon } from '../button/icon';

export type { LinkButtonProps, LinkButtonIconProps } from './types';

ButtonIcon.displayName = 'LinkButton.Icon';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is actually overriding the displayName set originally on the component (ie. Button.Icon).

We should probably create a separate, thin wrapper component.


/**
* A link that looks like a `Button`. Prefer `Link` for navigation unless
* button prominence is intentional.
Comment on lines +9 to +10

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be a bit more generic and less forward about Link here. 😅

*/
export const LinkButton = Object.assign( _LinkButton, {
/**
* An icon component specifically designed to work well when rendered inside
* a `LinkButton` component.
*/
Icon: ButtonIcon,
} );
54 changes: 54 additions & 0 deletions packages/ui/src/link-button/link-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useRender, mergeProps } from '@base-ui/react';
import clsx from 'clsx';
import { forwardRef } from '@wordpress/element';
import { type LinkButtonProps } from './types';
import buttonStyles from '../button/style.module.css';
import resetStyles from '../utils/css/resets.module.css';
import focusStyles from '../utils/css/focus.module.css';
import defenseStyles from '../utils/css/global-css-defense.module.css';
import styles from './style.module.css';

/**
* A link that looks like a `Button`. Prefer `Link` for navigation unless
* button prominence is intentional.
*
* @see {@link https://wordpress.github.io/gutenberg/?path=/docs/design-system-components-button-usage-guidelines--docs When to use Button, Link, or LinkButton}
*/
export const LinkButton = forwardRef< HTMLAnchorElement, LinkButtonProps >(
function LinkButton(
{
tone = 'brand',
variant = 'solid',
size = 'default',
className,
children,
render,
...props
},
ref
) {
const mergedClassName = clsx(
defenseStyles[ 'link-button' ],
styles[ 'link-button' ],
resetStyles[ 'box-sizing' ],
focusStyles[ 'outset-ring--focus-except-active' ],
variant !== 'unstyled' && buttonStyles.button,
buttonStyles[ `is-${ tone }` ],
buttonStyles[ `is-${ variant }` ],
buttonStyles[ `is-${ size }` ],
className
);

const element = useRender( {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, did we consider composing from the Link component, rather than building from scratch? I see there's even an unstyled variant for this kind of purpose. Not sure if that's a good idea, but good to think about the pros/cons nonetheless.

render,
defaultTagName: 'a',
ref,
props: mergeProps< 'a' >( props, {
className: mergedClassName,
children,
} ),
} );

return element;
}
);
Loading
Loading