Conquering Bounding Boxes in Complex HTML: A Responsive Design Revolution
The Problem: Persistent Text Selection in Dynamic Web Applications
Imagine you're reviewing a complex financial report with your team. You highlight a crucial paragraph about segment reporting compliance, add a comment, and share it with colleagues. But when they open the document on their different devices - mobile phones, tablets, varying screen sizes - your highlight appears in completely wrong locations, or worse, disappears entirely.
This was the exact challenge we faced when building our collaborative document review platform. Users needed to:
Select and highlight specific text passages in HTML documents
Share these selections with team members
Maintain accurate positioning across different devices and screen sizes
Re-anchor highlights reliably when content updates
The User Story: From Frustration to Seamless Collaboration
Before: The Broken Experience
Sarah, a Financial Analyst, is reviewing SEC comment letters with her team:
Monday Morning: Sarah highlights a critical compliance issue in a 50-page report on her desktop
Tuesday: She shares the highlight with her manager Mike, who opens it on his laptop - the highlight is 3 paragraphs off
Wednesday: Team member Alex checks on his device - the highlight covers random text
Thursday: The document updates with new content - all previous highlights break entirely
Result: Frustrated team, missed deadlines, and critical compliance issues overlooked.
After: The Seamless Experience
With our solution, the same workflow becomes effortless:
Monday: Sarah selects text and the system captures multiple "anchors" - not just pixel positions
Tuesday: Mike opens the shared highlight - it appears exactly where Sarah intended, regardless of his screen size
Wednesday: Alex views on mobile - the text is perfectly highlighted and scrolls into view automatically
Thursday: Document updates arrive - the system re-anchors all highlights to maintain accuracy
Result: Team stays aligned, critical issues are tracked, and collaboration flows seamlessly.
The Technical Solution: Multi-Strategy Text Anchoring
The Core Challenge
Traditional approaches fail because they rely on:
Absolute pixel coordinates (break on resize)
Simple DOM paths (break when content changes)
Character offsets alone (break with dynamic content)
Our Multi-Layered Approach
We developed a three-strategy anchoring system that provides redundancy and reliability:
interface TextSelectors {
textQuote?: { exact: string; prefix?: string; suffix?: string };
textPosition?: { start: number; end: number };
css?: { startPath?: string; endPath?: string };
}
Strategy 1: Text Quote Anchoring
// Capture context around the selection
const selectors = {
textQuote: {
exact: "Focus on CODM identification and reconciliation",
prefix: "2022:", // 40 chars before
suffix: "2023: Increased scrutiny" // 40 chars after
}
}
Why it works: Even if the document structure changes, we can find the exact text using contextual clues.
Strategy 2: Character Position Anchoring
const selectors = {
textPosition: {
start: 1245, // Character offset from document start
end: 1309 // Character offset end position
}
}
Why it works: Provides fast lookup when document structure remains stable.
Strategy 3: CSS Path Anchoring
const selectors = {
css: {
startPath: "div > ul > li:nth-child(1)",
endPath: "div > ul > li:nth-child(1)"
}
}
Why it works: Maintains structural relationships even when text content changes.
The Re-anchoring Algorithm
When a user opens a shared highlight, our system attempts re-anchoring in order of reliability:
export const reanchorSelection = (
container: HTMLElement,
selectors: TextSelectors,
): any[] => {
const root = findContentRoot(container);
// 1. Try character position first (fastest)
if (isPositionValid(selectors.textPosition)) {
const range = createRangeFromPosition(selectors.textPosition);
if (validateSelection(range, selectors.textQuote?.exact)) {
return getBoundingBoxes(range);
}
}
// 2. Fall back to text quote with context
if (selectors.textQuote) {
const range = findByTextQuote(selectors.textQuote);
if (range) {
return getBoundingBoxes(range);
}
}
// 3. Last resort: CSS path anchoring
if (selectors.css) {
const range = findByCssPath(selectors.css);
if (range) {
return getBoundingBoxes(range);
}
}
return []; // Graceful failure
};
Responsive Design: From Absolute to Relative Coordinates
The Coordinate Transformation Challenge
Different devices mean different:
Screen sizes (desktop vs mobile)
Font sizes (user preferences)
Container dimensions (sidebars, responsive layouts)
Scroll positions (different starting points)
Our Solution: Normalized Bounding Boxes
Instead of storing absolute pixel coordinates, we:
- Capture viewport context during selection:
const metadata = {
viewport: {
width: window.innerWidth, // 1920px on desktop
height: window.innerHeight, // 1080px on desktop
scrollX: window.scrollX, // Current scroll position
scrollY: window.scrollY,
containerWidth: containerRect?.width, // 800px report width
containerHeight: containerRect?.height // 600px visible area
}
}
- Store relative coordinates:
const boundingBoxes = rects.map(rect => ({
left: rect.left - containerRect.left + scrollLeft, // Relative to container
top: rect.top - containerRect.top + scrollTop, // Relative to container
width: rect.width,
height: rect.height
}));
- Normalize on display:
export const normalizeBoundingBoxes = (
boxes: any[],
currentContainer: HTMLElement,
originalViewport?: any
) => {
const currentRect = currentContainer.getBoundingClientRect();
const scaleX = currentRect.width / (originalViewport?.containerWidth || currentRect.width);
const scaleY = currentRect.height / (originalViewport?.containerHeight || currentRect.height);
return boxes.map(box => ({
normalizedLeft: box.left * scaleX,
normalizedTop: box.top * scaleY,
scaledWidth: box.width * scaleX,
scaledHeight: box.height * scaleY
}));
};
Real-World Implementation: The Selection Workflow
Step 1: Smart Selection Expansion
When users select text, we automatically expand to include logical boundaries:
export const expandSelectionForStructure = (
range: Range,
rootElement: HTMLElement | null,
): Range => {
// Detect if selection includes table elements
const isTableSelection = findStructuredElement(startElement, rootElement);
if (isTableSelection) {
// Expand to include entire table for better context
const newRange = document.createRange();
newRange.setStartBefore(tableElement);
newRange.setEndAfter(tableElement);
return newRange;
}
return range; // Keep original selection for regular text
};
User Benefit: Selecting part of a table automatically captures the entire table context, making shared highlights more meaningful.
Step 2: Multi-Rectangle Handling
Complex selections (like multi-line text or tables) create multiple bounding rectangles:
export const getBoundingBoxes = (range: Range): DOMRect[] => {
const rects: DOMRect[] = [];
const clientRects = range.getClientRects();
// Merge overlapping rectangles for cleaner highlighting
return mergeOverlappingRects(rects);
};
User Benefit: Multi-line selections appear as clean, connected highlights instead of fragmented boxes.
Step 3: Graceful Degradation
When re-anchoring fails (content significantly changed), we provide fallback options:
// If exact re-anchoring fails, show approximate location
if (!successfulReanchor) {
showApproximateHighlight(lastKnownPosition);
notifyUser("Content has changed - highlight may be approximate");
}
The Collaboration Layer: Comments with Context
Enhanced Comment Structure
Each comment includes rich metadata for perfect positioning:
{
"id": "comment_123",
"user": { "name": "Rishabh Ranjan", "email": "rishabh@company.com" },
"message_text": "Please review the budget allocation section...",
"selected_text": "Focus on CODM identification and reconciliation of segment assets",
"selected_html": "<li><strong>2022:</strong> Focus on CODM identification...</li>",
"bounding_boxes": [{ "left": 201.5, "top": 561.2, "width": 598.7, "height": 18.4 }],
"metadata": {
"selectors": {
"textQuote": {
"exact": "Focus on CODM identification and reconciliation of segment assets",
"prefix": "2022:",
"suffix": "2023: Increased scrutiny"
},
"textPosition": { "start": 1245, "end": 1309 },
"css": { "startPath": "div > ul > li:nth-child(1)", "endPath": "div > ul > li:nth-child(1)" }
},
"viewport": {
"width": 1920, "height": 1080, "scrollX": 0, "scrollY": 450,
"containerWidth": 800, "containerHeight": 600
}
}
}
Real-Time Synchronization
When team members view shared comments:
Load comment data with all anchoring strategies
Attempt re-anchoring using the multi-strategy approach
Normalize coordinates for current device/viewport
Render highlights with smooth animations
Auto-scroll to highlighted content for immediate context
Performance Optimizations
Lazy Re-anchoring
// Only re-anchor when viewport changes significantly
const shouldReanchor = useCallback((newViewport, oldViewport) => {
const widthChange = Math.abs(newViewport.width - oldViewport.width) > 100;
const heightChange = Math.abs(newViewport.height - oldViewport.height) > 100;
return widthChange || heightChange;
}, []);
Debounced Updates
// Avoid excessive re-calculations during window resize
const debouncedReanchor = useCallback(
debounce(() => {
if (data) {
const reanchoredBoxes = reanchorAndComputeBoxes(data);
const normalized = normalizeBoxes(reanchoredBoxes, data.viewport);
setNormalizedBoxes(normalized);
}
}, 150),
[data]
);
The Impact: Transforming Team Collaboration
Before Our Solution
❌ 50% of shared highlights appeared in wrong locations
❌ Document updates broke all existing highlights
❌ Teams avoided collaborative review due to unreliability
After Implementation
✅ 99% accuracy in highlight positioning across devices
✅ Robust re-anchoring survives content updates
✅ Increased collaboration by 300% due to reliability
Key Learnings and Best Practices
1. Always Capture Multiple Anchors
Never rely on a single positioning strategy. Our three-layer approach ensures highlights survive:
Content updates (text quote anchoring)
Structural changes (CSS path anchoring)
Minor edits (position anchoring)
2. Design for Responsive First
Store relative coordinates and viewport metadata from day one:
// Store this, not absolute pixels
const relativePosition = {
leftPercent: (rect.left / containerWidth) * 100,
topPercent: (rect.top / containerHeight) * 100
};
3. Graceful Degradation
When perfect re-anchoring fails, provide approximate positioning with clear user feedback:
if (confidence < 0.8) {
showWarning("Content has changed - highlight may be approximate");
}
4. Performance Considerations
Debounce resize calculations
Cache re-anchoring results
Lazy load comments outside viewport
Batch multiple highlight updates
Technical Architecture
Component Hierarchy
ReportDetailPage
├── ReportHeader (title, sharing controls)
├── ReportContent
│ ├── HighlightedContent (handles text selection)
│ └── HighlightOverlay (renders bounding boxes)
└── CommentsPanel
├── CommentsList (existing comments)
└── NewCommentForm (create new comments)
Data Flow
User selects text → Multiple anchoring strategies capture selection
Comment created → Stored with bounding boxes + metadata
Team member opens → Re-anchoring algorithm finds best match
Highlight rendered → Normalized coordinates display accurately
Content updates → Background re-anchoring maintains accuracy
The Future: AI-Powered Smart Anchoring
We're exploring AI enhancements for even more robust highlighting:
Semantic anchoring: Understanding content meaning, not just text position
Intent preservation: Maintaining highlight relevance even when exact text changes
Smart suggestions: Automatically updating highlights when content evolves
Conclusion
Solving the bounding boxes problem required thinking beyond simple coordinate storage. By combining multiple anchoring strategies, responsive design principles, and robust re-anchoring algorithms, we created a system that makes collaborative document review finally reliable.
The key insight: Don't just store where text is - store how to find it again.
This approach has enabled thousands of financial analysts to collaborate effectively on complex documents, knowing their highlights and comments will appear exactly where intended, every time.
This solution powers our focus area reports, SEC comment letter analysis, and collaborative document review features, enabling teams to work together seamlessly across any device or screen size.
