fix: add aria-labels to sidebar collapse and expand buttons#14563
fix: add aria-labels to sidebar collapse and expand buttons#14563costajohnt wants to merge 3 commits intostreamlit:developfrom
Conversation
Add accessible labels to the icon-only sidebar buttons so screen readers can identify them. The collapse button gets "Collapse sidebar" and the expand button gets "Expand sidebar". Closes streamlit#8399
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
|
Thanks for contributing to Streamlit! 🎈 Please make sure you have read our Contributing Guide. You can find additional information about Streamlit development in the wiki. The review process:
We're receiving many contributions and have limited review bandwidth — please expect some delay. We appreciate your patience! 🙏 |
The previous querySelector("button") approach would return null
because the data-testid is on the button element itself via
BaseButton prop forwarding. Use getByRole with name option
instead, which is the idiomatic testing-library pattern and
directly validates the accessible interface.
There was a problem hiding this comment.
Summary
This PR adds aria-label attributes to two icon-only sidebar toggle buttons that previously had no accessible name, making them invisible to screen readers:
aria-label="Collapse sidebar"on the sidebar collapse button inSidebar.tsxaria-label="Expand sidebar"on the sidebar expand button inHeader.tsx
Both changes are accompanied by unit tests. This is a partial fix for #8399.
Reviewer consensus: All three reviewers (claude-4.6-opus-high-thinking, gemini-3.1-pro, gpt-5.3-codex-high) approved this PR unanimously. No critical or blocking issues were raised.
Code Quality
The implementation is minimal, targeted, and correct. Adding aria-label directly to the BaseButton components follows the frontend/AGENTS.md accessibility guideline that "icon-only buttons/links must have aria-label." The labels are clear and descriptive. No new abstractions, side effects, or behavior changes are introduced beyond the accessibility improvement.
Test Coverage
Both new tests verify the presence of the aria-label:
- Header test: Uses
getByRole("button", { name: "Expand sidebar" }), the preferred RTL query per the query priority order. - Sidebar test: Uses
within(getByTestId(...)).getByRole("button")+toHaveAttribute, consistent with existing patterns in that file.
Two minor test improvements are suggested via inline comments (redundant toBeInTheDocument() assertion, and test consistency). Neither is blocking.
No E2E tests are added, which is appropriate — aria-label attributes are straightforward HTML attributes best validated by unit tests. The existing E2E sidebar tests cover functional sidebar behavior.
Backwards Compatibility
No breaking changes. Adding aria-label attributes to existing buttons is purely additive and does not affect any existing behavior or public API.
Security & Risk
No security concerns. The changes are limited to adding static aria-label string attributes to two buttons in the frontend UI. No new dependencies, network calls, dynamic content, or sensitive code paths are involved.
External test recommendation
- Recommend external_test: No
- Triggered categories: None
- Evidence:
frontend/app/src/components/Header/Header.tsx: Adds a staticaria-labelattribute — no routing, auth, embedding, or cross-origin impact.frontend/app/src/components/Sidebar/Sidebar.tsx: Adds a staticaria-labelattribute — no external behavior change.
- Suggested external_test focus areas: N/A
- Confidence: High
- Assumptions and gaps: None — these are purely presentational accessibility attributes with no effect on network, session, or embedding behavior.
Accessibility
This PR is specifically an accessibility improvement. The changes directly address the frontend/AGENTS.md guideline: "Focusable controls must have an accessible name — icon-only buttons/links must have aria-label." Both sidebar toggle buttons now have descriptive labels that screen readers can announce, making the sidebar collapse/expand functionality accessible to users of assistive technology. All three reviewers confirmed the implementation is correct.
Recommendations
- In
Header.test.tsx, replace.toBeInTheDocument()with.toBeVisible(). Perfrontend/AGENTS.mdtesting guidelines, usinggetByRole(which throws if not found) with.toBeInTheDocument()is redundant. All three reviewers flagged this. - Consider standardizing the Sidebar test to also use
getByRolewith thenameoption (matching the Header test pattern), which better reflects the RTL query priority order and directly validates the accessible interface.
Verdict
APPROVED: Clean, minimal accessibility fix that adds aria-labels to two icon-only sidebar buttons. All three reviewers approved unanimously. Only minor test assertion improvements were suggested — no blocking issues.
Consolidated AI review by claude-4.6-opus-high-thinking from 3/3 expected reviews (claude-4.6-opus-high-thinking, gemini-3.1-pro, gpt-5.3-codex-high). All reviews completed successfully.
This review also includes 2 inline comment(s) on specific code lines.
|
|
||
| expect( | ||
| screen.getByRole("button", { name: "Expand sidebar" }) | ||
| ).toBeInTheDocument() |
There was a problem hiding this comment.
suggestion: Per frontend/AGENTS.md testing guidelines, getByRole already throws if the element is not found, so asserting .toBeInTheDocument() is redundant. Replace with .toBeVisible() for a meaningful assertion:
expect(
screen.getByRole("button", { name: "Expand sidebar" })
).toBeVisible()| const collapseButton = within( | ||
| screen.getByTestId("stSidebarCollapseButton") | ||
| ).getByRole("button") | ||
| expect(collapseButton).toHaveAttribute("aria-label", "Collapse sidebar") |
There was a problem hiding this comment.
suggestion: Consider using getByRole with the name option instead of querying by test ID and checking the attribute. This mirrors the Header test pattern, better reflects how assistive tech interacts with the element, and follows the RTL query priority order (role > testid):
expect(
screen.getByRole("button", { name: "Collapse sidebar" })
).toBeVisible()There was a problem hiding this comment.
Pull request overview
Adds missing accessible names to the icon-only sidebar collapse/expand controls so screen readers can discover and announce them, along with unit tests to prevent regressions (partial fix for #8399).
Changes:
- Add
aria-label="Collapse sidebar"to the sidebar collapse button. - Add
aria-label="Expand sidebar"to the header’s sidebar expand button. - Add unit tests asserting both controls have accessible labels.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| frontend/app/src/components/Sidebar/Sidebar.tsx | Adds aria-label to the sidebar collapse BaseButton. |
| frontend/app/src/components/Sidebar/Sidebar.test.tsx | Adds a test intended to verify the collapse button label. |
| frontend/app/src/components/Header/Header.tsx | Adds aria-label to the header expand-sidebar BaseButton. |
| frontend/app/src/components/Header/Header.test.tsx | Adds a test verifying the expand button is queryable by accessible name. |
|
|
||
| const collapseButton = within( | ||
| screen.getByTestId("stSidebarCollapseButton") | ||
| ).getByRole("button") |
There was a problem hiding this comment.
This test is likely to fail because the collapse button is rendered with visibility: hidden until the sidebar header is hovered (see StyledCollapseSidebarButton). getByRole("button") filters out hidden elements by default, so it may not find the button here. Consider hovering stSidebarHeader before querying, or query with { hidden: true } and assert the accessible name/label on the underlying button element.
| ).getByRole("button") | |
| ).getByRole("button", { | |
| hidden: true, | |
| name: "Collapse sidebar", | |
| }) |
|
|
||
| expect( | ||
| screen.getByRole("button", { name: "Expand sidebar" }) | ||
| ).toBeInTheDocument() |
There was a problem hiding this comment.
getByRole(...) already throws if the element isn't found, so toBeInTheDocument() is redundant here and goes against the repo's RTL testing guidance. Prefer asserting something meaningful like toBeVisible() (or drop the extra assertion and just rely on getByRole).
| ).toBeInTheDocument() | |
| ).toBeVisible() |
Summary
aria-label="Collapse sidebar"to the sidebar collapse button inSidebar.tsxaria-label="Expand sidebar"to the sidebar expand button inHeader.tsxThese icon-only buttons had no accessible text, making them invisible to screen readers.
Partial fix for #8399