Skip to content

Add theme switcher to MainMenu#13937

Merged
mayagbarnes merged 8 commits intofeature/new-app-menufrom
theme-switcher
Feb 17, 2026
Merged

Add theme switcher to MainMenu#13937
mayagbarnes merged 8 commits intofeature/new-app-menufrom
theme-switcher

Conversation

@mayagbarnes
Copy link
Copy Markdown
Collaborator

@mayagbarnes mayagbarnes commented Feb 13, 2026

Describe your changes

Adds a theme toggle (System / Light / Dark) directly into the main menu as menuitemradio elements, building on the accessibility refactor in #13878.
Note: the Settings dialog's theme selection mechanism will be removed in a separate PR when the full SettingsDialog & menu option is removed.

Changes:

  • Three radio items (System, Light, Dark) appear at the top of the menu, inside a role="group" container with aria-label="Theme".
  • Each item uses role="menuitemradio" with aria-checked to reflect the active theme.
  • Selecting a theme calls setTheme via ThemeContext without closing the menu, so users can preview different themes.
  • Keyboard navigation (ArrowUp/Down, Home/End, Enter/Space) flows seamlessly between radio items and action items in a single roving tabindex.
Screenshot 2026-02-12 at 3 43 46 p m Screenshot 2026-02-12 at 3 43 38 p m

Testing Plan

  • JS Unit Tests: ✅ Added
  • E2E Tests: ✅ Added
  • Manual Testing: ✅

@mayagbarnes mayagbarnes added security-assessment-completed change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users labels Feb 13, 2026
@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Feb 13, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 13, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-13937/streamlit-1.54.0-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-13937.streamlit.app (☁️ Deploy here if not accessible)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 13, 2026

📉 Frontend coverage change detected

The frontend unit test (vitest) coverage has decreased by 0.1400%

  • Current PR: 86.9500% (13979 lines, 1823 missed)
  • Latest develop: 87.0900% (14096 lines, 1819 missed)

💡 Consider adding more unit tests to maintain or improve coverage.

📊 View detailed coverage comparison

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a theme switcher directly to the MainMenu, allowing users to toggle between System, Light, and Dark themes using menuitemradio elements. This builds on the accessibility refactor from #13878 and provides a more convenient way to change themes without opening the Settings dialog (which will be deprecated in a future PR).

Changes:

  • Added theme selection UI as three radio buttons (System, Light, Dark) at the top of the main menu
  • Integrated theme switching with existing ThemeContext to maintain theme state
  • Updated keyboard navigation to seamlessly flow between radio items and action items
  • Radio items do not close the menu when selected, allowing users to preview themes

Reviewed changes

Copilot reviewed 6 out of 12 changed files in this pull request and generated no comments.

Show a summary per file
File Description
frontend/app/src/components/MainMenu/themeSection.ts Pure logic for building theme section data structure from ThemeContext state
frontend/app/src/components/MainMenu/themeSection.test.ts Comprehensive unit tests for theme section builder logic
frontend/app/src/components/MainMenu/styled-components.ts Styled components for theme radio group and individual radio items
frontend/app/src/components/MainMenu/MainMenu.tsx Integration of theme section into menu with discriminated unions and roving tabindex
frontend/app/src/components/MainMenu/MainMenu.test.tsx Comprehensive unit tests for theme switcher integration in MainMenu
e2e_playwright/main_menu_test.py E2E tests for keyboard navigation, theme persistence, and OS preference changes
e2e_playwright/__snapshots__/* Updated visual snapshots for all browsers (Chromium, Firefox, WebKit) in both light and dark themes

@mayagbarnes mayagbarnes added the ai-review If applied to PR or issue will run AI review workflow label Feb 13, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Feb 13, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR adds a theme switcher (System / Light / Dark) directly into the MainMenu as menuitemradio elements, providing inline theme selection without needing to open the Settings dialog. The implementation introduces:

  • A new themeSection.ts module that builds theme radio item data from ThemeContext state.
  • ThemeRadioItemRow component in MainMenu.tsx rendering radio items with icons, proper ARIA attributes, and roving tabindex integration.
  • Radio items are wrapped in a role="group" container with aria-label="Theme".
  • Selecting a theme does not close the menu, allowing users to preview different themes.
  • Updated E2E tests to reflect the new menu structure (theme radios now appear at the top).
  • Comprehensive unit tests for both the data-building logic and the rendered behavior.

Code Quality

The code quality is excellent and follows established patterns well:

  • Clean separation of concerns: Pure data construction (buildThemeSection in themeSection.ts) is separated from rendering (ThemeRadioItemRow in MainMenu.tsx). This makes the logic independently testable.
  • Static data at module scope: THEME_OPTIONS is correctly extracted to module-level scope per the frontend coding guidelines.
  • TypeScript discriminated unions: The MenuItem = MenuActionItem | MenuRadioItem union with type discriminant and isRadioItem type guard is clean and type-safe.
  • React best practices: Proper use of useMemo for themeSection and sections, useCallback for stable refs, and memo for ThemeRadioItemRow.
  • Emotion styled components: New styled components (StyledThemeRadioGroup, StyledThemeRadioItem, StyledThemeRadioIcon) follow the Styled* naming convention and use theme tokens (no hardcoded pixels).

One minor note on themeSection.ts line 102-104:

const hasCustomTheme = availableThemes.some(
  theme =>
    theme.name === CUSTOM_THEME_NAME || theme.name.startsWith("Custom Theme")
)

The first clause (theme.name === CUSTOM_THEME_NAME) is technically redundant since CUSTOM_THEME_NAME = "Custom Theme" and "Custom Theme".startsWith("Custom Theme") is always true. The startsWith check alone would suffice. This is cosmetic and does not affect correctness.

Test Coverage

Test coverage is thorough and well-structured across three layers:

Unit tests (themeSection.test.ts):

  • findThemeForSelection: Parameterized tests for all selection types, custom theme variant priority, and the undefined case.
  • buildThemeSection: Tests for radio item structure/labels/icons, empty array cases, single custom theme hiding, checked state per active theme, onSelect behavior with metrics, and the edge case where no matching theme is found.

Integration tests (MainMenu.test.tsx):

  • New "Theme radio items" describe block with 14 tests covering: rendering 3 radio items, ARIA group attributes, hiding when no themes available, hiding for single custom theme, aria-checked state, setTheme invocation on click, keyboard activation (Enter/Space), focus retention after selection, metrics tracking, seamless ArrowDown/ArrowUp navigation between radio and action items, Home/End navigation, wrap-around, roving tabindex across both item types, and minimal mode rendering.
  • Uses renderWithContexts to inject theme context, following the codebase's test patterns.
  • Positive and negative assertions are balanced (e.g., checking queryByRole("menuitemradio") returns null when themes are hidden).

E2E tests (main_menu_test.py):

  • Updated existing keyboard navigation tests to account for the new theme radios at the top of the menu.
  • test_theme_switcher_changes_to_dark: Verifies clicking Dark changes background, menu stays open, and aria-checked is set.
  • test_theme_switcher_persists_cached_preference_on_reload: Verifies localStorage persistence across page reload.
  • test_auto_theme_recalibrates_on_system_change: Verifies System theme follows OS preference changes.
  • Removed the old test_cached_preference_persists_on_reload that used the Settings dialog.
  • The _select_theme helper reduces duplication nicely.
  • Updated snapshot images for the new menu appearance.

Backwards Compatibility

No breaking changes. The theme switcher is purely additive UI:

  • Existing menu items (Rerun, Settings, Clear cache, Print, Record screen, etc.) remain in the same relative order, just shifted down by the theme radio group.
  • The Settings dialog's theme selection mechanism is explicitly noted as being removed in a separate PR, so both mechanisms coexist temporarily.
  • The getMenuLabels helper only queries stMainMenuItemLabel (action items), so existing tests that check menu item order are unaffected by the new radio items.
  • No backend, protobuf, or API changes.

Security & Risk

No security concerns identified. The changes are entirely frontend UI:

  • No new network requests, authentication changes, or data exposure.
  • Theme state is stored in localStorage (existing mechanism).
  • No new dependencies introduced.
  • The findThemeForSelection safely returns undefined when no matching theme exists, preventing unintended setTheme calls.

Accessibility

Accessibility is a standout strength of this PR:

  • menuitemradio role: Each theme option uses role="menuitemradio" with aria-checked reflecting the active theme — correct per WAI-ARIA.
  • role="group" container: The three radio items are wrapped in a group with aria-label="Theme", providing screen reader context.
  • Roving tabindex: Works seamlessly across radio and action items in a single linear navigation flow. Only the focused item has tabIndex=0; all others have -1.
  • Keyboard support: ArrowUp/Down navigates through all items (radio + action), Home/End jump to first/last, Enter/Space activates, Escape closes the menu.
  • Focus management: Focus remains on the selected radio after clicking (menu stays open), and existing focus-return logic (Escape → trigger, Tab → next element) is preserved.
  • :focus-visible styling: StyledThemeRadioItem uses :focus-visible with theme.shadows.focusRingMuted, consistent with other menu items and the project's a11y guidelines.
  • No aria-hidden on interactive content: The radio group is properly accessible to assistive technology.

Recommendations

  1. Minor: In themeSection.ts:102-104, the theme.name === CUSTOM_THEME_NAME clause is redundant alongside theme.name.startsWith("Custom Theme"). Removing it would simplify the condition without changing behavior.

  2. Nit: Consider adding a brief comment in the E2E test test_keyboard_activates_menu_item explaining the magic number 4 in for _ in range(4) (3 theme radios + 1 Rerun = 4 ArrowDown presses to reach Settings). The current comment helps but the count relationship could be clearer.

Verdict

APPROVED: This is a well-implemented, thoroughly tested, and accessibility-compliant addition of a theme switcher to the MainMenu. The code follows established patterns, introduces no breaking changes, and provides excellent coverage across unit, integration, and E2E tests.


This is an automated AI review by opus-4.6-thinking.

@mayagbarnes mayagbarnes marked this pull request as ready for review February 13, 2026 23:57
Comment on lines +39 to +40
/** Theme selection options with their icons */
type ThemeSelection = "System" | "Light" | "Dark"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Please leverage the existing ThemeSelection type from frontend/lib/src/theme/types.ts, rather than redefining it here.

@mayagbarnes mayagbarnes merged commit bae168c into feature/new-app-menu Feb 17, 2026
39 of 40 checks passed
@mayagbarnes mayagbarnes deleted the theme-switcher branch February 17, 2026 18:54
sfc-gh-mbarnes pushed a commit that referenced this pull request Feb 19, 2026
Adds a theme toggle (System / Light / Dark) directly into the main menu as `menuitemradio` elements, building on the accessibility refactor in #13878.

**Changes:**
- Three radio items (System, Light, Dark) appear at the top of the menu,
inside a `role="group"` container with `aria-label="Theme"`.
- Each item uses `role="menuitemradio"` with `aria-checked` to reflect
the active theme.
- Selecting a theme calls `setTheme` via `ThemeContext` without closing
the menu, so users can preview different themes.
- Keyboard navigation (ArrowUp/Down, Home/End, Enter/Space) flows
seamlessly between radio items and action items in a single roving
tabindex.
sfc-gh-mbarnes pushed a commit that referenced this pull request Feb 24, 2026
Adds a theme toggle (System / Light / Dark) directly into the main menu as `menuitemradio` elements, building on the accessibility refactor in #13878.

**Changes:**
- Three radio items (System, Light, Dark) appear at the top of the menu,
inside a `role="group"` container with `aria-label="Theme"`.
- Each item uses `role="menuitemradio"` with `aria-checked` to reflect
the active theme.
- Selecting a theme calls `setTheme` via `ThemeContext` without closing
the menu, so users can preview different themes.
- Keyboard navigation (ArrowUp/Down, Home/End, Enter/Space) flows
seamlessly between radio items and action items in a single roving
tabindex.
mayagbarnes added a commit that referenced this pull request Feb 24, 2026
This PR is the culmination of the following PRs approved into this feature branch:
- [Core `MainMenu` Refactor](#13833)
- [`MainMenu` accessibility improvements](#13878)
- [Add theme switcher to `MainMenu`](#13937)
- [Add auto-rerun toggle to `MainMenu`](#13988)
- [Add version footer to `MainMenu`](#14028)
- [Remove `SettingsDialog`](#14048)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants