Skip to main content

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 DsSheet with the curated grid on top and a "Search more" affordance that expands a DsTextField search + the full categorized grid.
  • Curated set is context-aware (a curated param; 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.ttf as a design_system asset under assets/fonts/ (committed) + its CC-BY 4.0 license/attribution.
  • Declare it in design_system/pubspec.yaml fonts; expose its family name from the typography tokens (e.g. emojiFamily = 'Twemoji') as the brand styles' fontFamilyFallback tail — 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 a Semantics(identifier:) (Maestro) + a Semantics(label:) describing the pick. onTap opens the picker sheet.
  • DsEmojiPicker (molecule) — the sheet body: the curated grid (default) + the "Search more" expander (a DsTextField search + 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 a DsSheet.
  • 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-appropriate curated set; the selection feeds the controllers the page already owns, so SetupHouseholdNamed(name, emoji) / SetupKidSubmitted(name, colorIndex, emoji) now carry a real emoji. SetupBloc already persists it (household emoji via createHousehold; kid emoji via the non-fatal updateMember).
  • 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), onSelected callback, the None/clear path, reduced-motion static.
  • Widgetbook: a DsEmojiPicker story 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)

  1. Color-emoji font asset + loaders (runtime + golden) + prove color renders in a golden; retire the monochrome workaround.
  2. Emoji dataset (categorized + keywords) + the two curated sets.
  3. DsEmojiChip + DsEmojiPicker + showDsEmojiPicker (sheet, curated, search) + goldens + Widgetbook + unit tests.
  4. Wire into the Setup wizard (household + kid) + flow tests asserting the emoji reaches the bloc; regenerate goldens.