DEV Community

peekvision
peekvision

Posted on

Building an Accessible Browser-Based Vision Test with Flutter

Building an online vision screening tool sounds simple at first: display letters, ask the user whether they can read them, and progressively reduce the letter size.

In practice, building something usable across desktop and mobile browsers involves much more than that.

I recently worked on PeekVision, a browser-based visual acuity and color vision screening application built with Flutter. One of the most interesting parts of the project was making the test usable across different devices while also adding an accessible, voice-assisted screening option.

You can try the project here:

https://www.peekvision.io/

Methodology and limitations:

https://www.peekvision.io/methodology.html


The Goal

The goal of PeekVision is to provide a simple preliminary browser-based vision screening experience.

The application currently includes:

  • Distance visual acuity screening
  • Near / reading vision screening
  • Separate right-eye, left-eye, and both-eyes testing
  • Color vision screening
  • An accessible vision test
  • Voice-assisted responses
  • A screening report
  • Mobile and desktop browser support

It is important to emphasize that this is a screening tool, not a diagnostic medical device.

Screen size, browser zoom, display scaling, viewing distance, lighting, and device configuration can affect the results.


Why Browser-Based Vision Testing Is Difficult

Traditional eye charts are designed around a known physical size and a known viewing distance.

A browser does not give us that certainty.

Two devices may both report a width of 400 logical pixels, but the physical size of those pixels can be very different.

For example:

  • A phone
  • A tablet
  • A laptop
  • A desktop monitor

may all render the same Flutter widget differently in physical space.

That means browser-based visual acuity should be treated as a screening estimate, especially at very small letter sizes.

For PeekVision, the methodology clearly communicates this limitation instead of presenting browser measurements as clinically calibrated Snellen measurements.


Progressive Visual Acuity Testing

The standard test begins with larger letters and progressively moves toward smaller levels.

A simplified progression looks like this:

static const List<double> steps = [
  0.4,   // approximately 20/50
  0.3,   // approximately 20/40
  0.2,   // approximately 20/32
  0.1,   // approximately 20/25
  0.0,   // approximately 20/20
  -0.1,  // approximately 20/16
  -0.2,  // approximately 20/13
];
Enter fullscreen mode Exit fullscreen mode

Each row contains five letters.

The user selects:

  • I Can Read
  • I Can't Read

If they can read the row, the application advances to a smaller level.

If they cannot, the most recent successfully read level is recorded.

The same process is performed for:

  1. Right eye
  2. Left eye
  3. Both eyes

This allows the report to show differences between the two eyes as well as combined vision.


Why Five Letters?

Using multiple letters is more useful than showing a single character because it reduces the chance of a correct response simply through guessing.

PeekVision generates five-character rows using a restricted letter set:

static const String alphabet = 'CDHKNORSVZ';
Enter fullscreen mode Exit fullscreen mode

The row is generated dynamically:

line = List.generate(
  5,
  (_) => alphabet[random.nextInt(alphabet.length)],
).join(' ');
Enter fullscreen mode Exit fullscreen mode

The exact letters are intentionally not announced during the standard test.


Accessibility Was a Separate Design Problem

A traditional visual-acuity interface can still be difficult for users who have:

  • limited vision
  • difficulty locating small controls
  • motor limitations
  • difficulty reading instructions
  • screen-reader or voice interaction preferences

Instead of simply enlarging the normal interface, PeekVision includes a separate Accessible Vision Test.

The accessible workflow uses:

  • high contrast
  • large targets
  • simplified controls
  • spoken instructions
  • speech recognition
  • manual fallback controls

The letters themselves are never spoken aloud, because doing so would invalidate the test.


Voice-Assisted Interaction

The accessible mode uses text-to-speech for instructions and speech recognition for user responses.

Conceptually, the flow looks like this:

Speak instruction
      ↓
Display five letters
      ↓
Wait for user response
      ↓
Recognize "I can read" or "I can't read"
      ↓
Advance to next level
Enter fullscreen mode Exit fullscreen mode

A manual button remains available because microphone permissions and speech recognition support vary between browsers and devices.

This fallback is important for accessibility.

Accessibility should not depend on a single input method.


Responsive Design Became More Important Than Expected

One of the most practical challenges came from mobile screens.

On desktop, the visual acuity setup page had enough room for:

  • the test type selector
  • vision-correction selector
  • test-distance text
  • instructions
  • the letter row
  • response buttons

On a smaller mobile viewport, the same layout could push the letters outside the available area.

The solution was not simply to shrink everything.

Shrinking vision-test letters would directly affect the purpose of the interface.

Instead, the mobile layout changes once the active test begins.

During setup, the user sees:

Test type
Vision correction
Viewing distance
Instructions
Start test
Enter fullscreen mode Exit fullscreen mode

After the user starts testing, those controls are hidden on small screens:

Testing Right Eye
Five-letter row
Screening level
I Can Read
I Can't Read
Enter fullscreen mode Exit fullscreen mode

This frees vertical space while preserving the important testing elements.

A simplified Flutter condition looks like this:

final isMobile =
    MediaQuery.of(context).size.width < 600;

final isActivelyTesting =
    stage == Stage.testingRight ||
    stage == Stage.testingLeft ||
    stage == Stage.testingBoth;

if (!(isMobile && isActivelyTesting)) {
  // Show configuration controls.
}
Enter fullscreen mode Exit fullscreen mode

This was much better than reducing the letter size.


Preventing Letter Clipping

Even after simplifying the mobile layout, very large letters could potentially exceed their available space.

Using FittedBox provides an additional safety layer:

FittedBox(
  fit: BoxFit.scaleDown,
  child: Text(
    line,
    maxLines: 1,
    textAlign: TextAlign.center,
  ),
)
Enter fullscreen mode Exit fullscreen mode

The goal is to prevent accidental clipping while still allowing the intended letter-sizing logic to control the test.


Distance and Near Vision

PeekVision supports two screening modes.

Distance vision

The user is instructed to remain approximately:

10 ft / 3 m

from the screen.

Near vision

The screen is held approximately:

40 cm / 16 in

from the eyes.

The application records which screening mode was used so the report can distinguish near and distance results.


Why Screen Calibration Matters

One of the biggest limitations of online vision tests is physical screen calibration.

CSS pixels, Flutter logical pixels, and physical millimeters are not necessarily equivalent across devices.

A future improvement could allow the user to calibrate the display against a known physical object.

For example:

Place a standard-size card against the screen
          ↓
Adjust an on-screen rectangle
          ↓
Calculate pixels per millimeter
          ↓
Use the value when sizing optotypes
Enter fullscreen mode Exit fullscreen mode

Even with calibration, browser zoom, operating-system scaling, viewing distance, and posture can still affect the result.

That is why the current application clearly describes its result as preliminary screening.


Reporting Results Clearly

A screening application should not simply return:

20/40
Enter fullscreen mode Exit fullscreen mode

without context.

PeekVision reports:

  • Right Eye (OD)
  • Left Eye (OS)
  • Both Eyes (OU)
  • Screening level
  • Test type
  • Correction used
  • Test distance
  • Comparison with the normal 20/20 reference
  • Eye-to-eye comparison
  • Screening limitations

For very small letter levels, the report explicitly warns that the result is sensitive to physical display calibration.


Color Vision Screening

PeekVision also includes a color vision screening workflow.

Color screening has a different set of browser limitations:

  • brightness
  • display color profile
  • Night Shift / Night Mode
  • True Tone
  • blue-light filters
  • room lighting

Because of those variables, online color plate results should also be treated as preliminary screening rather than a diagnosis.


A Lesson From Building It

The biggest lesson from this project was that accessibility and responsive design should not be added at the end.

For a visual application, accessibility changes the architecture.

The correct question was not:

How can I make the desktop interface smaller?

It was:

What information does the user actually need at this stage of the test?

That led to a cleaner mobile testing interface while preserving the more detailed setup screen.


Technology

PeekVision is built primarily with:

  • Flutter
  • Dart
  • Flutter Web
  • Text-to-speech
  • Speech recognition
  • Responsive Material UI

The project is designed to run directly in a modern web browser.


Try PeekVision

Live application:

https://www.peekvision.io/

Visual acuity information:

https://www.peekvision.io/visual-acuity-test.html

Accessible vision screening:

https://www.peekvision.io/accessible-vision-test.html

Methodology and limitations:

https://www.peekvision.io/methodology.html

Source project:

https://github.com/pradeepsambamurthy/vision_scanner_flutter


Final Note

Browser-based vision screening can make basic screening more accessible, but it should not be confused with a comprehensive eye examination.

A browser cannot currently reproduce every controlled condition of a professional clinical test.

The interesting engineering challenge is therefore not simply displaying an eye chart.

It is designing a useful screening experience while being transparent about what the technology can and cannot measure.

Top comments (1)

Collapse
 
devshakib profile image
K M Shahriar Hossain

Worth testing with a screen reader running: once Flutter web's semantics is on, a Text widget's content is exposed to assistive tech, so VoiceOver or TalkBack can read the five-letter row aloud. That's the one thing the test is designed never to do.

Wrapping the row in ExcludeSemantics, or Semantics(label: 'Letter row', excludeSemantics: true, child: …), keeps it visual-only while the buttons stay announced.

Related: Flutter web doesn't build the semantics tree until something asks for it. A screen-reader user first has to find a hidden "Enable accessibility" button. Calling SemanticsBinding.instance.ensureSemantics() in main() turns it on from the first frame, which matters for a mode built for people using assistive tech.