Skip to content

Accessibility

RetailOS targets WCAG 2.1 AA compliance across all web and mobile applications. This page documents concrete requirements and implementation details.

Color Contrast

All text-on-background combinations must meet minimum contrast ratios.

Light Mode Contrast Results

CombinationRatioAA (Normal)AA (Large)
text-1 (#1A1816) on bg-body (#FAFAF7)15.8:1PassPass
text-1 (#1A1816) on bg-card (#FFFFFF)17.4:1PassPass
text-2 (#57534E) on bg-body (#FAFAF7)6.9:1PassPass
text-2 (#57534E) on bg-card (#FFFFFF)7.5:1PassPass
text-3 (#A8A29E) on bg-body (#FAFAF7)3.2:1FailPass
text-3 (#A8A29E) on bg-card (#FFFFFF)3.5:1FailPass
Brand (#2D6B4A) on bg-card (#FFFFFF)5.2:1PassPass
Brand (#2D6B4A) on brand-light (#E8F0EC)4.8:1PassPass
White (#FFFFFF) on Brand (#2D6B4A)5.2:1PassPass
Danger (#C4463A) on bg-card (#FFFFFF)4.6:1PassPass
Warning (#D4910A) on bg-card (#FFFFFF)3.8:1FailPass

text-3 usage

text-3 (#A8A29E) does NOT pass AA for normal text (14px). Use it ONLY for:

  • Placeholder text (not required to pass)
  • Disabled state text (exempt from contrast requirements)
  • Large text (18px+ or 14px+ bold)

For all other secondary text, use text-2.

Dark Mode Contrast Results

CombinationRatioAA (Normal)
text-1 (#F5F3EF) on bg-body (#1C1A17)14.2:1Pass
text-2 (#B5B0A8) on bg-body (#1C1A17)7.8:1Pass
text-3 (#6E6A64) on bg-body (#1C1A17)3.4:1Fail
Brand Dark (#4A9E72) on bg-body (#1C1A17)4.8:1Pass
Brand Dark (#4A9E72) on bg-card (#262420)4.3:1Fail (borderline)

Keyboard Navigation

Focus Ring

All interactive elements must show a visible focus indicator:

css
*:focus-visible {
  outline: 3px solid var(--color-brand);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
  • Color: Brand green (#2D6B4A light / #4A9E72 dark)
  • Width: 3px
  • Offset: 2px from element edge
  • Shape: follows element border-radius

Tab Order Requirements

ContextExpected Tab Order
SidebarSkip nav link, then each nav item top-to-bottom
Data tableColumn headers (sortable), then row-by-row left-to-right
Modal/DialogFocus trapped inside; first focusable element on open; Escape closes
FormLabel-input pairs in visual order
Stat cardsNot focusable (informational only)

Keyboard Shortcuts

PatternKeyAction
ModalEscapeClose modal
DropdownEnter / SpaceOpen/select
DropdownArrow Up/DownNavigate options
Tab barArrow Left/RightSwitch tabs
Date navigationArrow Left/RightPrevious/next day
Data tableEnter on headerToggle sort

ARIA Labels

Required ARIA Attributes

ComponentARIA Requirement
Icon-only buttonsaria-label describing action
Status badgesrole="status" or descriptive text
Sortable columnsaria-sort="ascending" / "descending" / "none"
Expandable sectionsaria-expanded, aria-controls
Loading statesaria-busy="true", aria-live="polite"
Toast notificationsrole="alert", aria-live="assertive"
Modalsrole="dialog", aria-modal="true", aria-labelledby
Sidebar navrole="navigation", aria-label="Navigasi utama"
Progress barsrole="progressbar", aria-valuenow, aria-valuemin, aria-valuemax
Toggle switchesrole="switch", aria-checked

Example: Icon Button

tsx
// Correct
<button aria-label="Hapus item" onClick={onDelete}>
  <Trash className="w-4 h-4" />
</button>

// Incorrect — missing label
<button onClick={onDelete}>
  <Trash className="w-4 h-4" />
</button>

Example: Data Table Sort

tsx
<th
  role="columnheader"
  aria-sort={sortDirection === 'asc' ? 'ascending' : sortDirection === 'desc' ? 'descending' : 'none'}
  tabIndex={0}
  onClick={toggleSort}
  onKeyDown={e => e.key === 'Enter' && toggleSort()}
>
  Tanggal
</th>

Screen Reader Considerations

Live Regions

Use aria-live for dynamic content updates:

tsx
{/* Toast container */}
<div aria-live="assertive" aria-atomic="true" className="sr-only">
  {toastMessage}
</div>

{/* Loading state */}
<div aria-live="polite" aria-busy={isLoading}>
  {isLoading ? 'Memuat data...' : content}
</div>

Skip Navigation

Every page must include a skip-to-content link as the first focusable element:

html
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-brand focus:text-white focus:rounded-lg">
  Langsung ke konten utama
</a>

Visually Hidden Text

For context that screen readers need but sighted users can infer:

css
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

Touch Targets (Mobile)

All interactive elements on mobile must meet minimum touch target sizes:

ElementMinimum SizeRecommended
Buttons44x44px48x48px
Icon buttons44x44px44x44px (with padding)
List items (tappable)44px height48px height
Checkboxes44x44px tap area24px visual, 44px tap
Toggle switches44x24px visual44x44px tap area
Tab bar items44px heightFull tab width

Tap area vs visual size

The visual element can be smaller than 44px, but the tap target (via padding or transparent hit area) must be at least 44x44px.

Reduced Motion

Respect the prefers-reduced-motion media query:

css
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Elements affected:

AnimationDefaultReduced Motion
Page transitions300ms slideInstant
Hover scale effects150ms scaleNo scale
Skeleton pulseContinuousStatic gray
Member card tiltGyroscope-drivenStatic
Toast slide-in200ms slideInstant appear
PIN dot fillScale animationInstant fill
Progress barsSmooth transitionInstant

React Native

ts
import { AccessibilityInfo } from 'react-native'

const [reduceMotion, setReduceMotion] = useState(false)

useEffect(() => {
  AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion)
  const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduceMotion)
  return () => sub.remove()
}, [])

Testing Tools

ToolPlatformPurpose
axe DevToolsChrome extensionAutomated WCAG checks
LighthouseChrome DevToolsAccessibility audit score
VoiceOvermacOS / iOSScreen reader testing
TalkBackAndroidScreen reader testing
Contrast CheckerWebAIMManual contrast verification
Tab keyAllKeyboard navigation testing

Required before launch

Every new screen or component must pass:

  1. axe DevTools scan with zero critical/serious violations
  2. Full keyboard-only navigation test
  3. VoiceOver or TalkBack walkthrough for key flows
  4. Color contrast verification for all text

RetailOS - Sistem ERP Retail Modern untuk Indonesia