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:
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
];
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:
- Right eye
- Left eye
- 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';
The row is generated dynamically:
line = List.generate(
5,
(_) => alphabet[random.nextInt(alphabet.length)],
).join(' ');
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
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
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
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.
}
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,
),
)
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
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
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:
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)
Worth testing with a screen reader running: once Flutter web's semantics is on, a
Textwidget'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, orSemantics(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()inmain()turns it on from the first frame, which matters for a mode built for people using assistive tech.