Skip to content

feat(headless): add a Button primitive with focusableWhenDisabled - #9319

Open
alexcarpenter wants to merge 3 commits into
mainfrom
carp/headless-button
Open

feat(headless): add a Button primitive with focusableWhenDisabled#9319
alexcarpenter wants to merge 3 commits into
mainfrom
carp/headless-button

Conversation

@alexcarpenter

Copy link
Copy Markdown
Member

Description

Adds a Button primitive to @clerk/headless, exported from @clerk/headless/button.

It covers the disabled behaviour a native <button> cannot express:

focusableWhenDisabled — a button that disables itself mid-interaction (while a form submits, while a request is in flight) drops focus to the body, and the user loses their place on the page. With this prop the button keeps its position in the tab order: the disabled attribute is omitted in favour of aria-disabled, and inertness comes from suppressing click, keydown, and keyup instead. The suppressing handlers overwrite rather than chain, so a consumer's onClick never runs while disabled. mousedown is also suppressed, so a pointer press does not pull focus off wherever it currently sits — matching what the disabled attribute does natively.

<Button type='submit' disabled={submitting} focusableWhenDisabled>Save</Button>

nativeButton={false} — for use with the shared render escape hatch when the rendered element is not a real <button>. Applies role="button", tab order, and Enter/Space activation. Without it, <Button render={<span />} /> would emit a type="button" attribute on a span and be non-interactive. Enter on a link is left to the browser so activation does not fire twice.

<Button nativeButton={false} render={<a href='/settings' />}>Settings</Button>

The API follows Base UI's button. @clerk/ui's Mosaic Button is not wired up to this yet; that is stacked on top in a follow-up.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@changeset-bot

changeset-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b2fd4f9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview Aug 4, 2026 7:55pm
swingset Ready Ready Preview Aug 4, 2026 7:55pm

Request Review

@pkg-pr-new

pkg-pr-new Bot commented Aug 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9319

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9319

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9319

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9319

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9319

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9319

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9319

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9319

@clerk/expo-google-signin

npm i https://pkg.pr.new/@clerk/expo-google-signin@9319

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9319

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9319

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9319

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9319

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9319

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9319

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9319

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9319

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9319

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9319

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9319

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9319

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9319

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9319

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9319

commit: b2fd4f9

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Added a client-side Button primitive with native and non-native rendering modes. It supports focusable disabled buttons, ARIA state, keyboard activation, event suppression, ref forwarding, and render state attributes. Added tests and documentation for its behavior and accessibility semantics. Added source, build, and package exports. Added a patch changeset.

Estimated code review effort: 3 (Moderate) | ~30 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the new Button primitive and its focusableWhenDisabled feature.
Description check ✅ Passed The description directly explains the Button primitive, its behavior, exports, tests, and documentation status.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/hip-moles-jam.md:
- Around line 1-2: Update the changeset frontmatter in
.changeset/hip-moles-jam.md to include a patch release entry for `@clerk/headless`
and add a concise release description for the new public ./button export; do not
leave the changeset empty.

In `@packages/headless/src/primitives/button/button.tsx`:
- Around line 30-32: Update suppressEvent to call event.stopPropagation()
alongside preventDefault(), ensuring disabled Button interactions do not reach
ancestor handlers. Add a regression test with a parent handler covering click,
keyboard, or pointer interaction on a focusable disabled Button.
- Around line 55-58: Update the non-native button props in
packages/headless/src/primitives/button/button.tsx:55-58 so disabled elements
use tabIndex -1 when focusableWhenDisabled is false, while preserving tabIndex 0
otherwise. In packages/headless/src/primitives/button/button.test.tsx:252-266,
render a disabled anchor with an href and verify keyboard navigation does not
focus it.
- Line 46: The Button component’s forwarded ref is restricted to
HTMLButtonElement even when nativeButton is false and useRender produces an
anchor or span. Update Button and its ref-related types to support the
non-native HTMLElement render path, and add type coverage confirming an anchor
ref is accepted.

In `@packages/headless/src/primitives/button/README.md`:
- Around line 13-16: Update the Button import in the README usage example to use
the published package subpath `@clerk/headless/button` instead of the internal
`@/primitives/button` alias, leaving the example’s component usage unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b019049-b545-44db-85f8-0dddb87e2402

📥 Commits

Reviewing files that changed from the base of the PR and between 0ba9dbe and 75e873a.

📒 Files selected for processing (7)
  • .changeset/hip-moles-jam.md
  • packages/headless/package.json
  • packages/headless/src/primitives/button/README.md
  • packages/headless/src/primitives/button/button.test.tsx
  • packages/headless/src/primitives/button/button.tsx
  • packages/headless/src/primitives/button/index.ts
  • packages/headless/vite.config.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)

Comment thread .changeset/hip-moles-jam.md
Comment thread packages/headless/src/primitives/button/button.tsx Outdated
Comment thread packages/headless/src/primitives/button/button.tsx Outdated
Comment thread packages/headless/src/primitives/button/button.tsx
Comment thread packages/headless/src/primitives/button/README.md

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/headless/src/primitives/button/button.tsx`:
- Around line 99-104: Update the disabled handling in the Button primitive’s
merged event handlers to assign suppressEvent to merged.onPointerDown,
preventing ancestor pointerdown actions for aria-disabled buttons; add the
requested ancestor onPointerDown regression assertion in
packages/headless/src/primitives/button/button.test.tsx at lines 193-210.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 610adb7b-e5d8-49a4-81f1-7ec1744c6aae

📥 Commits

Reviewing files that changed from the base of the PR and between 75e873a and 786dfe1.

📒 Files selected for processing (3)
  • packages/headless/src/primitives/button/README.md
  • packages/headless/src/primitives/button/button.test.tsx
  • packages/headless/src/primitives/button/button.tsx
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/headless/src/primitives/button/README.md

Comment thread packages/headless/src/primitives/button/button.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/headless/src/primitives/button/button.tsx (1)

74-85: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard non-native keyboard activation.

When nativeButton={false}, a key event from a focusable descendant can bubble to the Button handler and activate the Button via event.currentTarget.click(). Return unless event.target === event.currentTarget in both handlers. Also skip the synthetic click when event.defaultPrevented is true so consumers cannot cancel activation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/headless/src/primitives/button/button.tsx` around lines 74 - 85,
Update the onKeyDown and onKeyUp handlers in the Button component to ignore
bubbled key events unless event.target equals event.currentTarget. In onKeyUp,
also skip synthetic activation when event.defaultPrevented is true, while
preserving existing Space and Enter behavior for the Button itself.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/headless/src/primitives/button/button.tsx`:
- Around line 74-85: Update the onKeyDown and onKeyUp handlers in the Button
component to ignore bubbled key events unless event.target equals
event.currentTarget. In onKeyUp, also skip synthetic activation when
event.defaultPrevented is true, while preserving existing Space and Enter
behavior for the Button itself.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: ed728ecd-47fa-4088-b55b-021b33ba0ae3

📥 Commits

Reviewing files that changed from the base of the PR and between 786dfe1 and b2fd4f9.

📒 Files selected for processing (3)
  • packages/headless/src/primitives/button/README.md
  • packages/headless/src/primitives/button/button.test.tsx
  • packages/headless/src/primitives/button/button.tsx
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/headless/src/primitives/button/README.md
  • packages/headless/src/primitives/button/button.test.tsx

@alexcarpenter
alexcarpenter requested a review from a team August 5, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant