Accessibility
Accessibility
Nzinga is for people speaking their own languages on the phones they already have: in sunlight, on slow networks, with any level of reading or eyesight. The rules below are measured, and where the app falls short today the gap is listed.
Contrast
apps/mobile/utils/color-theme.ts app/contexts/ThemeColors.tsx docs/site/site.css
Ratios are computed with the WCAG 2 relative-luminance formula from the token values. Body text needs 4.5:1 (AA); large text (18pt, or 14pt bold) and the edges of controls need 3:1.
AAA 7:1 or more AA 4.5:1 or more Large or UI only 3:1 to 4.5:1 Fail under 3:1
App, Dark Theme (Default)
| Pair | Colours | Ratio | Result |
|---|---|---|---|
| Text on background | #F7F7FA on #0B0B10 | 18.36 | AAA |
| Text on secondary (cards) | #F7F7FA on #16161D | 16.83 | AAA |
| Subtext on background | #A8A8B3 on #0B0B10 | 8.34 | AAA |
| Subtext on secondary | #A8A8B3 on #16161D | 7.64 | AAA |
| Accent (Volt) text on background | #C8FF2E on #0B0B10 | 16.64 | AAA |
| Cyan text on background | #19E0E6 on #0B0B10 | 12.01 | AAA |
| Flame text on background | #FF6A1F on #0B0B10 | 6.86 | AA |
| Magenta text on background | #FF2D7A on #0B0B10 | 5.52 | AA |
| Violet text on background | #7C5CFF on #0B0B10 | 4.52 | AA |
| Input placeholder (40%) on secondary | #707075 on #16161D | 3.65 | Large or UI only |
App, Light Theme
| Pair | Colours | Ratio | Result |
|---|---|---|---|
| Text on background | #0B0B10 on #F7F7FA | 18.36 | AAA |
| Subtext on background | #55555F on #F7F7FA | 6.89 | AA |
| Subtext on secondary | #55555F on #FFFFFF | 7.37 | AAA |
| Accent text on background | #5C7A00 on #F7F7FA | 4.63 | AA |
| Violet text on background | #7C5CFF on #F7F7FA | 4.06 | Large or UI only |
| Magenta text on background | #FF2D7A on #F7F7FA | 3.32 | Large or UI only |
| Cyan text on background | #0FA9AE on #F7F7FA | 2.69 | Fail |
| Flame text on background | #FF6A1F on #F7F7FA | 2.68 | Fail |
| Volt text on background | #C8FF2E on #F7F7FA | 1.10 | Fail never used as text |
| Input placeholder (40%) on secondary | #9D9D9F on #FFFFFF | 2.71 | Fail |
Content on Colour (Both Themes)
| Pair | Black #0B0B10 | White #F7F7FA | Rule |
|---|---|---|---|
On Volt #C8FF2E | 16.64 AAA | 1.10 Fail | Always black. |
On Cyan #19E0E6 | 12.01 AAA | 1.53 Fail | Black. |
On Flame #FF6A1F | 6.86 AA | 2.68 Fail | Black. |
On Magenta #FF2D7A | 5.52 AA | 3.32 Large | Black for text; white only for large type. |
On Violet #7C5CFF | 4.52 AA | 4.06 Large | Black. This is why session panels use black content. |
This Site's Light Palette
| Pair | Colours | Ratio | Result |
|---|---|---|---|
| Text on page | #0B0B10 on #FFFFFF | 19.63 | AAA |
| Text on panels and sidebar | #0B0B10 on #F7F7FA | 18.36 | AAA |
| Subtext on page | #55555F on #FFFFFF | 7.37 | AAA |
| Subtext on panels | #55555F on #F7F7FA | 6.89 | AA |
| Subtext on hover grey | #55555F on #ECECF1 | 6.26 | AA |
| Accent text on page | #5C7A00 on #FFFFFF | 4.95 | AA |
| Accent text on panels | #5C7A00 on #F7F7FA | 4.63 | AA |
| Black on Volt fills (nav indicator, marks, tags) | #0B0B10 on #C8FF2E | 16.64 | AAA |
| Volt on white | #C8FF2E on #FFFFFF | 1.18 | Fail fills only, never text |
| Search field edge | #85858E on #FFFFFF | 3.66 | UI pass |
| Dividers | 10% black on #FFFFFF | 1.24 | Decorative, not required |
In dark mode the site uses the app's dark values: text 18.36, subtext 8.34 on the page and 8.17 on the sidebar.
To fix in the app's light theme. Cyan and Flame fail as text on the light ground, and Magenta and Violet pass only for large text. Today they colour small text in light mode: the In review and Not accepted badges, the Withdraw button's label, and input error lines. Give each a darkened text value for light mode, as accent already does for Volt, or keep these labels in the text colour with a coloured dot or icon.
Input placeholders at 40% fail in light mode and pass only for large text in dark mode. Raise them to the subtext colour.
Touch Targets
apps/mobile/CLAUDE.md §3b components/Header.tsx components/ConsentOption.tsx
- Every tappable thing is at least 44 by 44 points. Buttons are 44pt tall (
py-3), and the Welcome pills match. - Small icons get a larger hit area: header icons are 28pt drawn with an 8pt
hitSlopon every side, which makes 44pt. - Choice rows are pressable across their full width, not only on the 24pt radio.
- Keep at least 8pt between separate targets, as the chip and action rows do (
gap-2).
Text Size
components/ThemedText.tsx tailwind.config.js
- All copy goes through
ThemedText, which follows the phone's text size setting up to 1.25 times (maxFontSizeMultiplier). Body text at 14pt grows to 17.5pt. - Layouts must hold at 1.25 times: titles wrap rather than truncate, rows grow in height, and nothing important sits in a fixed-height box.
- The cap protects layouts but limits people who set very large text. Review it with readers who use the largest sizes before launch.
Screen Readers
components/ConsentOption.tsx components/RecordButton.tsx components/AudioPlayerBar.tsx app/(join)/welcome.tsx
The app sets roles and labels on its controls: buttons, headers, radio groups for the four choices, switches, tabs, a timer and an adjustable playback position. Examples from the code:
| Element | Role | What is read |
|---|---|---|
| A choice of who can use it | radio, in a radiogroup | "Nzinga's models. suggested. Teaches Nzinga your language. Paid", with selected and disabled states |
| Record button | button | "Start recording" or "Stop recording" |
| Playback bar | adjustable | "Playback position" |
| Welcome slide | text | "1 of 3. Your language. Your voice. Your terms. Record voice notes…" |
| Screen titles | header | The title, so readers can jump between sections |
| Status badge | text | The word itself: "In review", "Accepted" |
- Every icon-only control has a label that says what it does, not what it shows: "Open menu", not "Three lines".
- Read order follows the screen: title, then content, then the footer action.
- Money is read as words the reader can say: "$0.42", never a bare "42".
Motion
components/AnimatedView.tsx components/RecordButton.tsx docs/ds/nzinga-ds.css
- Motion confirms an action; it never carries information on its own. The live recording state also says "Recording" in words and shows a timer.
- When the phone asks for reduced motion, replace slides and flips with a plain fade or no animation, and stop the breathing record button.
Not built yet The app does not read the Reduce Motion setting today. In this site All transitions stop under prefers-reduced-motion.
Low Literacy and Low Bandwidth
SPEC.md §3.3–3.4 apps/mobile/CLAUDE.md lib/sessions.ts lib/prompts.ts
The product is speaking, not typing. The main action is one large button, ideas to record are short spoken-style prompts, and Record Well teaches in five one-line lessons.
Every icon comes with a word, every status is a word, and choice rows are one line with a short pay tag.
Audio stays on the phone; only small details are sent. Screens load a few small JSON answers. The spec plans low-bitrate Opus audio and uploads that can wait for Wi-Fi.
Every list has an error state with Try again. Recordings, progress and payout methods are kept on the phone, so nothing is lost when a request fails.
Colour-Blind Safe States
components/StatusBadge.tsx Timelines in recording-detail.tsx components/ConsentOption.tsx
No state is shown by colour alone. Badges carry a word, timelines carry a check or a cross, and the selected choice carries a check as well as its Volt edge.
A word in every badge: "Accepted", "In review", "Not accepted", with a dot for states still moving.
A green dot for accepted and a red dot for rejected.
Indistinguishable for many readers, and meaningless to a screen reader.