How I Learned to Stop Worrying and Love DOM Orchestration

23 days ago

Illustration

The Context

SeekOut Spot is an agentic AI tool that helps hiring managers find exceptional talent. One of its core features is a beautiful candidate presentation that showcases the best-fit candidates we’ve identified. Here’s how it works: Hiring managers define a rubric—essentially the attributes of an ideal candidate. When we present matches, we highlight evidence markers from a candidate’s profile that align with that rubric. This means we need to highlight specific text in the UI and position a tooltip over it. The tooltip allows users to navigate between sections (Resume, Screening Questions, Profile) and jump between multiple highlights within or across those sections.


The Problem

Before React 19, this was straightforward:

  • Use dangerouslySetInnerHTML to render the HTML string returned by our LLM.
  • Run a querySelector to target the rubric IDs in the DOM.
  • Add a class to highlight those elements with some background color and padding.
  • If we wanted to get fancy, we’d add a pseudo-element to offset the highlight visually.

This worked great. We built a hook that abstracted away the tooltip logic and used getBoundingClientRect to position the tooltip directly over the element. We’d grab the elements, pass them to the tooltip hook, and it would center the tooltip above the first one.

Then came the upgrade.

Upgrades are always tricky. Everyone wants the latest and greatest, but engineering teams know the fatigue that comes with it. Spot was relatively new, so we figured we’d get ahead of the curve. We migrated to React 19 after doing a fair bit of research. We knew forwardRef was going to be deprecated eventually, so we moved all refs to component props. That part was surprisingly painless. So painless, in fact, we got suspicious. That’s it?

But during smoke testing, we found the smoking gun: highlights no longer worked. After digging around, we found that React’s new aggressive reconciliation algorithm meant we could no longer reliably manipulate DOM elements we’d queried. This broke our entire highlight + tooltip flow. At this point, skipping the upgrade crossed our minds. It was the gut reaction. But the truth was: our codebase was still early. Staying behind wasn’t justifiable anymore. Plus, the new compiler, startTransition the new use hook, and other upcoming improvements made it too good of an offer to pass up. Moreover, our highlight code was getting way too imperative and brittle, and we were skeptical whether it was reusable enough to be plugged in with any div that contains elements marked with rubric IDs and highlight them.


The Whiteboard

The problem presented itself to us as a set of requirements:

  1. It had to be reusable across components. Once we pass a container ref to the hook—where child elements carry data-rubric-ids="1,2,3"—it should return functions highlighting the corresponding elements.
  2. It had to be built from compositions of smaller hooks. Our highlighting system needed to orchestrate the following:
    1. Scroll to the first match inside the container
    2. Highlight the matched text
    3. Position a tooltip on the first match, with arrow buttons to navigate across sections
    4. Animate the interaction when switching rubric items, ideally using a cascading effect to signal the update
      1. What do we use for that?
  3. It had to support section navigation via URL parameters. This, too, ideally needed to be abstracted into its own hook.
  4. It had to expand the collapsed sections before doing anything else—More on this delightful challenge later. Only once a section is fully expanded should highlighting and scrolling begin.
  5. It had to handle the quirks of Resume PDFs without using any 3rd party libaries. That means calculating height, width, and page numbers, and ensuring everything is fully loaded and rendered before kicking off the highlight system.

The Solution

1. Reusability

Logic

The best candidate for reusing logic and UI were hooks. React 19 made this the natural choice, given that it introduced a lot of extensibility and performance improvements.

We tried to encapsulate the logic within the hook and abstracted out only a few essential functions and attributes:

  • currentRubricSelected
  • handleSelectRubric
  • resetHighlights

Nuance:

Hooks needed to balance between being powerful and minimal—they shouldn’t expose too much, but also not hide useful capabilities.

Resolution:

We exposed only the smallest useful surface area, while keeping the rest internally orchestrated inside the hook.

2. Composition

Logic:

Declaratively, we decided to let the consumer of the sets of our hooks determine what features they wanted the ref that they were passing in to support.

  • Core logic for determining the section for highlighting:
    • This combs through different kinds of sections and checks for the presence of different rubric items.
    • It uses a querySelector to match data attributes with the selected rubric ID or downloads PDFs for different rubric IDs.
  • Tooltip positioning logic:
    • Determines where to place the tooltip by rerunning querySelector and getBoundingClientRect multiple times.
    • Calculates the scrollHeight, clientHeight and scrolls to the first element.
    • Accepts a ref to a component which handles the render/UI logic.
  • Overlay positioning logic:
    • Calculates positions of elements to be highlighted.
    • Gives positioning logic to the ref of an Overlay component supplied to it.

Nuance:

  • The logic for combing through the data-* attributes was tricky—we maintained a Set with the full data attribute and queried them again when we had to highlight.
  • The position of the highlights would change after scrolling—so orchestration here became key. We had to ensure that the order of operations was maintained.
  • We could not just add a class anymore—it wouldn’t work! We had to create an absolutely positioned element which positioned itself over elements.

Resolution:

  • Built modular hooks that expose imperative APIs, but work declaratively when composed.
  • Sequenced operations such that:
    1. Scroll happens first.
    2. Then position calculations.
    3. Then tooltip rendering.
  • Used dynamic overlays for accurate highlight visuals, at the cost of some added complexity—this tradeoff made the whole system more reusable.

3. Expandable Text

Logic:

We needed to expand the text when a rubric was clicked.

  • Registered all text components which exceed a certain height.
  • Provided functionality to smoothly expand a section over a set duration.

Nuance:

We could not adequately get the DOM measurements for collapsed text. A nightmare to work with. Moreover, we did not want to cause unnecessary reflows after calculation of the height.

Resolution:

We decided to go with useContext to abstract away and provide top-level functionality to the hook, so it could coordinate expansion from anywhere in the tree. This "registers" all expandable text elements that we figured could overflow with the context. When we want to highlight it, we check if it has a data attribute which marks it as collapsed, and if so, we get the ID and expand it.

4. Navigation

Logic:

We created another hook that reads and changes URL parameters to navigate within tabs.

Nuance:

  • We can’t just unmount the component! We need it to remain in the DOM.
  • If we change the display to none, we get inaccurate bounding box measurements.
  • We didn’t want a visual flicker where long text elements first rendered in their full height and then collapsed.

Resolution:

  • The container maintained data-* attributes whose mutations we could listen to using a MutationObserver.
  • Added a ref callback to ensure the element had rendered before we did any positioning or collapsing logic.

5. Resume PDFs

Logic:

An iframe handles showing different URLs and informs the parent component that the container has loaded.

Nuance:

  • We needed to be sure that the PDF has loaded and rendered.
  • onLoad has limited support across browsers and does not trigger if the PDF was cached, so we wouldn’t know if it’s rendered.

Resolution:

  • We polled the iframe content to make sure it was loaded.
  • Added a loading overlay to let the PDF ugly-load in the background while we showed a shimmer loader.

The Impact

Candidates that we want to close often have illustrious and complex careers

  • Multiple positions across industries
  • Long and winding screening interviews
  • Resumes with varying structure, multi-page and not legible

Using the candidate navigation system, we were able to create an absolutely stunning presentation that highlights the best of a candidate in a palatable, intuitive way.

  • Animations make the experience feel interactive and dynamic
  • Excellent UX abstracts away heavy lifting for the user
  • Smart calculations ensure that everything works reliably across browsers and environments

What we’re especially proud of is how none of this was imperative. Almost every calculation was fine-tuned and declarative — no random setTimeouts, no requestAnimationFrame hacks.

While escape hatches in React (manipulating the DOM manually) are often considered anti-patterns, they are the bread and butter of real-world applications — especially when you’re orchestrating complex interactions across third-party HTML.

If you choose to walk down this scary path, you must buckle up, go back to basics, and learn to love the DOM. Maybe don't Google that exact phrase.

How I Learned to Stop Worrying and Love DOM Orchestration