
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:
- 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. - It had to be built from compositions of smaller hooks. Our highlighting system needed to orchestrate the following:
- Scroll to the first match inside the container
- Highlight the matched text
- Position a tooltip on the first match, with arrow buttons to navigate across sections
- Animate the interaction when switching rubric items, ideally using a cascading effect to signal the update
- What do we use for that?
- It had to support section navigation via URL parameters. This, too, ideally needed to be abstracted into its own hook.
- 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.
- 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
- function to select a rubric
- reset highlights
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 decide 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:
- Scroll happens first.
- Then position calculations.
- 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.
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.
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.