Color-Blind Friendly Games in Flutter
Most bubble shooter games ask the player to do one thing: tell the colors apart. Match three red bubbles, pop them, and move on. For a player with color vision deficiency, that core loop is broken from the first level. The game is not hard. It is unreadable. Bubble Shooter: Bubble Pop, a relaxing bubble shooter I built in Flutter, takes a different approach. Every bubble carries its own symbol as well as a color, so players can match by shape as well as color. The symbols are on by default, the system supports deuteranopia, protanopia and tritanopia, and the game is built around low pressure: no timers, no pressure and no purchases, with progress saved on the device and no account needed. This article walks through how that design works in Flutter, and how to apply the same ideas to any game or app where color carries meaning. You can see the finished game in the Bubble Shooter case study. The rule: never encode meaning in color alone Color vision deficiency is not one condition. Deuteranopia, protanopia and tritanopia each confuse different pairs of colors, and what looks obvious to one player can be invisible to another. That is why the fix is not a better palette. A palette can be tuned for one type of color vision and still fail another. The fix is a second channel of information that does not depend on color at all. In Bubble Shooter, that second channel is a symbol. Each bubble type has a distinct shape drawn on the bubble, so a red bubble and a green bubble differ by shape even when their colors look identical to a player. The rule generalizes to any Flutter app: if two things must be told apart, they must differ by more than color. A status dot needs a label or an icon next to it. A chart series needs a line style or a marker shape as well as a color. An error state needs text, not just a red border. The practical test is simple. Take a screenshot, convert it to grayscale, and try to play. If the game is still readable, the second channel works. If it is not, the design still depends on color. Design the symbol system Symbols only help if they are part of the data model, not painted on afterwards. Each bubble type should carry its color and its symbol together, so the two can never drift apart: \`dart enum BubbleKind { ruby(Color(0xFFE53935), Icons.star), ocean(Color(0xFF1E88E5), Icons.circle), leaf(Color(0xFF43A047), Icons.favorite), sun(Color(0xFFFDD835), Icons.diamond), ; const BubbleKind(this.color, this.symbol); final Color color; final IconData symbol; } `\ The widget that draws a bubble then reads both fields from the same source of truth, and it announces itself to screen readers too: \`dart class BubbleWidget extends StatelessWidget { const BubbleWidget({ super.key, required this.kind, required this.showSymbols, }); final BubbleKind kind; final bool showSymbols; @override Widget build(BuildContext context) { return Semantics( label: 'Bubble: \${kind.name}', child: Container( decoration: BoxDecoration( color: kind.color, shape: BoxShape.circle, ), child: showSymbols ? Icon(kind.symbol, color: Colors.white, size: 18) : null, ), ); } } `\ Three details matter here. First, the symbol is drawn inside the bubble in white at a fixed size, so it stays legible at the smallest bubble size the game uses. Second, the widget carries a Semantics label naming the bubble kind, so a screen reader can describe the board. Third, the symbols are on by default. An accessibility feature that ships off is a feature most players never find, and the players who need it are the ones least likely to dig through settings to turn it on. Bubble Shooter keeps symbols on by default and lets players switch them off in Settings if they prefer a cleaner look. Make aiming readable too Matching is only half the game. Aiming is the other half, and it has the same problem: a bare drag gesture gives the player no feedback about where the shot will land. Bubble Shooter solves this with two aiming aids. Drag to aim shows a laser sight that bounces off the walls, and a ghost bubble shows exactly where the shot will land. Neither aid uses color to carry its message: the sight is a line the player can follow, and the ghost is a shape sitting in the landing position. The lesson is that feedback should be positional and geometric, not chromatic. A player should be able to aim by following a line and a shape, with color as decoration rather than information. The same idea applies outside games: a drag-and-drop target should show an outline where the item will land, and a slider should show a value label, not just a colored fill. Settings players can control Accessibility is personal, so the settings that control it should be real settings, not build-time constants. Bubble Shooter exposes the symbol toggle in Settings, along with separate controls for haptics, sound and music, and a reduce-motion option. Separate controls matter: a player may want haptics on and music off, or sound effects without music, and bundling everything into one accessibility mode switch takes that choice away. A small ChangeNotifier backed by SharedPreferences is enough to hold these preferences: \`dart class AccessibilitySettings extends ChangeNotifier { bool _showSymbols = true; bool _reduceMotion = false; bool _haptics = true; bool get showSymbols => _showSymbols; bool get reduceMotion => _reduceMotion; bool get haptics => _haptics; Future load() async { final prefs = await SharedPreferences.getInstance(); _showSymbols = prefs.getBool('show_symbols') ?? true; _reduceMotion = prefs.getBool('reduce_motion') ?? false; _haptics = prefs.getBool('haptics') ?? true; notifyListeners(); } Future setShowSymbols(bool value) async { _showSymbols = value; final prefs = await SharedPreferences.getInstance(); await prefs.setBool('show_symbols', value); notifyListeners(); } } `\ Persisting the settings is the point. A preference that resets every launch is worse than no preference at all, because the player has to fight the app before every session. Load the settings once at startup, and let every widget that cares read them from the same source. The pattern is the same one used for on-device privacy in Privacy-First Flutter Apps: On-Device Data: keep the data on the device, load it early, and never ask the player for an account to remember it. Respect motion preferences Bubble Shooter also ships a reduce-motion option, and it is worth treating as a first-class setting rather than an afterthought. Popping animations, screen shake and celebratory effects are fun for most players and genuinely uncomfortable for others, including players with vestibular disorders. Flutter exposes the system preference through MediaQuery, and the game setting should win when it is stricter: \dart Duration popDuration(BuildContext context, bool reduceMotionSetting) { final systemReduced = MediaQuery.disableAnimationsOf(context); if (reduceMotionSetting || systemReduced) { return Duration.zero; } return const Duration(milliseconds: 250); } \\ When motion is the feature, it should be smooth, slow and easy to follow, as in the breathing pacer from Flutter Breathing Pacer Animation Guide. When motion is decoration, it should be removable with one toggle. Test it like a color-blind player Three checks, run before every release: The grayscale test. Convert a screenshot to grayscale and play a level from it. If matching is still possible, the symbols are doing their job. The simulator test. Run the game through a color blindness simulator for deuteranopia, protanopia and tritanopia, one at a time. A symbol system should pass all three without changes, because it never depended on color in the first place. The symbols-off test. Turn the symbols off and play a level. If the game becomes hard to read, the symbols are carrying real information, which confirms they were needed. Then turn them back on and confirm a player who cannot distinguish any of the colors can still finish the level. That is the real acceptance test. Also test on a small, old phone. Bubble Shooter is built to run on old phones, tablets and Android TV, and small screens make symbols harder to read. If a symbol is legible on the smallest screen you support, it is legible everywhere. Low pressure is accessibility too The game has 100 levels across four worlds — Lagoon Shallows, Coral Reef, Deep Blue and Sunken Temple — with up-to-three-star ratings, limited shots per level and +100 points per shot left. Boosters such as Bomb, Fireball and Rainbow are earned through three-star finishes, and an endless mode on Easy, Medium and Hard drops a new row after a few misses. There are no timers, no pressure and no purchases, and the game is free to play. That is a design choice, not a missing feature. Timers punish slow play, and slow play is exactly what a player who is reading symbols instead of colors does at first. Removing the timer costs nothing and makes the game playable for the people the symbols were added for. Difficulty should come from the puzzle — limited shots, a new row in endless mode — not from a clock. Checklist before you ship Every game element that must be told apart differs by shape or symbol, not just color. Symbols ship on by default, with a Settings toggle to turn them off. Aiming and feedback are positional and geometric: lines, outlines and shapes. Accessibility settings are real, persisted Settings with separate controls for symbols, haptics, sound, music and motion. The game passes the grayscale test and the simulator test for all three common types of color vision deficiency. The smallest supported screen still shows legible symbols, and the game has no timer punishing slow play. Build the symbol system first, test it in grayscale, and the rest of the accessibility work gets easier, because the game was never really about color in the first place. Get in touch and let's talk about your project. Originally published at uzairxdev.online
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to