Skip to content

feat: improve accessibility with aria-labels on icon buttons#53

Merged
APPLEPIE6969 merged 1 commit intomainfrom
fix-a11y-aria-labels-14217599385858934600
Mar 14, 2026
Merged

feat: improve accessibility with aria-labels on icon buttons#53
APPLEPIE6969 merged 1 commit intomainfrom
fix-a11y-aria-labels-14217599385858934600

Conversation

@APPLEPIE6969
Copy link
Copy Markdown
Owner

@APPLEPIE6969 APPLEPIE6969 commented Mar 12, 2026

Improved accessibility across the application by adding aria-label attributes to icon-only buttons. Screen readers will now be able to correctly interpret the functionality of these buttons. Components updated: AuthForm, EmptyState, VoiceInput, Select, and AnimatedDropdown.


PR created automatically by Jules for task 14217599385858934600 started by @APPLEPIE6969

Summary by CodeRabbit

  • Accessibility Improvements
    • Enhanced screen reader support for dropdown menus with dynamic selection labeling
    • Added accessibility labels to password visibility toggles for improved screen reader experience
    • Improved action button accessibility with descriptive labels
    • Voice input control now includes proper accessibility labeling
    • Selection menu options and triggers now properly labeled for all assistive technologies

Added `aria-label` attributes to various icon-only buttons across components:
- `AuthForm`: Password visibility toggle
- `EmptyState`: Action back arrow
- `VoiceInput`: Microphone toggle
- `Select`: Dropdown toggle and option selections
- `AnimatedDropdown`: Dropdown toggle

This ensures that screen readers can properly announce the purpose of these interactive elements.

Co-authored-by: APPLEPIE6969 <242827480+APPLEPIE6969@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 12, 2026

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

Project Deployment Actions Updated (UTC)
studyflow Ready Ready Preview, Comment Mar 12, 2026 1:20pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 12, 2026

📝 Walkthrough

Walkthrough

This PR adds aria-label attributes to interactive elements across multiple components to improve accessibility for screen readers. Changes include labels on dropdown toggles, password visibility toggles, action buttons, voice input buttons, and select triggers, with values dynamically generated from component state or providing fallback values.

Changes

Cohort / File(s) Summary
Form Input Components
components/AnimatedDropdown.tsx, components/AuthForm.tsx, components/ui/Select.tsx
Added aria-label attributes to dropdown toggle buttons and password visibility toggle, with labels reflecting current selection or placeholder values; Select.tsx also adds labels to individual dropdown options.
Interactive UI Components
components/EmptyState.tsx, components/VoiceInput.tsx
Added aria-label attributes to action buttons, with labels derived from component props or recording state; VoiceInput label accounts for permission errors and i18n fallback.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~4 minutes

Possibly related PRs

Poem

🐰 Hop hop, the buttons now speak clear,
With aria-labels for all to hear,
Screen readers rejoice, the web grows kind,
Accessibility improvements, one label at a time! 🎙️

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title mentions adding aria-labels to icon buttons, which aligns with the main objective of improving accessibility, though the actual changes include aria-labels on various button types, not strictly 'icon buttons' as the title suggests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-a11y-aria-labels-14217599385858934600
📝 Coding Plan for PR comments
  • Generate coding plan

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (2)
components/ui/Select.tsx (1)

54-59: ⚠️ Potential issue | 🟠 Major

This still needs select/listbox semantics, not just labels.

The trigger and options are still exposed as plain buttons, so assistive tech won’t get expanded state or which option is selected. Please add the missing semantics here (aria-expanded/aria-haspopup="listbox" on the trigger, role="listbox" on the popup, and role="option" + aria-selected on each option), or switch to a native/headless select primitive.

Example of the minimal ARIA shape
<button
  type="button"
  onClick={() => setIsOpen(!isOpen)}
+ aria-haspopup="listbox"
+ aria-expanded={isOpen}
  aria-label={`Select option, current value: ${selectedOption?.label || placeholder}`}
>
- <div className="max-h-64 overflow-y-auto py-1">
+ <div role="listbox" className="max-h-64 overflow-y-auto py-1">
<button
  key={option.value}
  type="button"
+ role="option"
+ aria-selected={value === option.value}
  onClick={() => {
    onChange(option.value)
    setIsOpen(false)
  }}
  aria-label={`Select ${option.label}`}
>

Also applies to: 73-98

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ui/Select.tsx` around lines 54 - 59, The trigger button (onClick
toggling setIsOpen, using isOpen, selectedOption, placeholder) lacks ARIA
listbox semantics; add aria-expanded={isOpen} and aria-haspopup="listbox" to
that button and ensure its aria-label remains, then give the popup container
role="listbox" (and manage aria-activedescendant if using roving focus), and
each rendered option element should use role="option" plus
aria-selected={option.value === selectedOption?.value}; update focus/keyboard
handling accordingly or replace with a headless/native select primitive to
provide these semantics automatically (ensure references to setIsOpen, isOpen,
selectedOption, and placeholder are preserved while adding the attributes).
components/VoiceInput.tsx (1)

207-219: ⚠️ Potential issue | 🟡 Minor

Include the processing state in the spoken label.

When isProcessing is true, the button is disabled and shows a spinner, but the aria-label still resolves to the idle action. Derive a single state-aware label for both title and aria-label so processing is announced too.

Suggested shape
+ const micLabel = isProcessing
+   ? t("tutor.processing") || "Processing audio"
+   : isRecording
+     ? t("tutor.stop") || "Stop recording"
+     : lastError
+       ? "Microphone Access Blocked"
+       : t("tutor.voice_mode") || "Voice mode"

<button
  onClick={handleMicClick}
  disabled={disabled || isProcessing}
- title={isRecording ? t("tutor.stop") : lastError ? "Microphone Access Blocked" : t("tutor.voice_mode")}
- aria-label={isRecording ? t("tutor.stop") || "Stop recording" : lastError ? "Microphone Access Blocked" : t("tutor.voice_mode") || "Voice mode"}
+ title={micLabel}
+ aria-label={micLabel}
+ aria-busy={isProcessing}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/VoiceInput.tsx` around lines 207 - 219, Create a single
state-aware label in the VoiceInput component (e.g., compute a const like
buttonLabel) that reflects isProcessing, isRecording, lastError, and
diagInfo.permissionState; use that one label for both the button's title and
aria-label so the “processing” state is announced (e.g., when isProcessing true
return a localized "Processing" or "Recording stopped — processing" string via
t(), otherwise return the existing "Stop recording", "Microphone Access
Blocked", or "Voice mode" variants). Update the JSX button to reference this
computed label instead of building title and aria-label separately; ensure the
logic covers isProcessing first, then isRecording, then error/permission cases,
and falls back to the normal voice mode text.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/AnimatedDropdown.tsx`:
- Around line 40-45: The trigger button in AnimatedDropdown uses
selectedOption?.label directly, causing empty visible text and an incomplete
aria-label when no option matches; update the button to use a non-empty fallback
(e.g., "Select option" or "No selection") for both the visible label and the
aria-label—replace selectedOption?.label with a fallbackLabel variable or inline
fallback in the JSX (refer to AnimatedDropdown, setIsOpen, and selectedOption)
so the button always renders readable text and a complete aria-label like
`Toggle dropdown, current selection: ${fallbackLabel}`.

In `@components/AuthForm.tsx`:
- Around line 87-89: In the AuthForm component, wire up a showPassword boolean
state (e.g., useState in AuthForm) and toggle it from the password-visibility
button: add an onClick handler on the button that flips showPassword, set the
password input (id="password") type to "text" when showPassword is true else
"password", update the button icon text to "visibility" vs "visibility_off"
accordingly, and expose the pressed state via aria-pressed (and update
aria-label to reflect current action like "Show password" / "Hide password") so
the control truly functions as a toggle.

---

Outside diff comments:
In `@components/ui/Select.tsx`:
- Around line 54-59: The trigger button (onClick toggling setIsOpen, using
isOpen, selectedOption, placeholder) lacks ARIA listbox semantics; add
aria-expanded={isOpen} and aria-haspopup="listbox" to that button and ensure its
aria-label remains, then give the popup container role="listbox" (and manage
aria-activedescendant if using roving focus), and each rendered option element
should use role="option" plus aria-selected={option.value ===
selectedOption?.value}; update focus/keyboard handling accordingly or replace
with a headless/native select primitive to provide these semantics automatically
(ensure references to setIsOpen, isOpen, selectedOption, and placeholder are
preserved while adding the attributes).

In `@components/VoiceInput.tsx`:
- Around line 207-219: Create a single state-aware label in the VoiceInput
component (e.g., compute a const like buttonLabel) that reflects isProcessing,
isRecording, lastError, and diagInfo.permissionState; use that one label for
both the button's title and aria-label so the “processing” state is announced
(e.g., when isProcessing true return a localized "Processing" or "Recording
stopped — processing" string via t(), otherwise return the existing "Stop
recording", "Microphone Access Blocked", or "Voice mode" variants). Update the
JSX button to reference this computed label instead of building title and
aria-label separately; ensure the logic covers isProcessing first, then
isRecording, then error/permission cases, and falls back to the normal voice
mode text.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 28a8664f-7feb-459d-999e-3459117a4067

📥 Commits

Reviewing files that changed from the base of the PR and between 9c71df9 and a77e892.

📒 Files selected for processing (5)
  • components/AnimatedDropdown.tsx
  • components/AuthForm.tsx
  • components/EmptyState.tsx
  • components/VoiceInput.tsx
  • components/ui/Select.tsx

Comment on lines 40 to 45
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 rounded-lg bg-slate-100 px-3 py-2 text-xs font-medium text-slate-600 transition-all hover:bg-slate-200 focus:outline-none focus:ring-2 focus:ring-primary/50 dark:bg-[#1a1622] dark:text-[#a69db9] dark:hover:bg-[#2e2839]"
aria-label={`Toggle dropdown, current selection: ${selectedOption?.label || ''}`}
>
{selectedOption?.label}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use a non-empty fallback when value no longer matches an option.

If selectedOption is undefined, this becomes Toggle dropdown, current selection: and the trigger renders as icon-only. Please give both the visible label and the aria-label a real fallback such as “Select option” or “No selection”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/AnimatedDropdown.tsx` around lines 40 - 45, The trigger button in
AnimatedDropdown uses selectedOption?.label directly, causing empty visible text
and an incomplete aria-label when no option matches; update the button to use a
non-empty fallback (e.g., "Select option" or "No selection") for both the
visible label and the aria-label—replace selectedOption?.label with a
fallbackLabel variable or inline fallback in the JSX (refer to AnimatedDropdown,
setIsOpen, and selectedOption) so the button always renders readable text and a
complete aria-label like `Toggle dropdown, current selection: ${fallbackLabel}`.

Comment on lines 87 to 89
<input className="w-full h-12 pl-11 pr-4 bg-gray-50 dark:bg-[#1f1c27] border border-gray-200 dark:border-[#433b54] rounded-xl text-slate-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all placeholder:text-gray-400" id="password" placeholder="••••••••" type="password" />
<button className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-gray-400 hover:text-white transition-colors" type="button">
<button aria-label="Toggle password visibility" className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-gray-400 hover:text-white transition-colors" type="button">
<span className="material-symbols-outlined text-[20px]">visibility_off</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t label this as a toggle until it actually toggles.

Right now the button is focusable and announced as a password-visibility control, but it never changes the input type or icon. Please wire a showPassword state into the input, add an onClick, and expose the pressed state.

Minimal fix
+ const [showPassword, setShowPassword] = useState(false)
- <input className="w-full h-12 pl-11 pr-4 bg-gray-50 dark:bg-[`#1f1c27`] border border-gray-200 dark:border-[`#433b54`] rounded-xl text-slate-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all placeholder:text-gray-400" id="password" placeholder="••••••••" type="password" />
- <button aria-label="Toggle password visibility" className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-gray-400 hover:text-white transition-colors" type="button">
-   <span className="material-symbols-outlined text-[20px]">visibility_off</span>
+ <input className="w-full h-12 pl-11 pr-4 bg-gray-50 dark:bg-[`#1f1c27`] border border-gray-200 dark:border-[`#433b54`] rounded-xl text-slate-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all placeholder:text-gray-400" id="password" placeholder="••••••••" type={showPassword ? "text" : "password"} />
+ <button
+   aria-label={showPassword ? "Hide password" : "Show password"}
+   aria-pressed={showPassword}
+   onClick={() => setShowPassword(v => !v)}
+   className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-gray-400 hover:text-white transition-colors"
+   type="button"
+ >
+   <span className="material-symbols-outlined text-[20px]">{showPassword ? "visibility" : "visibility_off"}</span>
  </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/AuthForm.tsx` around lines 87 - 89, In the AuthForm component,
wire up a showPassword boolean state (e.g., useState in AuthForm) and toggle it
from the password-visibility button: add an onClick handler on the button that
flips showPassword, set the password input (id="password") type to "text" when
showPassword is true else "password", update the button icon text to
"visibility" vs "visibility_off" accordingly, and expose the pressed state via
aria-pressed (and update aria-label to reflect current action like "Show
password" / "Hide password") so the control truly functions as a toggle.

@APPLEPIE6969 APPLEPIE6969 merged commit 3c64542 into main Mar 14, 2026
3 checks passed
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