# GoodGo Platform Frontend - Comprehensive Accessibility Audit Report
**Date:** April 10, 2026
**Audited:** apps/web (Next.js 14)
**Total Files Analyzed:** 90+ TSX/JSX files
**ARIA Attributes Found:** 75 instances across 14 files
---
## Executive Summary
The GoodGo Platform frontend demonstrates **good foundational accessibility practices** with a skip-to-content link, proper semantic HTML roles, ARIA labels on interactive elements, and form validation. However, there are several areas requiring improvement:
**Key Findings:**
- ✅ Skip-to-content link implemented
- ✅ 75 ARIA attributes across codebase
- ✅ Dialog component with focus management
- ✅ Error handling with proper ARIA attributes
- ⚠️ Icon-only buttons missing aria-labels in some areas
- ⚠️ Thumbnail buttons in image gallery lack accessible names
- ⚠️ Dialog component missing accessibility features (role, focus trap)
- ⚠️ No color contrast definitions documented
- ❌ Some form inputs missing proper label associations
---
## 1. CURRENT ARIA USAGE ANALYSIS
### Summary
- **Total ARIA Attributes:** 75 instances
- **Files Using ARIA:** 14 files
- **Most Common:** aria-label (41 instances), aria-hidden (17 instances)
### Detailed ARIA Breakdown by File
#### Public Layout (`apps/web/app/[locale]/(public)/layout.tsx`)
**Line 49:** `aria-label={t('nav.mainNav')}`
- Location: Desktop navigation element
- Type: Semantic navigation landmark
**Line 91:** `aria-label={mobileMenuOpen ? t('nav.closeMenu') : t('nav.openMenu')}`
- Location: Mobile hamburger button
- Type: Dynamically updated label based on state
- Status: ✅ Properly implemented
#### Root Layout (`apps/web/app/[locale]/layout.tsx`)
**Lines 105-109:** Skip-to-content link
```tsx
{t('skipToContent')}
```
- Status: ✅ Properly implemented with focus visibility
- Hidden by default with -translate-y-16, visible on focus
- Links to `id="main-content"` on main element
#### Dashboard Layout (`apps/web/app/[locale]/(dashboard)/layout.tsx`)
**Line 47:** `aria-label={t('nav.dashboardNav')}`
- Location: Mobile sidebar navigation
- Type: Navigation landmark
**Line 58:** `aria-label={t('nav.closeMenu')}`
- Location: Mobile sidebar close button
- Type: Icon-only button with label
**Line 79:** `aria-hidden="true"`
- Location: Emoji icon in navigation items
- Type: Decorative content hiding
**Line 95:** `aria-hidden="true"`
- Location: LogOut icon
- Type: Decorative icon hiding
**Line 108:** `aria-label={t('nav.openMenu')}`
- Location: Mobile hamburger button
- Type: Icon-only button
**Line 120:** `aria-label={t('nav.dashboardNav')}`
- Location: Desktop navigation
- Type: Navigation landmark
**Line 125:** `aria-label={item.label}`
- Location: Navigation items
- Type: Redundant label (text already visible)
- Status: ⚠️ Not needed when text is visible
**Line 133:** `aria-hidden="true"`
- Location: Icon in navigation items
- Type: Decorative content
**Line 150:** `aria-label={theme === 'light' ? t('dashboard.darkMode') : t('dashboard.lightMode')}`
- Location: Theme toggle button
- Type: Dynamic aria-label ✅ Good
**Lines 154, 158:** `aria-hidden="true"` on SVGs
- Location: Theme toggle icons
- Type: Decorative content hiding
#### Admin Layout (`apps/web/app/[locale]/(admin)/layout.tsx`)
**Line 60:** `aria-hidden="true"`
- Location: Mobile overlay backdrop
**Line 67:** `aria-label={t('nav.adminNav')}`
- Location: Sidebar navigation
**Line 81:** `aria-label={t('adminNav.closeMenu')}`
- Location: Close button
**Line 89:** `aria-label={t('nav.adminNav')}`
- Location: Navigation container (duplicate)
**Line 108:** `aria-hidden="true"`
- Location: Navigation icon
**Line 126:** `aria-hidden="true"`
- Location: LogOut icon
**Line 135:** `aria-label={t('adminNav.openMenu')}`
- Location: Mobile hamburger button
#### Language Switcher (`apps/web/components/ui/language-switcher.tsx`)
**Line 29:** `aria-label={`${t('label')}: ${t(locale)} → ${t(nextLocale)}`}`
- Location: Language toggle button
- Status: ✅ Descriptive label indicating action
**Line 31:** `aria-hidden="true"`
- Location: Emoji display
- Type: Decorative content
#### Search/Filter Components (`apps/web/components/search/filter-bar.tsx`)
**Line 79:** `role="search" aria-label={t('filters')}`
- Location: Filter section
- Status: ✅ Proper semantic role with label
**Lines 87, 101, 115, 129:** `aria-label` on select inputs
- Location: Filter dropdowns
- Pattern: `aria-label={t('allTransactions')}`, `aria-label={t('allPropertyTypes')}`, etc.
- Status: ✅ Accessible form controls
**Lines 150, 158, 167, 182, 192:** `aria-label` on range inputs
- Location: Area and bedroom filters
- Status: ✅ Clear labels for input ranges
#### Image Gallery (`apps/web/components/listings/image-gallery.tsx`)
**Line 47:** `aria-label="Ảnh trước"` (Previous image)
- Location: Previous button
- Status: ✅ Icon-only button with aria-label
**Line 54:** `aria-label="Ảnh tiếp"` (Next image)
- Location: Next button
- Status: ✅ Icon-only button with aria-label
#### Property Card (`apps/web/components/search/property-card.tsx`)
**Line 36:** `aria-label={`${listing.property.title} — ${transactionLabel} ${propertyTypeLabel}, ${formatPrice(listing.priceVND)} VNĐ`}`
- Location: Article element wrapping property card
- Status: ✅ Descriptive accessible name for card
**Line 50:** `aria-hidden="true"`
- Location: "No image" placeholder
- Type: Decorative content
**Line 64:** `aria-label={`${listing.property.media.length} ảnh`}`
- Location: Image count badge
- Status: ⚠️ Badge is decorative, aria-label not needed
**Line 81:** `aria-label="Thông tin bất động sản"`
- Location: Property details list
- Status: ✅ List with semantic label
#### Authentication Pages
**Login Page (`apps/web/app/[locale]/(auth)/login/page.tsx`)**
- Line 76: `aria-describedby={errors.phone ? 'phone-error' : undefined}`
- Line 77: `aria-invalid={!!errors.phone}`
- Line 92: `aria-label={showPassword ? t('hidePassword') : t('showPassword')}`
- Line 102: `aria-describedby={errors.password ? 'password-error' : undefined}`
- Line 103: `aria-invalid={!!errors.password}`
- Line 112: `aria-hidden="true"` on loading spinner
- Status: ✅ Comprehensive form accessibility
**Register Page (`apps/web/app/[locale]/(auth)/register/page.tsx`)**
- Similar pattern to login page with aria-describedby and aria-invalid
- Lines 70, 71, 86, 87, 102, 103, 118, 128, 129, 144, 145
- Status: ✅ Properly implemented
#### Landing/Home Page (`apps/web/app/[locale]/(public)/page.tsx`)
**Line 85:** `role="search" aria-label={t('common.search')}`
- Location: Main search form
- Status: ✅ Proper semantic role
**Line 92:** `aria-label={t('landing.searchPlaceholder')}`
- Location: Search input
- Status: ⚠️ Redundant with placeholder
**Line 99:** `aria-label={t('landing.transactionTypeLabel')}`
- Location: Transaction type select
- Status: ✅ Good for form accessibility
**Line 114:** `aria-hidden="true"`
- Location: Decorative SVG
**Line 147:** `aria-labelledby="featured-heading"`
- Location: Featured listings section
- Status: ✅ Section properly labeled
**Line 162:** `role="status" aria-label={t('common.loading')}`
- Location: Loading indicator
- Status: ✅ Proper role for status messages
**Line 163:** `aria-hidden="true"`
- Location: Spinner animation
**Line 188:** `aria-labelledby="districts-heading"`
- Location: Districts section
- Status: ✅ Section label
**Line 204:** `aria-hidden="true"`
- Location: Emoji decorations
**Line 219:** `aria-labelledby="stats-heading"`
- Location: Statistics section
- Status: ✅ Section label
**Line 234:** `aria-hidden="true"`
- Location: Emoji icons in stats
#### Error and Not-Found Pages
**error.tsx:**
- Line 51: `aria-hidden="true"` on decorative emoji
- Line 85: `aria-hidden="true"` on SVG spinner
**not-found.tsx:**
- Line 10: `aria-hidden="true"` on 404 number
#### UI Component Tests (`components/ui/__tests__/select.spec.tsx`)
- Lines 9, 22, 34: `aria-label` on select components
- Status: ✅ Tests verify accessibility
---
## 2. ICON-ONLY BUTTONS ANALYSIS
### Buttons Requiring aria-label
#### ✅ Properly Labeled Icon Buttons
1. **Mobile Menu Toggle** (`apps/web/app/[locale]/(public)/layout.tsx:90-96`)
```tsx
```
- Status: ✅ Dynamic aria-label based on state
2. **Theme Toggle** (`apps/web/app/[locale]/(dashboard)/layout.tsx:146-160`)
```tsx
```
- Status: ✅ Proper button with icon and aria-label
3. **Language Switcher** (`apps/web/components/ui/language-switcher.tsx:25-34`)
```tsx
```
- Status: ✅ Uses both aria-label and sr-only for redundancy
4. **Image Gallery Navigation** (`apps/web/components/listings/image-gallery.tsx:44-57`)
```tsx
```
- Status: ✅ Previous/Next buttons have clear labels
#### ⚠️ Icon-Only Buttons Requiring Attention
1. **Thumbnail Buttons** (`apps/web/components/listings/image-gallery.tsx:69-84`)
```tsx
```
- **Issue:** No aria-label on thumbnail buttons
- **Impact:** Screen reader users can't identify which thumbnail they're selecting
- **Fix:** Add `aria-label={`Select image ${index + 1}`}`
2. **Admin/Dashboard Mobile Close Button** (`apps/web/app/[locale]/(admin)/layout.tsx:80-86`)
```tsx
```
- Status: ✅ Has aria-label (verified)
3. **Dashboard Mobile Close Button** (`apps/web/app/[locale]/(dashboard)/layout.tsx:57-63`)
```tsx
```
- Status: ✅ Has aria-label (verified)
4. **Admin Mobile Menu Button** (`apps/web/app/[locale]/(admin)/layout.tsx:135`)
```tsx
```
- Status: ✅ Has aria-label (verified)
5. **Password Show/Hide Button** (`apps/web/app/[locale]/(auth)/login/page.tsx:88-95`)
```tsx
```
- Status: ✅ Has aria-label (verified) - though it also has visible text
6. **Save Search Dialog Toggle** (`apps/web/app/[locale]/(public)/search/page.tsx`)
- **Issue:** Dialog toggle button may be icon-only
- **Status:** Requires verification in full file
### Summary of Icon-Only Buttons
- **Properly Labeled:** 10+ buttons
- **Missing Labels:** Thumbnail buttons in image gallery (1 instance with multiple buttons)
- **Recommended Action:** Add aria-label to thumbnail buttons for better screen reader support
---
## 3. FORM INPUTS WITHOUT LABELS
### Analyzed Form Components
#### ✅ Properly Associated Labels
1. **Login Form** (`apps/web/app/[locale]/(auth)/login/page.tsx`)
```tsx
{errors.phone && (
{errors.phone.message}
)}
```
- Status: ✅ Label with htmlFor, aria-describedby for errors
2. **Register Form** (`apps/web/app/[locale]/(auth)/register/page.tsx`)
- Full Name, Phone, Email, Password, Confirm Password inputs
- All have Label components with htmlFor
- Status: ✅ Properly labeled
3. **Search/Filter Forms** (`apps/web/components/search/filter-bar.tsx`)
```tsx
```
- Status: ✅ Uses aria-label instead of visual label (acceptable for filters)
4. **Valuation Form** (`apps/web/components/valuation/valuation-form.tsx`)
```tsx