feat(kumo): add disabled prop to LinkButton#535
Open
andystalick wants to merge 1 commit into
Open
Conversation
Adds a `disabled?: boolean` prop to `LinkButton` with parity to `Button`'s
disabled behavior, using `aria-disabled` rather than swapping the rendered
element type.
When `disabled` is true:
- Renders `<a aria-disabled="true" data-disabled="true">`
- Calls preventDefault + stopPropagation on click (blocks navigation and
consumer onClick)
- Calls preventDefault on Enter and Space keydown (blocks keyboard activation)
- Lets other keys (Tab, etc.) pass through to consumer handlers
- Element remains focusable so assistive technology can announce its state
Styling is shared with `Button` via `buttonVariants()`: every existing
`disabled:` Tailwind class is paired with an `aria-disabled:` counterpart,
and `not-disabled:hover:` selectors become `not-disabled:not-aria-disabled:hover:`
to suppress hover on either kind of disabled control. The `aria-disabled`
side gains `pointer-events-none` since browsers do not auto-block clicks
on aria-disabled anchors the way they do on `<button disabled>`.
The spread order in the rendered JSX places `{...externalProps}` and
`{...props}` BEFORE the explicit `aria-disabled`, `data-disabled`,
`onClick`, and `onKeyDown` attributes so that consumers cannot bypass the
disabled behavior by passing those props directly. `onClick` and
`onKeyDown` are destructured out of `...props` so the wrapped versions
always win and are the only path that invokes the consumer's handler.
Tests cover: attribute emission, click blocking, Enter and Space activation
blocking, Tab non-blocking (keeps the link focusable), and a regression
test that non-disabled `onClick` still fires.
Docs add a "Disabled Link" subsection to the Button docs page and a
`LinkButtonDisabledDemo` demo function.
andystalick
commented
May 22, 2026
| // Disabled state | ||
| // Disabled state — applies to native :disabled (Button) and aria-disabled (LinkButton) | ||
| "disabled:cursor-not-allowed disabled:text-kumo-subtle", | ||
| "aria-disabled:cursor-not-allowed aria-disabled:text-kumo-subtle aria-disabled:pointer-events-none", |
Author
There was a problem hiding this comment.
We may roll back pointer-events-none and allow the JS to handle things.
andystalick
commented
May 22, 2026
| href, | ||
| shape = "base", | ||
| size = "base", | ||
| variant = "ghost", |
Author
There was a problem hiding this comment.
This would be a breaking change, but why is the default ghost here when Button's default is secondary?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
disabled?: booleanprop toLinkButtonwith parity toButton's disabled behavior, usingaria-disabledrather than swapping the rendered element type.Behavior when
disabledistrue<a aria-disabled="true" data-disabled="true">preventDefault+stopPropagationon click (blocks navigation and consumeronClick)preventDefaulton Enter and Space keydown (blocks keyboard activation)Button(shared viabuttonVariants())Why
aria-disabledand not "render as<button>"Anchors are the right semantic element for navigation triggers. Swapping the DOM node based on a runtime flag would break ref typing (
HTMLAnchorElementvsHTMLButtonElement) and cause focus jumps when the prop toggles.aria-disabledis the WAI-ARIA pattern for "exists but inert" controls and matches Radix, Base UI, and the W3C ARIA APG.Styling implementation
Extended
buttonVariants()to pair every existingdisabled:Tailwind class with anaria-disabled:counterpart.not-disabled:hover:selectors becamenot-disabled:not-aria-disabled:hover:so hover is suppressed for either kind of disabled control. Thearia-disabledside gainspointer-events-nonebecause browsers do not block clicks onaria-disabledanchors the way they do on native<button disabled>.This keeps
ButtonandLinkButtonvisually identical when disabled and centralises the styling — no per-variant drift.Spread order (defense against bypass)
Consumer props are spread FIRST, then the explicit disabled-related attributes and wrapped handlers — so a consumer cannot accidentally override disabled semantics.
onClickandonKeyDownare also destructured out of...propsso the wrapped handlers always own those slots and remain the only path that invokes the consumer's handler.Tests
7 new tests in
button.test.tsxcover:aria-disabledanddata-disabledattribute emissiononClicknot invoked,defaultPrevented === true)onKeyDownfires)onClickstill firesAll 1086 kumo tests pass.
Docs
LinkButtonDisabledDemoinButtonDemo.tsx/components/buttonexplaining the contractLinkButtonProps.disableddocuments the behavior for IDE tooltipsKnown limitations (not addressed in this PR)
LinkButtonis not a separate entry in the auto-generated component registry — it's bundled underButton. Thedisabledprop is documented via the new "Disabled Link" prose section, the live demo, and source JSDoc, but does not get its own row in the PropsTable. Fix would require expandingscripts/component-registry/to extract sub-components.destructivevariant withdisabled(oraria-disabled) still looks visually identical to its enabled state because!text-whiteoverridesaria-disabled:text-kumo-subtleand there's no per-variantaria-disabled:bg-kumo-danger/50. This is a pre-existing issue that also affects<Button variant="destructive" disabled>and is out of scope here.Verification
pnpm typecheck→ 0 errors workspace-widepnpm lint→ 0 errors workspace-widepnpm --filter @cloudflare/kumo test→ 1086/1086 passpnpm --filter @cloudflare/kumo-docs-astro build→ successaria-disabled="true",data-disabled="true",tabIndex=0,pointer-events: nonecomputed, click/Enter/Space alldefaultPrevented, Tab NOT prevented, focus ring visible when disabled link is focusedChangeset
minorbump for@cloudflare/kumo(additive prop, no breaking change).