Browser extension · Designed & built end-to-end

One profile, accessible
on every page.

Accessibility on the web is fragmented — every site asks you to reconfigure font size, contrast, and reading aids from scratch. Lens fixes that: one profile follows you everywhere. I designed and built the extension, marketing site, design system, and Chrome Web Store submission package solo.

Live · site & extension

Open the live site

The marketing site with a live before/after demo. Install the extension to carry your profile across every page you visit.

Open site
Reference

Design system

Forest-green parchment tokens, Atkinson Hyperlegible type, accessible radii, the aperture mark — the full, code-truthful spec.

View system
Engineering

Under the hood

Manifest V3 · Cloudflare Workers · Anthropic API · Shadow DOM. A stateless backend, encrypted sync, and a full MV3 extension — zero auth friction.

See the build
The problem

Accessibility is fragmented. Every page starts from zero.

Roughly one in five people uses the web with some kind of accessibility need — larger text, higher contrast, a color-blindness filter, reduced motion, a simplified layout. The tools exist. The problem is that every tool is per-site, per-session, per-device. There's no memory, no continuity, no profile that follows you. Reader-mode helps on articles; it breaks on dashboards. System-level settings help with contrast; they can't reflow a dense form. Every accommodation is a fresh negotiation with the page.

The flow

One profile, five layers of adaptation.

I mapped the adaptation pipeline on paper first — from onboarding to a page that reshapes itself around you — before writing a single line of content script.

a single adaptation pass from "who are you?" to "the page, your way"
1
onboarding
7 questions,
60 seconds,
no account
2
profile
stored locally,
E2E-encrypted
sync optional
3
content
script
shadow DOM
reading view
injected
4
AI adapt
Cloudflare
Worker streams
Anthropic SSE
5
your page
reflow, filters,
alt-text, forms —
all applied
profile never leaves the device
show original anytime ↑
Three design bets

The decisions that shaped everything else.

"One profile, everywhere" is easy to say. Making it trustworthy, instant, and invisible required three early bets — on where data lives, how fast the first page adapts, and what the extension must never do.

01

Profile lives on the device, not a server.

Your font size, contrast level, color-blindness filter, and reading preferences are stored in chrome.storage.local — no account, no server, nothing to leak. The optional cross-device sync uses AES-GCM encryption with a key that never leaves your devices. The adaptation API is stateless: it receives the page content and profile settings per call, stores nothing.

Cloud profile, login wall  →  Local-first, opt-in sync
02

Adaptation streams — the page moves as it renders.

The Cloudflare Worker streams Anthropic's SSE response directly to the content script. The reading view starts populating within ~200 ms of clicking "Adapt this page" — paragraphs appear one by one as the model produces them. There's no blank white screen followed by a page swap. The incremental feel makes it obvious something real is happening, and makes a 2–3 s total latency feel fast.

Buffer full response, then render  →  Stream deltas into shadow DOM
03

The extension never reads sensitive pages.

A hardcoded domain allowlist (banking, medical, government, legal) silently skips adaptation without prompting. The sensitive-domain logic lives in the profile layer, not the UI, so it can't be accidentally overridden. No content from those domains is ever sent to the backend — the popup simply shows "Lens is paused here for your safety" and offers a manual override for power users who knowingly want it.

Adapt everything, ask later  →  Sensitive domains off by default
UX choices that matter

Small calls that change how the whole thing feels.

Accessibility tooling has a trust problem — most of it feels clinical, brittle, or broken. The micro-decisions below are about earning trust one interaction at a time.

a.

Onboarding asks, never assumes.

Seven conversational questions in ~60 seconds: reading level, contrast preference, color-blindness type, preferred font size, whether you use a screen reader, whether you want form simplification, and a free-text "anything else?" The profile is populated from answers, not inferred from browsing. Users know exactly what Lens knows about them — because they typed it.

b.

"Show original" is always one click away.

The floating Lens badge stays pinned in the corner after adaptation. One click toggles back to the original page. There's no cognitive cost to trying an adaptation — you can always undo it instantly. This made users far more willing to try the "full reflow" mode in testing, because the worst case is one click to reverse.

c.

Color-blindness filters use actual CSS SVG matrices.

Eight filters — Protanopia, Deuteranopia, Tritanopia, Protanomaly, Deuteranomaly, Tritanomaly, Achromatopsia, and Achromatomaly — are each a precise SVG feColorMatrix applied as a root-level CSS filter. They're exact perceptual simulations, not approximations. Selecting one is instantaneous, non-destructive, and reversible.

d.

Form simplification is opt-in per form, not global.

The form-simplification module presents one question at a time with a large text field. Activating it on a specific form doesn't affect anything else on the page. Users who want the simplified view can trigger it on a dense checkout form; users who need to see all fields at once aren't penalised. Accessibility shouldn't override agency.

e.

AI alt-text labels the gap, not the image.

When the AI alt-text module encounters an unlabeled image, it generates a description and injects it as the actual alt attribute — not a tooltip, not a caption. The injected text is prefixed with "(AI-generated)" so screen reader users know the provenance. Honest about its limits; useful where it counts.

f.

The screen-reader companion mode is cooperative, not competitive.

Lens's companion mode works alongside NVDA, JAWS, and VoiceOver — it doesn't try to replace them. It patches missing ARIA roles, injects landmark headings where the page has none, and normalises reading order. The extension announces itself to the active screen reader via a live region and defers to the reader's navigation model. An accessibility extension that breaks screen readers was the one thing I refused to ship.

Behind the design

One legible system, used everywhere.

Lens runs on a "forest canvas" system — warm parchment background, near-black ink, deep-forest-green primary, and sage as the secondary. Atkinson Hyperlegible is the type family throughout: it was designed specifically for low-vision readers, which makes it not just on-brand but the right technical choice. The full spec lives on the Design page.

Color · forest canvas palette

Green that communicates safety. Parchment that rests the eye.

The palette is built around low-contrast fatigue — warm parchment backgrounds (#faf9f5) instead of cold white, near-black ink (#141413) instead of pure black. The deep forest green carries trust and calm without the clinical coldness of a standard blue primary. Sage is used only for secondary actions and highlights.

Forest
#2a4d3a
Primary action, CTAs
Mid Forest
#4a6f5a
Hover, active states
Sage
#b4cbc3
Buttons, highlights
Canvas
#faf9f5
Page background
Parchment
#efe9de
Card surfaces
Ink
#141413
Primary text
Type · designed for low vision

Atkinson Hyperlegible — the accessible choice is the right choice.

Atkinson Hyperlegible 700 Display · headings
Your profile, every page.

Designed by the Braille Institute specifically for readers with low vision. Exaggerated letterform distinctions — I vs l vs 1, O vs 0 vs D — reduce misreads without looking clinical. It's the only typeface that earns a spot in an accessibility product on merit, not aesthetics.

Atkinson Hyperlegible 400 Body · UI · settings copy
Adapt this page to your reading profile — larger text, higher contrast, simplified forms, and colour adjustments applied instantly.

One family at regular weight for body text. The same unambiguous letterforms carry through to every piece of instructional copy in the extension.

JetBrains Mono Eyebrows · version tags · spec labels
MV3 · CLOUDFLARE WORKERS · v1.2.0

Mono only where a label needs to feel technical — section eyebrows, API version tags, and the design-spec voice you're reading now.

Shape · 4 radii · legible corners

Soft but not ambiguous.

Corners are rounded to reduce visual sharpness on high-contrast displays, but not so round that buttons and cards lose their shape distinction. Four steps, from the pill badge down to the smallest chip.

Pill Tags, status badges, nav links
9999px
Card Panels, popup container
16px
Button Primary and ghost CTAs
12px
Chip Small labels, filter badges
6px
Identity · the aperture mark

A lens. A reading eye. The same shape.

Lens logo — a forest-green circle with concentric rings suggesting a lens aperture
Lens
Aperture mark — a focusing eye

The mark is a forest-green circle with two concentric rings — part optical lens, part reading eye, part aperture. It works at 16 × 16 px in the browser toolbar and at marketing-page scale. The forest-green radial gradient is the single loudest brand element; every other decision defers to legibility. Icons throughout the extension use a consistent 1.5 px stroke with rounded caps, matching the body weight of Atkinson Hyperlegible.

Motion · purposeful, not decorative

Motion that explains, not entertains.

adapt
Page adapting content fades in paragraph by paragraph as SSE streams
saved
Profile save confirmation pulse, 300ms ease-out
filter
Filter applied subtle bob confirms the switch, no full-page flash
blocked
Sensitive domain gentle shake — "paused here" without alarming
See the full design spec Open the live site
What's deliberately missing

The cuts I'd defend.

An accessibility product needs to earn trust before it earns features. Every item below was a real temptation — and a real cut made on purpose.

No cloud profile

Your profile is on your device, not our server. A cloud profile would enable seamless cross-device sync without any friction — but it would also mean a database of every user's disability profile, a breach target, and a GDPR Art. 9 liability. The trade-off is explicit: local-first by default, opt-in encrypted sync for power users. No server, no leak.

No passive learning

Lens never silently observes your reading behaviour. The per-site fine-tuning module (site-tuning.js) tracks only explicit manual adjustments the user makes — not reading time, scroll depth, or whether they toggled back to original. On-device only. Nothing inferred without action.

No automatic adapt

Adaptation only runs when you click "Adapt this page." An always-on mode that adapts every page on load would be faster, but would also consume API credits, introduce latency on pages you don't need adapted, and trigger on pages where adaptation is harmful. Explicit beats automatic for an accessibility tool — you should always know when Lens is active.

No social layer

No shared profiles, no community presets, no leaderboard of "most accessible sites." These features would add engagement but would require accounts, moderation, and infrastructure. The first goal is for one person to have a better reading experience on one page. Everything else comes after that works reliably.

Under the hood

Built for cross-browser, built for privacy.

One MV3 extension ships to Chrome and Edge on day one, with Firefox and Safari as first-tier targets. The backend is a stateless Cloudflare Worker that never logs content — streaming Anthropic's API without storing a word of what it processes.

Extension

Manifest V3 · Chrome / Edge / Firefox

  • Service worker background, shadow-DOM content script
  • 8 CSS SVG color-blindness filter matrices — instantaneous
  • Popup + full settings editor + conversational onboarding
  • chrome.storage.local — zero network on profile reads
Backend

Cloudflare Workers · Anthropic API

  • Stateless adaptation API — receives page + profile, stores nothing
  • SSE streaming from Anthropic directly to the content script
  • Stripe Free $0 / Pro $4.99 entitlement layer
  • CORS locked to chrome-extension://* in production
Site

Static HTML · Atkinson · Vercel

  • Marketing site with live original/adapted demo
  • Plain-language privacy policy + GDPR Art. 9 basis
  • Chrome Web Store listing, permission justifications, demo script
  • i18n framework — EN / HE / ES live; DE / FR / PT / AR ready