Ask someone where rice comes from and most will say Asia. They are right, but only halfway.
There are actually two independent origins of cultivated rice. Oryza sativa emerged from the Yangtze River Basin around 9000 BCE. Oryza glaberrima, a separate species, came out of the Niger River Delta around 3000 BCE. Two continents, thousands of years apart, arriving at the same crop.
We wanted a format that could carry the weight of the migration without flattening it. pudding.cool had been the reference for a while, projects that treat the web as a format and not just a medium. A scrollytelling essay in React, with Framer Motion handling the transitions, felt right for the kind of slow reveal the topic asked for.
The layout is a 30/70 split. A NarrativePanel holds the section content on the left while a StickyVisual locks the map on the right and updates as the reader scrolls. Scroll tracking is done with IntersectionObserver directly, no library. Each section fires when it crosses a 30% viewport threshold, updates the active section in state, and the StickyVisual reacts. AnimatePresence handles the switch between the WorldMap and the LineageDiagram that appears in the final section.
The data model mattered more than any visual decision. Migration is not a single mechanism. Rice moved through trade routes, colonial expansion, the slave trade, agricultural diffusion, and diaspora. Each carries different weight and different moral context. Collapsing them into one generic route type would have flattened the story, so they became distinct types in the data, each rendered differently on the map.
The LineageDiagram at the end came together best. It draws animated lines from origin coordinates to specific dishes. Jollof rice back to the Niger River Delta. Pilau to Arab trade routes along the Indian Ocean. Rice and peas to the slave trade and the Caribbean. That required a Dish data type with coordinates, cultural pathways, and related dishes so the animation could resolve into something concrete and recognisable.
Mobile required a separate layout pass. Desktop is the 30/70 split with the sticky visual. On mobile the structure flips. A half-viewport StickyVisual stacks above a half-viewport NarrativePanel per section. It works. The animation timing is looser than we would like, and there is a pass we still want to do on it.