DsEmojiPicker + color-emoji rendering (design spec, 2026-06-20)
Brainstormed + approved 2026-06-20. First of two deferred follow-ups from the
auth/onboarding branch (the second is kid colorKey↔Color persistence). Gives
the app a reusable emoji picker and makes color emoji render everywhere —
including Flutter goldens — closing the loop the Setup wizard left open (the
wizard collects name + color but passed emoji: null because no picker existed;
SetupBloc already persists a non-null emoji).
Goal
A reusable, calm, low-stim emoji picker for the design system, wired into the Setup wizard (household + kid emoji), plus the asset/loader work to render color emoji at runtime AND in goldens (retiring the monochrome-emoji workaround).
Decisions (approved)
- Picker model: curated by default, expandable to a full searchable grid (progressive disclosure — the calm path is the curated set; power on demand).
- Presentation: a tappable emoji chip in the step/editor → opens a
DsSheetwith the curated grid on top and a "Search more" affordance that expands aDsTextFieldsearch + the full categorized grid. - Curated set is context-aware (a
curatedparam; household vs kid get fitting defaults). - Full-grid data: a bundled data-only categorized emoji dataset (const
Dart / JSON asset in
design_system) rendered by our own DS-styled grid — NOT a third-party picker package (which ships its own UI and fights our design + a11y). - Color emoji render in goldens — bundle Twemoji (COLRv0): ship the Twemoji COLRv0 font everywhere (runtime + golden). This is the foundation; the picker builds on it.
Color-emoji font (foundation — build first)
Today color emoji do not render in color in Flutter goldens; packages/flow_test
(golden_fonts.dart / flutter_config.dart) uses a monochrome Noto Emoji
workaround, and design_system typography_tokens.dart / pubspec.yaml carry
the current font setup.
Empirically established (pixel-probe experiment, 2026-06-21): color emoji DO
rasterize in Flutter 3.44's headless golden pipeline — but the format matters.
Measured foundColor per format: CBDT bitmap → yes, COLRv0 → yes,
COLRv1 → NO (renders nothing). The earlier COLRv1 attempt failed only because
COLRv1 is the one format this engine can't draw. So color-in-goldens is solved
by choosing a supported format.
Decision: bundle Twemoji (COLRv0) — ~1.5 MB, crisp vector, renders color in
goldens AND on device, and shipping it everywhere means goldens show EXACTLY what
users see on every platform (no per-OS divergence). Source: the Mozilla/jdecked
Twemoji COLRv0 build (TwemojiMozilla.ttf, CC-BY 4.0). The proven font is staged
at /tmp/Twemoji.ttf from the experiment.
- Bundle
Twemoji.ttfas adesign_systemasset underassets/fonts/(committed) + its CC-BY 4.0 license/attribution. - Declare it in
design_system/pubspec.yamlfonts; expose its family name from the typography tokens (e.g.emojiFamily = 'Twemoji') as the brand styles'fontFamilyFallbacktail — so device + web render Twemoji color emoji. - Register it in the golden-test font loader (
packages/flow_test/golden_fonts.dart) under the same bare family so goldens render the SAME color emoji. - Retire the monochrome workaround in
flow_test; regenerate existing emoji-bearing committed goldens to true color. - Verify with a pixel-color probe (the experiment's technique): a golden test that loads Twemoji, renders 🦊🏡⭐, and asserts the rendered pixels contain color (chromatic pixels present) — the deterministic proof that color renders.
Components (design_system)
DsEmojiChip(atom) — a tappable rounded tile showing the current emoji, or a neutral placeholder ("+"/face) when null. Carries aSemantics(identifier:)(Maestro) + aSemantics(label:)describing the pick.onTapopens the picker sheet.DsEmojiPicker(molecule) — the sheet body: the curated grid (default) + the "Search more" expander (aDsTextFieldsearch + the full categorized grid, filtered live by keyword). Returns the chosen emoji to the caller.- Props:
selected(String?),curated(List— context default), onSelected(String). - A convenience
showDsEmojiPicker(context, ...)opening it in aDsSheet.
- Props:
- Emoji dataset — a categorized list with search keywords (data-only) + the two default curated sets (household, kid).
Interaction / behavior
- Tap chip → sheet opens (curated grid visible). One tap on a curated emoji = select + close. "Search more" expands the search field + full grid; typing filters by keyword/name; tapping any result = select + close.
- Clearing/removing an emoji: a "None" affordance returns null (emoji is optional everywhere).
- Reduced motion: the sheet open + the "Search more" expand honor
MotionTokens.durationOrZero/MediaQuery.disableAnimationsOf(static when reduced).
Wiring (app)
- Setup wizard: the name step (household) and kid step each replace their
"emoji deferred" slot with a
DsEmojiChip+ a context-appropriatecuratedset; the selection feeds the controllers the page already owns, soSetupHouseholdNamed(name, emoji)/SetupKidSubmitted(name, colorIndex, emoji)now carry a real emoji.SetupBlocalready persists it (household emoji viacreateHousehold; kid emoji via the non-fatalupdateMember). - The rebuild has no other emoji-editing UI yet, so this is the full wiring
surface. (Future household/kid editors reuse
DsEmojiChip.)
Accessibility
- Each emoji button:
Semantics(label:)= the emoji's human name (not an opaque glyph); selected state announced; the search field labeled. Semantics(identifier:)on the chip + the sheet + a couple of stable emoji for Maestro.- a11y floor (focus ring, hit target ≥ 48dp) like every DS atom; AA contrast on the tiles.
Testing
- Color-emoji proof (task 1): a golden that renders a known color emoji and visibly shows color (the gate for the rest).
- DS goldens:
DsEmojiChip(empty + filled) and the picker sheet (curated + expanded-search), rendering real color emoji. - Unit: search filtering (keyword → results),
onSelectedcallback, the None/clear path, reduced-motion static. - Widgetbook: a
DsEmojiPickerstory under the molecules grouping. - Setup flow tests: pick a household + kid emoji and assert the chosen emoji
reaches
SetupHouseholdNamed/SetupKidSubmitted(and persists). Regenerate affected goldens (now true-color).
Out of scope / deferred
- Kid
colorKey↔Color persistence (the SECOND follow-up — separate spec). - Recently-used / frequently-used emoji memory.
- Skin-tone variant selection.
- Custom/imported images as an "emoji".
Sequencing (for the plan)
- Color-emoji font asset + loaders (runtime + golden) + prove color renders in a golden; retire the monochrome workaround.
- Emoji dataset (categorized + keywords) + the two curated sets.
DsEmojiChip+DsEmojiPicker+showDsEmojiPicker(sheet, curated, search) + goldens + Widgetbook + unit tests.- Wire into the Setup wizard (household + kid) + flow tests asserting the emoji reaches the bloc; regenerate goldens.