Skip to content

feat: add theme persistence with custom storage key#136

Open
quemsah wants to merge 2 commits into
mainfrom
feature/theme-persistence
Open

feat: add theme persistence with custom storage key#136
quemsah wants to merge 2 commits into
mainfrom
feature/theme-persistence

Conversation

@quemsah

@quemsah quemsah commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add theme preference persistence using custom storage key
  • Add ARIA labels to theme toggle icons for accessibility

Changes

  • Add THEME_LABELS constant for consistent labels
  • Add currentTheme state for aria-label
  • Add aria-label to theme toggle button
  • Add aria-hidden to Sun/Moon icons
  • Update ThemeProvider to use storageKey=theme-preference

Summary by CodeRabbit

  • New Features
    • Theme toggle accessibility improved with dynamic labels displaying the current theme mode
    • Theme preference storage now configured with a dedicated key for better persistence

- Add storageKey='theme-preference' to ThemeProvider for explicit localStorage key
- Add aria-label to theme toggle button showing current theme
- Add aria-hidden to sun/moon icons for better accessibility
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

ThemeProvider in providers.tsx gains storageKey="theme-preference". ThemeToggle adds a THEME_LABELS constant and ThemeMode type, reads theme from useTheme, computes currentTheme with a "system" fallback before mount, and replaces the static screen-reader <span> with a dynamic aria-label.

Changes

Theme Toggle Accessibility and Storage

Layer / File(s) Summary
ThemeProvider storage key config
ui/src/providers/providers.tsx
Adds storageKey="theme-preference" to the ThemeProvider props to control local storage key naming.
ThemeToggle dynamic aria-label and theme labeling
ui/src/components/common/ThemeToggle.tsx
Introduces THEME_LABELS constant and ThemeMode type; reads theme from useTheme; computes currentTheme defaulting to "system" before mount; replaces static screen-reader <span> with a dynamic aria-label built from THEME_LABELS[currentTheme].

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~5 minutes

Poem

🐇 Hop hop, the toggle speaks its name,
A label bright, no longer plain!
"Light" or "dark" the button cries,
While "system" waits till mount time flies.
The storage key now has a home —
This bunny's theme shall never roam! 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add theme persistence with custom storage key' accurately describes the main changes: theme persistence implementation via a custom storage key in ThemeProvider configuration.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/theme-persistence

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 and usage tips.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request improves the accessibility of the ThemeToggle component by adding aria-labels and hiding decorative icons from screen readers, and configures a custom storage key for the ThemeProvider. The review feedback suggests enhancing the button's aria-label to describe both the action and the current state, and improving type safety by avoiding unsafe type assertions when resolving the current theme.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +24 to +30
const currentTheme = mounted ? resolvedTheme : 'system'

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="relative" size="icon" suppressHydrationWarning variant="outline">
<Sun className={`h-[1.2rem] w-[1.2rem] transition-all ${isDark ? 'rotate-90 scale-0' : 'rotate-0 scale-100'}`} />
<Button
aria-label={`Current theme: ${THEME_LABELS[currentTheme as keyof typeof THEME_LABELS] || 'System'}`}

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.

high

Accessibility & Type Safety Improvements

  1. Accessibility (Button Action): A button's accessible name should describe its action (what it does), not just its current state. Setting aria-label to "Current theme: Light" overrides the inner <span className="sr-only">Toggle theme</span> text, leaving screen reader users with a button labeled only with the state, which is confusing. Combining the action and the state (e.g., "Toggle theme. Current theme: Light") provides a much better user experience.

  2. Type Safety: The type assertion currentTheme as keyof typeof THEME_LABELS is unsafe because resolvedTheme can be undefined (e.g., during initialization or SSR), which would make currentTheme undefined at runtime. We can resolve this cleanly and safely by explicitly checking the resolved theme value, which also eliminates the need for any type assertions.

Suggested change
const currentTheme = mounted ? resolvedTheme : 'system'
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="relative" size="icon" suppressHydrationWarning variant="outline">
<Sun className={`h-[1.2rem] w-[1.2rem] transition-all ${isDark ? 'rotate-90 scale-0' : 'rotate-0 scale-100'}`} />
<Button
aria-label={`Current theme: ${THEME_LABELS[currentTheme as keyof typeof THEME_LABELS] || 'System'}`}
const currentTheme = (mounted && resolvedTheme === 'dark') ? 'dark' : (mounted && resolvedTheme === 'light') ? 'light' : 'system'
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label={`Toggle theme. Current theme: ${THEME_LABELS[currentTheme]}`}

@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.

🧹 Nitpick comments (1)
ui/src/components/common/ThemeToggle.tsx (1)

26-26: ⚡ Quick win

Type assertion may be unsafe if theme can have unexpected values.

The as ThemeMode cast assumes theme will always be one of 'light' | 'dark' | 'system'. If next-themes can return other theme values (e.g., custom themes), accessing THEME_LABELS[currentTheme] on line 32 could fail.

Consider defensive coding:

🛡️ Safer implementation with fallback
-  const currentTheme = (mounted && theme ? theme : 'system') as ThemeMode
+  const currentTheme: ThemeMode = (mounted && theme && theme in THEME_LABELS ? theme as ThemeMode : 'system')
🤖 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 `@ui/src/components/common/ThemeToggle.tsx` at line 26, The type assertion `as
ThemeMode` on the currentTheme assignment is unsafe because it assumes the theme
value from next-themes will always be one of the expected values without
validation. If next-themes returns an unexpected theme value, accessing
THEME_LABELS[currentTheme] will fail. Remove the unsafe type assertion and
instead add validation logic that checks whether the resolved theme value is one
of the valid ThemeMode values ('light', 'dark', 'system'), and provide a
fallback to 'system' if the theme is not recognized. This ensures the code
handles unexpected theme values gracefully without runtime errors.
🤖 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.

Nitpick comments:
In `@ui/src/components/common/ThemeToggle.tsx`:
- Line 26: The type assertion `as ThemeMode` on the currentTheme assignment is
unsafe because it assumes the theme value from next-themes will always be one of
the expected values without validation. If next-themes returns an unexpected
theme value, accessing THEME_LABELS[currentTheme] will fail. Remove the unsafe
type assertion and instead add validation logic that checks whether the resolved
theme value is one of the valid ThemeMode values ('light', 'dark', 'system'),
and provide a fallback to 'system' if the theme is not recognized. This ensures
the code handles unexpected theme values gracefully without runtime errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2aead1b2-6f70-435a-b44b-0acbd1360617

📥 Commits

Reviewing files that changed from the base of the PR and between 11833c7 and e02af04.

📒 Files selected for processing (2)
  • ui/src/components/common/ThemeToggle.tsx
  • ui/src/providers/providers.tsx

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