-
Notifications
You must be signed in to change notification settings - Fork 4.8k
UI: add LinkButton #78944
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
UI: add LinkButton #78944
Changes from all commits
64c2c75
9f23b0f
2cceabf
2ed5181
745591b
06302c3
0df33c4
0777eca
582fbd7
6aa67da
3e44d0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
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 | | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| | 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 | | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The URL generated by Storybook doesn't have the hyphen
Suggested change
|
||||||||
| `}</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 | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The URL generated by Storybook doesn't have the hyphen
Suggested change
|
||||||||
| 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. | ||||||||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
| ), | ||
| }; | ||
| 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'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line is actually overriding the 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
| } ); | ||
| 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( { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Curious, did we consider composing from the |
||
| render, | ||
| defaultTagName: 'a', | ||
| ref, | ||
| props: mergeProps< 'a' >( props, { | ||
| className: mergedClassName, | ||
| children, | ||
| } ), | ||
| } ); | ||
|
|
||
| return element; | ||
| } | ||
| ); | ||
There was a problem hiding this comment.
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.