DEV Community

zhen
zhen

Posted on

Capturing custom dropdowns is harder than you think: lessons from building a form recovery extension

Native <select> elements are easy. You read .value, you set .value, done. The problem is that half the web doesn't use native selects anymore.

Every UI framework has its own dropdown: React Select, MUI Autocomplete, Ant Design Select, custom-built divs with ARIA roles. They look like dropdowns, they act like dropdowns, but under the hood they're divs, spans, and JavaScript state that the DOM doesn't know about.

I ran into this building FormSafe, a Chrome extension that saves what you type in forms so you can restore it after a tab crash. Text inputs were straightforward. Custom dropdowns nearly broke the whole project.

Why custom dropdowns are hard to capture

A native select stores its state in the DOM. A custom dropdown stores its state in JavaScript. When you need to save "what did the user pick," you're asking two different questions:

  1. What does the DOM show? (the visible label, e.g. "California")
  2. What does the app think is selected? (the internal value, e.g. "CA")

For restoration, you need #2. But #2 lives in a closure, a React state hook, a Vue ref — somewhere your content script can't reach.

Approach 1: Read the visible text (fragile)

The naive approach: find the element that displays the selected value, save its textContent.

This breaks immediately. The visible text is "California" but the form expects "CA". On restore, you set the text to "California" and the app's internal state still says nothing is selected. Submit the form and the server gets an empty value.

Worse: some dropdowns show different text than the value (e.g. displaying "CA — California" but storing "CA"). Text matching is a guessing game.

Approach 2: Simulate the clicks (slow but honest)

What actually works: on restore, programmatically reopen the dropdown and click the right option, the same way a user would.

The sequence:

  1. Find the dropdown trigger element (the thing the user clicks to open it)
  2. Dispatch a real click event on it
  3. Wait for the options list to render (MutationObserver, with a timeout fallback)
  4. Find the option matching the saved value
  5. Dispatch click on that option
  6. Verify the selection took effect

This works because you're going through the same code path as a real user interaction. The framework's internal state updates correctly because as far as it knows, someone actually clicked.

The downside: it's slow. Each dropdown takes 200-500ms to restore (open, wait, click, verify). A form with 10 custom dropdowns takes a few seconds. Acceptable for a recovery flow, terrible for anything real-time.

Approach 3: Framework-specific hooks (powerful, unmaintainable)

You can reach into React's fiber tree, or Vue's component instance, and read/write state directly. I've seen extensions do this.

I decided against it. Every framework version changes internals. You'd be maintaining a compatibility matrix instead of building features. The click-simulation approach is framework-agnostic — it doesn't care if it's React 18, Vue 3, or vanilla JS.

The edge cases that keep me up at night

Async option loading. Some dropdowns fetch options from an API when opened. On restore, you open the dropdown, the options aren't there yet, your MutationObserver fires on the loading spinner. You need to wait for the actual options, with a sane timeout.

Multi-select with chips. The selected values render as removable chips. Restoring means clicking each option individually, then verifying the chip count matches.

Dependent dropdowns. Country → State → City. You must restore them in order, waiting for each to populate the next. Get the order wrong and you're selecting "California" in a city dropdown.

Virtualized lists. Long option lists only render visible items. The option you need might not be in the DOM until you scroll. This one I still don't handle perfectly.

What I'd tell someone starting this today

Don't try to be clever. Simulate user interactions. It's slower and less elegant than reaching into framework internals, but it works across every framework and doesn't break when React ships a new version.

And be honest about failures. If a dropdown can't be restored reliably, tell the user which one. Silently filling in wrong data is worse than admitting a gap.


This is from building FormSafe, a free Chrome extension for form recovery. The dropdown problem was the hardest part by far — text inputs took a day, dropdowns took two weeks.

Top comments (0)