Skip to content

UI: add LinkButton#78944

Open
simison wants to merge 11 commits into
trunkfrom
add/ui-linkbutton
Open

UI: add LinkButton#78944
simison wants to merge 11 commits into
trunkfrom
add/ui-linkbutton

Conversation

@simison
Copy link
Copy Markdown
Member

@simison simison commented Jun 4, 2026

What?

Resolves #77098

  • Adds LinkButton component to UI package, following most of the visual features of Button.
  • Adds guidance on when to choose Button, Link, or LinkButton.
  • Supports icons via LinkButton.Icon component, which is just re-export of ButtonIcon

Does not support:

  • openInNewTab like Link (good for a follow-up)
  • Semantic props relevant to "real" buttons (nativeButton, loadingAnnouncement, loading, focusableWhenDisabled, disabled and aria-pressed).
Screenshot 2026-06-05 at 15 58 44

Link button behaves visually like Button, but accepts href and semantically behaves like Link.

The component has its own defence styles, instead of re-using a defence. Otherwise, we'd see incorrect text colors;
Screenshot 2026-06-05 at 16 00 01

Why?

In principle, we'd prefer people use Link for actual links, and Button for non-links. In practise, these expectations are blurred lines for both designers and users, and sometimes even React apps (including Gutenberg) have internal, in-app flows which require using href instead of onClick.

In the guidance added in this PR we're still encouraging people to choose Button or Link over LinkButton, and explain user expectations.

How?

Testing Instructions

  • See Storybook guide added under Button /?path=/docs/design-system-components-button-usage-guidelines--docs
  • See component from storybook /?path=/docs/design-system-components-linkbutton--docs

Testing Instructions for Keyboard

Use of AI Tools

@simison simison added the [Status] In Progress Tracking issues with work in progress label Jun 4, 2026
@github-actions github-actions Bot added [Package] UI /packages/ui and removed [Status] In Progress Tracking issues with work in progress labels Jun 4, 2026
@simison simison changed the title UI: add link button UI: add LinkButton Jun 4, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

Size Change: +687 B (+0.01%)

Total Size: 8.21 MB

📦 View Changed
Filename Size Change
build/modules/boot/index.min.js 51.9 kB +80 B (+0.15%)
build/modules/content-types/index.min.js 158 kB +266 B (+0.17%)
build/scripts/block-editor/index.min.js 379 kB +86 B (+0.02%)
build/scripts/block-library/index.min.js 324 kB +57 B (+0.02%)
build/scripts/edit-site/index.min.js 296 kB +145 B (+0.05%)
build/scripts/editor/index.min.js 463 kB +54 B (+0.01%)
build/scripts/media-utils/index.min.js 113 kB -1 B (0%)

compressed-size-action

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

Flaky tests detected in 45696b0.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/26951289449
📝 Reported issues:

@simison simison force-pushed the add/ui-linkbutton branch from 45696b0 to 92c1cd0 Compare June 5, 2026 12:20
@simison simison marked this pull request as ready for review June 5, 2026 13:17
@simison simison requested a review from a team as a code owner June 5, 2026 13:17
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 5, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: simison <simison@git.wordpress.org>
Co-authored-by: jameskoster <jameskoster@git.wordpress.org>
Co-authored-by: ciampo <mciampini@git.wordpress.org>
Co-authored-by: mirka <0mirka00@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@simison simison added the [Type] Enhancement A suggestion for improvement. label Jun 5, 2026
@simison
Copy link
Copy Markdown
Member Author

simison commented Jun 5, 2026

Users can now use icons by using Button.Icon:

<LinkButton href="#">
    <Button.Icon icon={ <svg /> } />
    Link
</LinkButton>

...is that ok, or should we have LinkButton.Icon and basically re-export Button.Icon?

Edit: there's now LinkButton.Icon

Comment thread packages/ui/src/link-button/types.ts Outdated
@jameskoster
Copy link
Copy Markdown
Contributor

Should this support openInNewTab?

@jameskoster
Copy link
Copy Markdown
Contributor

jameskoster commented Jun 5, 2026

I could potentially see this being used in the Site Editor sidebar which makes me wonder if it should support a 'selected' state, or would that be a property of a higher-level menu component?

@simison simison force-pushed the add/ui-linkbutton branch from f5b5d76 to 6aa67da Compare June 5, 2026 16:37
@simison
Copy link
Copy Markdown
Member Author

simison commented Jun 5, 2026

Should this support openInNewTab?

Possibly; just not sure how we should go about the icon in that case (since buttons can have icons by other methods), so I left it for a separate PR to get this in sooner as-is without the support for now.

@simison
Copy link
Copy Markdown
Member Author

simison commented Jun 5, 2026

I could potentially see this being used in the Site Editor sidebar which makes me wonder if it should support a 'selected' state, or would that be a property of a higher-level menu component?

Easy enough to add later when it is used there, rather than hypothetically ahead of time. :-)

@ciampo
Copy link
Copy Markdown
Contributor

ciampo commented Jun 5, 2026

I could potentially see this being used in the Site Editor sidebar which makes me wonder if it should support a 'selected' state, or would that be a property of a higher-level menu component?

LinkButton should be used for links that look like buttons. In that sense, selected isn't really a quality of a link, while it can be associated with a button (pressed) or a checkbox / radio menu item (checked)

Comment on lines +9 to +10
* A link that looks like a `Button`. Prefer `Link` for navigation unless
* button prominence is intentional.
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. 😅

Copy link
Copy Markdown
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

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

Thank you for working on this!

Comment on lines +8 to +9
Choose the component based on **what happens when the user activates it**, not
only on how it should look.
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

<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 |

@@ -0,0 +1,18 @@
@layer wp-ui-utilities, wp-ui-components, wp-ui-compositions, wp-ui-overrides;

@layer wp-ui-components {
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.

Not sure if we should use the compositions layer here, since we technically want to make sure these styles win over Button styles. Also in relationship to #78953

Comment on lines +121 to +129
.link-button,
.link-button:is(:hover, :focus, :active, :visited) {
outline: var(--_gcd-link-button-outline, 0 solid transparent);
color: var(--_gcd-link-button-color, inherit);
box-shadow: var(--_gcd-link-button-box-shadow, none);
border-radius: var(--_gcd-link-button-border-radius, 0);
transition: var(--_gcd-link-button-transition, none);
text-decoration: var(--_gcd-link-button-text-decoration, none);
}
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.

Some of these new gdc variables are not being set, causing the focus ring is currently invisible, and the Button's border radius is not applied.

I played with these variables locally, and this seems to work well
diff --git i/packages/ui/src/link-button/link-button.tsx w/packages/ui/src/link-button/link-button.tsx
index e68ed654e49..97381ea931e 100644
--- i/packages/ui/src/link-button/link-button.tsx
+++ w/packages/ui/src/link-button/link-button.tsx
@@ -29,10 +29,10 @@ export const LinkButton = forwardRef< HTMLAnchorElement, LinkButtonProps >(
 	) {
 		const mergedClassName = clsx(
 			defenseStyles[ 'link-button' ],
-			styles[ 'link-button' ],
 			resetStyles[ 'box-sizing' ],
 			focusStyles[ 'outset-ring--focus-except-active' ],
 			variant !== 'unstyled' && buttonStyles.button,
+			variant !== 'unstyled' && styles[ 'link-button' ],
 			buttonStyles[ `is-${ tone }` ],
 			buttonStyles[ `is-${ variant }` ],
 			buttonStyles[ `is-${ size }` ],
diff --git i/packages/ui/src/link-button/style.module.css w/packages/ui/src/link-button/style.module.css
index 3ed7f450998..af7b72025c1 100644
--- i/packages/ui/src/link-button/style.module.css
+++ w/packages/ui/src/link-button/style.module.css
@@ -6,6 +6,15 @@
 		   global CSS defense. */
 		--_gcd-link-button-color: var(--wp-ui-button-foreground-color);
 		--_gcd-link-button-text-decoration: none;
+		--_gcd-link-button-border-radius: var(--wpds-border-radius-sm);
+
+		/* Extend the focus utility's outline transition (set in
+		   `focus.module.css`) with a `color` transition, so styled variants fade
+		   their foreground like `Button`. The `unstyled` variant omits this class
+		   and falls back to the utility's outline-only transition. */
+		@media not ( prefers-reduced-motion ) {
+			--_gcd-link-button-transition: color 0.1s ease-out, outline 0.1s ease-out;
+		}
 
 		&:visited {
 			--_gcd-link-button-color: var(--wp-ui-button-foreground-color);
diff --git i/packages/ui/src/utils/css/focus.module.css w/packages/ui/src/utils/css/focus.module.css
index b8834f98bbd..57d8bd2bd0b 100644
--- i/packages/ui/src/utils/css/focus.module.css
+++ w/packages/ui/src/utils/css/focus.module.css
@@ -10,6 +10,7 @@
 	.outset-ring--focus-parent-visible {
 		@media not ( prefers-reduced-motion ) {
 			--_gcd-a-transition: outline 0.1s ease-out;
+			--_gcd-link-button-transition: outline 0.1s ease-out;
 
 			transition: outline 0.1s ease-out;
 		}
@@ -29,6 +30,7 @@
 	:focus-visible .outset-ring--focus-parent-visible {
 		--_gcd-a-outline: var(--wpds-border-width-focus) solid var(--wpds-color-stroke-focus-brand);
 		--_gcd-div-outline: var(--wpds-border-width-focus) solid var(--wpds-color-stroke-focus-brand);
+		--_gcd-link-button-outline: var(--wpds-border-width-focus) solid var(--wpds-color-stroke-focus-brand);
 
 		outline: var(--wpds-border-width-focus) solid var(--wpds-color-stroke-focus-brand);
 	}


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.

| --- | --- | --- | --- |
| Change something on the current page | [Button](?path=/docs/design-system-components-button--docs) | \`<button>\` | Save, Delete, 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 |

> 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

Copy link
Copy Markdown
Member

@mirka mirka left a comment

Choose a reason for hiding this comment

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

Sorry I didn't have time to run the code or ask my questions to an agent first, but wrote down my initial thoughts 😄 Thanks for working on this!

Comment on lines +13 to +16
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).',
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?

Comment on lines +23 to +28
docs: {
description: {
component:
'See [Usage Guidelines](?path=/docs/design-system-components-button-usage-guidelines--docs) for when to use `Button`, `Link`, or `LinkButton`.',
},
},
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.

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.

* A styled anchor element with support for semantic color tones and an
* unstyled escape hatch.
*
* @see {@link https://wordpress.github.io/gutenberg/?path=/docs/design-system-components-button-usage-guidelines--docs When to use Button, Link, or LinkButton}
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.

Related to my previous comment about consolidating component descriptions, and not sure if this still holds true in the latest version of Storybook, but @ tags in JSDoc tend to be dropped the docgen, so we've been preferring to write everything without these tags.

Comment on lines +119 to +122
/* Anchor elements styled as buttons (`LinkButton`). Separate from `.a` because
button foreground colors differ from link colors. */
.link-button,
.link-button:is(:hover, :focus, :active, :visited) {
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.

The component has its own defence styles, instead of re-using a defence. Otherwise, we'd see incorrect text colors;

Not sure I understand why though? We can't just set --_gcd-a-color to the correct color in the component stylesheet?

Ideally this defense module stylesheet knows nothing about our specific components, only the raw elements/selectors that we're defending against.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] UI /packages/ui [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UI: Add LinkButton, IconLinkButton components

4 participants