Branch: landing-dashboard-page
File: components/wallet/WalletModal.tsx
Affected Pages: app/page.tsx (landing), app/dashboard/page.tsx (dashboard)
WCAG Reference: WCAG 2.1 SC 4.1.2 — Name, Role, Value (Level AA)
Summary
The WalletModal component renders a custom overlay modal but does not expose the semantics required for assistive technologies (screen readers, switch controls). It is currently implemented as a plain <div> with no ARIA dialog role, no focus management, and no keyboard escape handling — making it effectively invisible and unusable for keyboard-only and screen reader users.
Steps to Reproduce
- Checkout the
landing-dashboard-page branch and run the dev server (npm run dev).
- Open the page in a browser with a screen reader active (e.g. NVDA + Firefox, or macOS VoiceOver + Safari).
- Click "Connect Wallet" in the Navbar or the Hero Section to open the modal.
- Observe that:
- The screen reader does not announce the dialog or its label.
- Focus is not moved into the modal; it remains behind the backdrop.
- Pressing
Escape does not close the modal.
- Focus is not trapped — the reader can tab to elements hidden behind the backdrop.
- The close button (
✕) has no accessible label.
Root Cause
1. Missing role="dialog" and aria-modal="true" on the modal card (Line 33)
The modal card <div> is a plain div. Without role="dialog", assistive technologies do not know a modal context has been opened.
2. Missing aria-labelledby linking the modal to its visible title (Lines 33 & 36)
The <h3>Connect a Wallet</h3> (line 36) exists in the DOM but is never associated with the dialog container, so screen readers cannot announce the modal's purpose.
3. Missing aria-label on the close button (Lines 37–43)
The close button uses the Unicode character ✕ as its only content. Screen readers will either announce it as "multiplication sign" or skip it entirely.
4. No focus management on open/close
When the modal opens, focus stays on the triggering button behind the backdrop. WCAG 2.4.3 requires that focus move into the dialog. When the modal closes, focus must return to the trigger element.
5. No Escape key handler
The WAI-ARIA Authoring Practices Guide (APG) for the Dialog Pattern specifies that pressing Escape must close a modal dialog. There is currently no keydown handler.
6. No focus trap
While the backdrop onClick prevents interaction, focus can still leave the modal via Tab/Shift+Tab, allowing screen reader users to interact with content behind the overlay.
Proposed Fix
The following diff illustrates the minimum changes needed to achieve WCAG 2.1 AA compliance for dialog semantics. A useRef on the modal container, combined with a useEffect, handles focus-on-open, Escape-key-close, and focus-trap.
'use client';
import React, { useEffect, useRef } from 'react';
import { useWallet } from '@/context/WalletContext';
export default function WalletModal() {
const {
walletModalOpen,
setWalletModalOpen,
isConnecting,
connectingWallet,
connectWallet,
} = useWallet();
const modalRef = useRef<HTMLDivElement>(null);
const titleId = 'wallet-modal-title';
// --- FIX 1: Focus management + Escape key handler ---
useEffect(() => {
if (!walletModalOpen) return;
// Move focus into the modal on open
modalRef.current?.focus();
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && !isConnecting) {
setWalletModalOpen(false);
}
// Basic focus trap
if (e.key === 'Tab' && modalRef.current) {
const focusable = modalRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [walletModalOpen, isConnecting, setWalletModalOpen]);
if (!walletModalOpen) return null;
const wallets = [
{ name: 'MetaMask', icon: '🦊', desc: 'Popular EVM browser extension' },
{ name: 'Coinbase Wallet', icon: '🛡️', desc: 'Secure self-custody wallet' },
{ name: 'Rainbow', icon: '🌈', desc: 'Fun and easy Ethereum wallet' },
{ name: 'WalletConnect', icon: '🔌', desc: 'Scan with mobile wallet' },
];
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
onClick={() => !isConnecting && setWalletModalOpen(false)}
aria-hidden="true" // FIX 2: hide backdrop from the accessibility tree
/>
{/* FIX 3: role="dialog", aria-modal, aria-labelledby, tabIndex for focus target */}
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
tabIndex={-1}
className="relative w-full max-w-md overflow-hidden rounded-3xl border border-neutral-200 bg-white p-6 text-black shadow-2xl transition-all duration-300 outline-none"
>
{/* Modal Header */}
<div className="mb-6 flex items-center justify-between">
{/* FIX 4: give the title a stable id to satisfy aria-labelledby */}
<h3 id={titleId} className="text-xl font-bold tracking-tight">
Connect a Wallet
</h3>
{/* FIX 5: aria-label on the icon-only close button */}
<button
onClick={() => !isConnecting && setWalletModalOpen(false)}
disabled={isConnecting}
aria-label="Close wallet connection modal"
className="flex h-8 w-8 items-center justify-center rounded-full bg-neutral-100 text-neutral-500 hover:bg-neutral-200 hover:text-black transition-colors disabled:opacity-50"
>
<span aria-hidden="true">✕</span>
</button>
</div>
{/* ... rest of the component unchanged ... */}
</div>
</div>
);
}
Branch:
landing-dashboard-pageFile:
components/wallet/WalletModal.tsxAffected Pages:
app/page.tsx(landing),app/dashboard/page.tsx(dashboard)WCAG Reference: WCAG 2.1 SC 4.1.2 — Name, Role, Value (Level AA)
Summary
The
WalletModalcomponent renders a custom overlay modal but does not expose the semantics required for assistive technologies (screen readers, switch controls). It is currently implemented as a plain<div>with no ARIA dialog role, no focus management, and no keyboard escape handling — making it effectively invisible and unusable for keyboard-only and screen reader users.Steps to Reproduce
landing-dashboard-pagebranch and run the dev server (npm run dev).Escapedoes not close the modal.✕) has no accessible label.Root Cause
1. Missing
role="dialog"andaria-modal="true"on the modal card (Line 33)The modal card
<div>is a plaindiv. Withoutrole="dialog", assistive technologies do not know a modal context has been opened.2. Missing
aria-labelledbylinking the modal to its visible title (Lines 33 & 36)The
<h3>Connect a Wallet</h3>(line 36) exists in the DOM but is never associated with the dialog container, so screen readers cannot announce the modal's purpose.3. Missing
aria-labelon the close button (Lines 37–43)The close button uses the Unicode character
✕as its only content. Screen readers will either announce it as "multiplication sign" or skip it entirely.4. No focus management on open/close
When the modal opens, focus stays on the triggering button behind the backdrop. WCAG 2.4.3 requires that focus move into the dialog. When the modal closes, focus must return to the trigger element.
5. No
Escapekey handlerThe WAI-ARIA Authoring Practices Guide (APG) for the Dialog Pattern specifies that pressing
Escapemust close a modal dialog. There is currently nokeydownhandler.6. No focus trap
While the backdrop
onClickprevents interaction, focus can still leave the modal viaTab/Shift+Tab, allowing screen reader users to interact with content behind the overlay.Proposed Fix
The following diff illustrates the minimum changes needed to achieve WCAG 2.1 AA compliance for dialog semantics. A
useRefon the modal container, combined with auseEffect, handles focus-on-open, Escape-key-close, and focus-trap.