A real WCAG 2.1 AA floor on a static site — what shipped and what we deferred
The decision, before the engineering
Asotele’s existing positioning is structural — multilingual financial intelligence for the Nigerians excluded by English-only banking communication. The exclusion stack doesn’t stop at language. Roughly 15% of Nigerians have some form of disability (WHO/World Bank baseline estimates, ~27M+ people); disabled Hausa or Pidgin speakers face compounded exclusion against a wall already too high for everyone in their language community.
The decision to treat disability inclusion as a deliberate workstream — voice-first interaction, audio briefings in the four target languages, screen-reader-compatible UI — was made this week and committed in the project memory regardless of any specific grant outcome. The substrate already exists: NaijaVoices (1,867 hours of Hausa/Igbo/Yoruba speech from the Lacuna Fund), BibleTTS Hausa+Yoruba (CC-BY-SA, 86 hours each, studio quality), four shipped multilingual sentiment classifiers. The integration is what funding accelerates; the commitment itself doesn’t depend on it.
That decision was easy. What follows is harder.
What we shipped
A real WCAG 2.1 AA floor, in one session, on a Cloudflare Pages static site with a Cloudflare Access-gated advisor portal layered on top.
Structural foundation
- Skip-to-content link on every page (focusable on first Tab press, hidden until focused, lands on the main content region — standard WCAG 2.4.1)
- Semantic landmarks:
<main id="main-content">,<nav aria-label="Main navigation">,<footer>,<aside>where appropriate, everyportal-quicklinks<section>upgraded to a real<nav aria-label>element - Visible focus ring at high contrast — 2px outline of
--ng-green-darkwith 2px offset, applied globally via:focus-visible(not:focus, so mouse users don’t see ring spam) .sr-onlyutility for screen-reader-only content- Heading hierarchy across every rendered page: h1 → h2 → h3, no skipped levels. Footer column headings upgraded from
<h4>to<h3>to close the jump.
Forms — the area where most static sites quietly fail
- Every
<label>has an explicitfor=matched to its input’sid=. No proximity-only labels. aria-required="true"on every required inputaria-describedbylinking field hints (e.g., “We use this to contact you about your application”) to the input itself, so screen readers announce the hint when focus lands- The honeypot anti-spam field gets
aria-hidden="true"so screen readers don’t waste user time on the bait - Error and success message regions tagged with
role="alert" aria-live="assertive"(errors — interrupt the user) androle="status" aria-live="polite"(success — announce when convenient) <noscript>fallback explaining that the form requires JavaScript and offering an email-based alternative — a small but real assistive-tech population uses JS-disabled configurations
The demo video — the most-likely-to-fail surface for a fintech site
Asotele’s home page carries a 40-second product demo video with no spoken audio: every frame is on-screen text. For a sighted user this is fine. For a blind tech journalist visiting from a screen reader, this would have been forty seconds of silence.
What we shipped:
- A
<details>element below the video with a full written transcript, frame by frame, ending in the final closing card (“Asotele. Built in Nigeria. Open by design.”) - A WebVTT captions file (
/assets/asotele-demo.en.vtt) wired via<track kind="captions" srclang="en" label="English captions" default>so the native browser caption controls work for users watching with sound off aria-describedbylinking the<video>element to the transcript region, so screen readers announce that the transcript exists when the user focuses the video
Color and motion
--ink-mutedbody color bumped from#767676to#6a6a6a— body-text contrast rises from 4.54:1 (just barely AA) to 5.7:1 (comfortable for low-vision and dyslexic readers)prefers-reduced-motion: reducemedia query caps animations and transitions at near-zero for users who request it (vestibular-disorder users see no surprise motion)prefers-contrast: moremedia query for Windows High Contrast Mode and macOS Increase Contrast — focus ring thickens to 3px, link underlines become permanent, muted text deepens to full ink
External links
- Every
target="_blank"link now carries a visible “↗” icon (wrapped inaria-hidden="true"so it doesn’t read as “arrow”) AND a screen-reader-only “(opens in new tab)” span. Sighted users get the visual cue; screen-reader users hear the announcement; nobody hears “arrow”.
Tables
- A
render_md()post-process addsscope="col"to every<th>in blog post tables. NVDA and JAWS announce the column header on every cell when navigating tabular data — substantially clearer than scope-less headers.
Nigerian-language phrase tagging
A small but proud one. Markdown-rendered <code> blocks containing distinctively-Nigerian-language characters (the Hausa-only ɓ ɗ ƙ ƴ, the Igbo subdot ụ and ị, the Yoruba ẹ ṣ ń with tonal marks) get an automatic lang="ha|yo|ig" attribute added by the build pipeline. So when a screen reader hits <code lang="yo">pẹ́tírólù</code> in a blog post about the Yoruba pilot, it can use Yoruba pronunciation rules instead of fighting the English defaults.
The heuristic is deliberately conservative: only tags when the character class is unambiguous, leaves anything else untagged for the screen reader’s own auto-detection to handle. False positives are worse than no tag.
Advisor portal (Cloudflare Access gated)
Same treatment, applied to the post-CF-Access dynamic surface that signed-in advisors see:
- Form labels in
renderProfileForm()andpostQuestionCard()wired withfor/idlike the public form - Error and success boxes carry
roleandaria-live - Section headings rendered via
<div class="portal-h">gotrole="heading" aria-level="3"to preserve the visual design while exposing the right semantic tree to screen readers <noscript>fallback explains the portal requires JS and offers an email path
The receipts
A full inventory, plus the commitment language, lives at asotele.apexgridapps.com/accessibility. That page is also linked from every footer. The intent is auditability — anyone using an assistive technology can read the inventory, check the work against their own tools, and tell us where we’re wrong.
What we deferred, and why
Honesty matters more than a perfect-sounding statement.
Language switcher — A real multilingual interface across the four target languages plus English would require native-speaker-reviewed Hausa / Yoruba / Igbo / Pidgin UI copy. We refused to AI-translate UI copy without native review — our no-hallucination discipline applies to interface text the same way it applies to financial claims. The language switcher is committed work; it’s not deferred because we don’t think it matters, it’s deferred because shipping it without a native-reviewer pass would be the wrong shape of progress.
Voice-first SME tier — TTS in Hausa / Yoruba / Igbo / Pidgin; voice input via Nigerian-language ASR. This is the largest piece of the disability-inclusion commitment and it’s sequenced for the next 12 months. The substrate is in hand (NaijaVoices for ASR, BibleTTS for two of the four target languages); the integration is the work. Honest about the timing: shipping the substrate is not the same as shipping the experience, and we won’t pretend otherwise on the accessibility page.
Sign-language considerations — Nigerian Sign Language (NSL) is a future workstream and depends on community partnership we don’t yet have.
Custom dialog replacing native alert()/confirm() in the advisor portal’s founder workflow — native modals are accessible by default; the design could be more consistent. Founder-only, low blast radius; we deferred.
Frame-accurate WebVTT caption timings — the captions file we shipped tonight uses approximate timing cues derived from the demo’s storyboard manifest, not from a frame-accurate re-cut. The captions appear at roughly the right moments, but a careful pass with a video editor would tighten them. On the maintenance queue, not the launch queue.
Legacy blog posts (pre-2026-06-16) — the automated Nigerian-language lang annotation runs on the build pipeline so all newly-built posts get tagged; older posts predate the heuristic and may carry Nigerian-language phrases without explicit lang attributes. Rolling forward from most-recent posts is the sweep pattern.
Why this matters beyond compliance
Two things, briefly.
The first is structural. An accessibility floor isn’t a one-time launch milestone; it’s the discipline of refusing to ship regressions. We’ve added an /accessibility page so anyone can verify the floor we claim. The harder discipline is not removing things from it. Every UI change from here forward inherits the constraint that the focus ring still has to land, the labels still have to wire, the captions still have to play. That’s a real constraint and we’re committing to it publicly so we’re accountable to it.
The second is mission alignment. Asotele’s positioning has always been about closing exclusion — language exclusion first, but never only. Financial information in Nigeria is gated by language, gated by literacy register, and gated by the technology accessibility floor of the apps and websites that publish it. A platform that fixes one of those gates and ignores the other two isn’t really doing inclusion; it’s doing market segmentation with a nicer story attached. Closing the disability gate as a deliberate workstream — not a checkbox, not a grant-application moment — is what makes the inclusion thesis serious.
What’s next
The voice-first SME tier is the largest piece of the work that’s still ahead of us; that’s a 12-month workstream with sequenced milestones. In the near term: we’re opening an advisory channel to the Nigerian disability-rights and accessibility-tech community to make sure the floor we just shipped is actually the floor people use, not just the floor we measured. That’s its own posture — disability advisors get the same async-only, no-equity, honorarium-available, public-attribution-at-your-discretion terms as everyone else on the advisory committee, and the first contact is always a real product question, not a credentialing ask.
The receipts are at asotele.apexgridapps.com/accessibility. If anything we claim there doesn’t work with your assistive technology, please tell us — accessibility issues are bugs, not feature requests.