-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
A11y: Replace disabled attr with aria-disabled in Button #32325
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: next
Are you sure you want to change the base?
A11y: Replace disabled attr with aria-disabled in Button #32325
Conversation
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.
1 file reviewed, 1 comment
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.
Thanks for your contribution, @WioletaKolodziej!
Let's first start with addressing the feedback from Greptile, and with fixing the PR description. Please copy the PR template that you deleted and please fill it in carefully. It helps us speed up reviews.
You'll also need to address the following:
- Keep the Button API stable, it's a public component and there's no need for a breaking change here; map the
disabled
prop toaria-disabled
without changing the public API - Update Button.stories.tsx to ensure a disabled story exists; in that story, add a play function that proves the button has the aria-disabled attribute and that clicking it does not trigger
onClick
- Add a story where the button is
render
ed with two sibling buttons, and add a play function showing that pressingTab
after focusing the previous button moves focus to the aria-disabled button (which is what we want to fix here) - Check if any existing test or story uses
toBeDisabled
for this button, and if so, switch totoHaveAttribute('aria-disabled', true)
PR template
Closes #
<!-- If your PR is related to an issue, provide the number(s) above; if it resolves multiple issues, be sure to break them up (e.g. "closes #1000, closes #1001"). -->
<!--
Thank you for contributing to Storybook! Please submit all PRs to the `next` branch unless they are specific to the current release. Storybook maintainers cherry-pick bug and documentation fixes into the `main` branch as part of the release process, so you shouldn't need to worry about this. For additional guidance: https://storybook.js.org/docs/contribute
-->
## What I did
<!-- Briefly describe what your PR does -->
## Checklist for Contributors
### Testing
<!-- Please check (put an "x" inside the "[ ]") the applicable items below to communicate how to test your changes -->
#### The changes in this PR are covered in the following automated tests:
- [ ] stories
- [ ] unit tests
- [ ] integration tests
- [ ] end-to-end tests
#### Manual testing
_This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!_
<!-- Please include the steps to test your changes here. For example:
1. Run a sandbox for template, e.g. `yarn task --task sandbox --start-from auto --template react-vite/default-ts`
2. Open Storybook in your browser
3. Access X story
-->
### Documentation
<!-- Please check (put an "x" inside the "[ ]") the applicable items below to indicate which documentation has been updated. -->
- [ ] Add or update documentation reflecting your changes
- [ ] If you are deprecating/removing a feature, make sure to update
[MIGRATION.MD](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md)
## Checklist for Maintainers
- [ ] When this PR is ready for testing, make sure to add `ci:normal`, `ci:merged` or `ci:daily` GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in `code/lib/cli-storybook/src/sandbox-templates.ts`
- [ ] Make sure this PR contains **one** of the labels below:
<details>
<summary>Available labels</summary>
- `bug`: Internal changes that fixes incorrect behavior.
- `maintenance`: User-facing maintenance tasks.
- `dependencies`: Upgrading (sometimes downgrading) dependencies.
- `build`: Internal-facing build tooling & test updates. Will not show up in release changelog.
- `cleanup`: Minor cleanup style change. Will not show up in release changelog.
- `documentation`: Documentation **only** changes. Will not show up in release changelog.
- `feature request`: Introducing a new feature.
- `BREAKING CHANGE`: Changes that break compatibility in some way with current major version.
- `other`: Changes that don't fit in the above categories.
</details>
### 🦋 Canary release
<!-- CANARY_RELEASE_SECTION -->
This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the `@storybookjs/core` team here.
_core team members can create a canary release [here](https://github.com/storybookjs/storybook/actions/workflows/canary-release-pr.yml) or locally with `gh workflow run --repo storybookjs/storybook canary-release-pr.yml --field pr=<PR_NUMBER>`_
<!-- CANARY_RELEASE_SECTION -->
<!-- BENCHMARK_SECTION -->
<!-- BENCHMARK_SECTION -->
Thanks!
b2b6297
to
9b937f8
Compare
Package BenchmarksCommit: The following packages have significant changes to their size or dependencies:
|
Before | After | Difference | |
---|---|---|---|
Dependency count | 48 | 48 | 0 |
Self size | 30.74 MB | 30.62 MB | 🎉 -121 KB 🎉 |
Dependency size | 17.61 MB | 17.61 MB | 0 B |
Bundle Size Analyzer | Link | Link |
@storybook/cli
Before | After | Difference | |
---|---|---|---|
Dependency count | 204 | 219 | 🚨 +15 🚨 |
Self size | 879 KB | 879 KB | 0 B |
Dependency size | 81.72 MB | 81.82 MB | 🚨 +98 KB 🚨 |
Bundle Size Analyzer | Link | Link |
@storybook/codemod
Before | After | Difference | |
---|---|---|---|
Dependency count | 173 | 188 | 🚨 +15 🚨 |
Self size | 35 KB | 35 KB | 🎉 -54 B 🎉 |
Dependency size | 76.79 MB | 76.89 MB | 🚨 +98 KB 🚨 |
Bundle Size Analyzer | Link | Link |
create-storybook
Before | After | Difference | |
---|---|---|---|
Dependency count | 49 | 49 | 0 |
Self size | 1.52 MB | 1.52 MB | 🎉 -22 B 🎉 |
Dependency size | 48.35 MB | 48.23 MB | 🎉 -121 KB 🎉 |
Bundle Size Analyzer | node | node |
@Sidnioulz It’s important to note that even if our Button Interface handles disabled, this does not guarantee that the native HTML disabled attribute won’t end up in the DOM. Since disabled is a standard HTML attribute, React will always forward it – whether through ...props, styled-components, or simply if someone uses a plain button somewhere in the code. As a result, our Button.tsx is not resilient to such mistakes, because disabled can still leak directly into the DOM. Approach is to rely on aria-disabled, but instead of exposing disabled in the API, we should consider a custom prop name like isDisabled (or similar) that prevents the native attribute from being accidentally passed through anyway. |
That's a legitimate concern, but I don't share your analysis. The way I see it, you're arguing in favour of the Button with Button with I'm not a fan of
|
9b937f8
to
2824a75
Compare
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.
Reviewing changes made in this pull request
|
||
const toolbar = page.getByTitle('Change the size of the preview'); | ||
|
||
await expect(toolbar).toBeDisabled(); | ||
await expect(toolbar).toHaveAttribute('aria-disabled'); |
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.
style: Consider adding a more specific assertion to check that the attribute value is 'true' rather than just checking for its presence
await expect(toolbar).toHaveAttribute('aria-disabled'); | |
await expect(toolbar).toHaveAttribute('aria-disabled', 'true'); |
|
||
await userEvent.selectOptions(screen.getByRole('combobox'), ['We use it at work']); | ||
await expect(button).not.toBeDisabled(); | ||
await expect(button).not.toHaveAttribute('aria-disabled', true); |
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.
logic: The assertion not.toHaveAttribute('aria-disabled', true)
may not work as expected. When aria-disabled
is false, the attribute might be present with value 'false' rather than absent. Consider using toHaveAttribute('aria-disabled', 'false')
or checking if the attribute is absent entirely.
await expect(button).not.toHaveAttribute('aria-disabled', true); | |
await expect(button).toHaveAttribute('aria-disabled', 'false'); |
Closes #31678
What I did
Replaced native disabled with aria-disabled for accessibility.
This keeps the button focusable and prevents conflicts with native behavior while preserving styling and click handling.
Changes made:
Replaced native disabled attribute with aria-disabled to preserve focusability and accessibility.
Updated handleClick to prevent clicks when disabled is true, instead of conditionally removing the onClick handler.
Styled button still uses disabled for cursor and opacity styling.
Reasoning:
Using native disabled removes the button from keyboard navigation and makes it invisible to screen readers.
By using aria-disabled, the button remains focusable and perceivable by assistive technologies, while still preventing interactions.
Checklist for Contributors
Testing
The changes in this PR are covered in the following automated tests:
Manual testing
This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!
Documentation
MIGRATION.MD
Checklist for Maintainers
When this PR is ready for testing, make sure to add
ci:normal
,ci:merged
orci:daily
GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found incode/lib/cli-storybook/src/sandbox-templates.ts
Make sure this PR contains one of the labels below:
Available labels
bug
: Internal changes that fixes incorrect behavior.maintenance
: User-facing maintenance tasks.dependencies
: Upgrading (sometimes downgrading) dependencies.build
: Internal-facing build tooling & test updates. Will not show up in release changelog.cleanup
: Minor cleanup style change. Will not show up in release changelog.documentation
: Documentation only changes. Will not show up in release changelog.feature request
: Introducing a new feature.BREAKING CHANGE
: Changes that break compatibility in some way with current major version.other
: Changes that don't fit in the above categories.🦋 Canary release
This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the
@storybookjs/core
team here.core team members can create a canary release here or locally with
gh workflow run --repo storybookjs/storybook canary-release-pr.yml --field pr=<PR_NUMBER>
Greptile Summary
Updated On: 2025-09-10 14:51:07 UTC
This PR implements accessibility improvements by replacing the native HTML
disabled
attribute witharia-disabled
in the Button component. The change addresses the issue where disabled buttons are removed from the accessibility tree and cannot be navigated via keyboard, making them invisible to screen readers.The PR updates the Button component to use
aria-disabled="true"
while maintaining visual styling through CSS that still references thedisabled
prop. The implementation adds click prevention logic in thehandleClick
method to prevent interactions whendisabled
is true, rather than relying on the native HTML disabled behavior. This approach keeps buttons focusable and perceivable by assistive technologies while still preventing user interactions.Corresponding test files have been updated across the codebase to check for
aria-disabled
attributes instead of using DOM testing library matchers liketoBeDisabled()
. The changes affect E2E tests for addon-toolbars, addon-viewport, component testing, and IntentSurvey stories. The Button stories have been enhanced with proper TypeScript types and a custom render function for the disabled state demonstration.Confidence score: 1/5
disabled
andaria-disabled
attributes simultaneously, creating conflicting implementations