Dhaka, Bangladesh
Case Studies

The Select Component Trade-off: Migrating to Gutenberg's SelectControl

Cover image for the case study "The Select Component Trade-off: Migrating to Gutenberg's SelectControl"
SelectCompact migrating to Gutenberg's SelectControl, a branch fork showing the pre-migration version surviving in parallel history, and a smaller, later call-site swap drawn as its own separate event.
In December 2025 I migrated WowShipping's SelectCompact wrapper component to sit on top of Gutenberg's own SelectControl, preserving its existing prop API behind a compatibility layer so every call site kept working unchanged. The migration merged, but it didn't stay the live implementation for long. Within days, other commits - including one of my own - continued from a branch that had forked before the migration and never incorporated it. By mid-January 2026, SelectCompact was fully back to its original, custom implementation. Separately, in February 2026, I moved one call site off a directly-imported SelectControl and onto the SelectCompact wrapper - narrower, later, and differently motivated than a direct undo of the December migration.
One component migrated to a platform primitive, one prop API preserved across the swap, zero call sites broken, one call site moved back onto the wrapper two months later.
TL;DR summary
ComponentSelectCompact, a wrapper around WowShipping's shared Select primitive
What I didMigrated it to wrap Gutenberg's native SelectControl behind the existing prop API
What happened nextThe migration didn't survive a branch merge - a parallel branch became the surviving history for the file within days
OutcomeEvery call site kept working throughout, because the compatibility layer made the underlying implementation swappable
TimelineMigrated December 2025, superseded by mid-January 2026, one call site moved onto the wrapper in February 2026
TeamBuilt and maintained as shared codebase work
The low-level Select primitive, iterated on for months, feeding into the higher-level SelectCompact wrapper that page-level call sites actually import.
WowShipping's Select primitive had already been through months of iteration by November 2025 - z-index fixes, scroll behavior, a sticky search field, dropdown-close bugs. I added SelectCompact that same November: a higher-level wrapper around Select that accepted a flat array of options instead of composed children, and I used it to replace a shipping-method page's inline select the same day. That wrapper - not Select itself - is what the December migration and the February call-site swap both touch.
SelectCompact's public prop list drawn as a stable line across the diagram, with the implementation swapped from the custom Select primitive to Gutenberg's SelectControl underneath it.
In December 2025 I replaced SelectCompact's implementation with a thin wrapper around Gutenberg's SelectControl, keeping the old implementation as a reference file rather than deleting it. The change mapped the wrapper's existing props onto SelectControl's own, and added new Gutenberg-native props the wrapper didn't have before. The file dropped by about a fifth in size, and SelectControl brought its own keyboard and screen-reader handling with it - a real, if unmeasured, accessibility upside.
A branch graph showing the migration merging in, a second branch forking before that merge and continuing in parallel, and that second branch becoming the surviving history for the file weeks later.
I expected to find a single commit that reverted the December migration. There isn't one. A commit of my own two days later wasn't a descendant of the migration's merge at all - its parent still had the pre-migration implementation, meaning it continued a branch that had forked before the migration and never picked it up. From that point, every later commit touching the wrapper built on the pre-migration branch, including a further rewrite by a teammate the following month. By then, Gutenberg's SelectControl was gone from the wrapper entirely, and I can't find a commit where anyone chose that outcome. A separate, much narrower change in February moved one call site off a directly-imported SelectControl and onto the wrapper - which by then wrapped the custom Select primitive, not Gutenberg's component. Two sibling call sites still use SelectControl directly today, so whatever prompted the swap wasn't applied as a rule.
The SelectCompact prop API drawn as a stable boundary line, with two different implementations swapping underneath it without any caller-side code changing.
The part of this story that held up regardless of which implementation won was the compatibility layer itself. Every call site kept passing the same props, and none of them needed to know or care which implementation was underneath at any given moment. That's also why the migration's disappearance went unnoticed for weeks. Nothing broke, so nothing forced anyone to notice the branch divergence until I traced the history while writing this case study. Adopting a platform primitive doesn't have to be an all-or-nothing decision enforced everywhere at once. The February call-site swap shows one usage moving onto the wrapper on its own timeline, independent of what the wrapper's internals happened to be that week.
Four of five FAQ questions paired with compressed answers, three traced to a commit or diff and the call-site-swap question marked as an open, honest non-answer.
Why migrate SelectCompact to Gutenberg's SelectControl instead of keeping the custom implementation? The stated reasoning was built-in Gutenberg accessibility and a shorter implementation. The change supports both: SelectControl brings its own keyboard and screen-reader handling, and the replacement file was meaningfully shorter. Why didn't the migration stay the live implementation? No commit reverted it on purpose. A branch that had forked before the migration merged became the surviving line for the file within days, and a teammate's later commit built further on that non-Gutenberg branch - parallel branch work outrunning the migration, not a decision anyone made to reject it. Why keep the pre-migration implementation as a separate file instead of deleting it? The migration preserved it verbatim under a new filename rather than removing it, which reads as a deliberate fallback or reference copy kept around during the swap. Why move one call site onto the wrapper in February but not the others still using SelectControl directly? I don't have a documented reason for that - sibling call sites still use SelectControl directly today, so whatever prompted the one swap wasn't applied as a rule across the file. Why call this a trade-off rather than a straightforward migration story? Because the real trade-off wasn't Gutenberg-versus-custom in the abstract - it was between how much the prop-API compatibility layer cost to build and how much risk it removed. That bet paid off in a way I didn't expect: it kept every call site working even while the underlying implementation silently changed direction without anyone deciding it should.
Part of the WowShipping frontend engineering case study series →
On this page
Quick answer
TL;DR
The setup
The migration
The partial revert
What this shows
FAQ