Iframe Embedding Guide - Best Practices & Infinite Loop Prevention
Status: Complete
Category: Development & Technical Guides
Tags: #iframe #embedding #documentation #auto-resize #infinite-loop-prevention
Overview
This guide provides the definitive patterns for embedding HTML content via iframes in markdown documentation, with a special focus on preventing the dreaded infinite resize loop that can make pages unusable. Based on real-world experience and hard-learned lessons from the NudgeCampaign documentation system.
When to Use Iframes
Perfect For:
- Interactive HTML mockups and prototypes
- Live component demonstrations
- Interactive calculators and tools
- Isolated HTML/CSS/JS examples
- Third-party widget embedding
Avoid For:
- Static images (use
<img>tags) - Simple text content (use markdown)
- Content that needs SEO indexing
- Content requiring frequent updates
π΄ The Infinite Resize Loop Problem
Understanding the Root Cause
The infinite resize loop is a critical issue that occurs when:
- Iframe sends its height to parent via
postMessage - Parent resizes the iframe based on received height
- THE PROBLEM: Resizing the iframe triggers a
resizeevent INSIDE the iframe - Iframe's resize listener fires and sends height again
- Loop continues infinitely, making the page flicker and become unusable
// β WRONG - This causes infinite loops!
window.addEventListener('resize', function() {
sendHeight(); // This will fire when parent resizes the iframe!
});
Visual Explanation
Why This Happens
- When the parent changes the iframe's height attribute, it triggers a
resizeevent inside the iframe - The iframe's JavaScript detects this resize and sends another height message
- This creates an endless feedback loop
ResizeObservercan make it worse by detecting every pixel change
Correct Implementation Pattern
The Golden Rule
Static content should ONLY send height on initial load, NEVER on resize events
HTML File Pattern (Embedded Content)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Interactive Content</title>
<!-- Your styles here -->
</head>
<body>
<!-- Your content here -->
<script>
// β
CORRECT - Auto-resize for static content
(function() {
function sendHeight() {
const height = document.documentElement.scrollHeight;
// Send height to parent with specific ID
window.parent.postMessage({
type: 'resize-iframe',
id: 'your-unique-iframe-id', // Must match iframe ID in markdown
height: height + 20 // Add buffer to prevent scrollbars
}, '*');
}
// CRITICAL: Only send height on initial load for static content
// DO NOT listen to resize events - that causes infinite loops!
// Option 1: Send after DOM is ready
window.addEventListener('DOMContentLoaded', function() {
setTimeout(sendHeight, 200); // Wait for initial render
setTimeout(sendHeight, 800); // Catch late-loading resources
});
// Option 2: Send after all resources load
window.addEventListener('load', function() {
setTimeout(sendHeight, 100);
});
// β οΈ ONLY use ResizeObserver for truly dynamic content
// For static mockups, NEVER use this!
// if (contentIsDynamic) {
// const resizeObserver = new ResizeObserver(debounce(sendHeight, 100));
// resizeObserver.observe(document.documentElement);
// }
})();
</script>
</body>
</html>
Markdown Embedding Pattern
## Interactive Example
<div style="width: 100%; margin: 20px 0;">
<iframe
id="your-unique-iframe-id"
src="./path/to/your-file.html"
style="width: 100%; min-height: 600px; border: 1px solid #e5e7eb; border-radius: 8px; transition: height 0.3s ease;"
title="Descriptive Title">
</iframe>
</div>
<!-- Other iframes here if needed -->
<!-- Single Consolidated Resize Handler at Document End -->
<script>
// Auto-resize iframes based on content messages
(function() {
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'resize-iframe' && e.data.height) {
// List of valid iframe IDs to prevent unauthorized resizing
const validIds = [
'your-unique-iframe-id',
'another-iframe-id'
// Add all iframe IDs from this document
];
const targetId = e.data.id;
if (targetId && validIds.includes(targetId)) {
const iframe = document.getElementById(targetId);
if (iframe) {
// Set minimum height to prevent collapse
const newHeight = Math.max(400, e.data.height);
iframe.style.height = newHeight + 'px';
}
}
}
});
})();
</script>
Dynamic Content Pattern
For content that actually changes (calculators, interactive tools):
// β
CORRECT - Pattern for dynamic content with debouncing
(function() {
let resizeTimeout;
let lastHeight = 0;
function sendHeight() {
const height = document.documentElement.scrollHeight;
// Only send if height actually changed
if (Math.abs(height - lastHeight) > 5) {
lastHeight = height;
window.parent.postMessage({
type: 'resize-iframe',
id: 'calculator-iframe',
height: height + 20
}, '*');
}
}
function debouncedSendHeight() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(sendHeight, 100);
}
// Initial load
window.addEventListener('load', function() {
setTimeout(sendHeight, 100);
});
// ONLY send height when content actually changes
// For example, when calculation results are shown
function onCalculate() {
// Your calculation logic
doCalculation();
// Then update height
setTimeout(sendHeight, 100); // After DOM updates
setTimeout(sendHeight, 500); // After animations
}
// DO NOT listen to window resize!
// DO listen to your specific content changes
document.getElementById('calculate-btn').addEventListener('click', onCalculate);
})();
Common Mistakes to Avoid
1. Listening to Window Resize in Iframes
// β NEVER DO THIS in an iframe
window.addEventListener('resize', sendHeight);
// Why: Parent resizing the iframe triggers this, causing infinite loop
2. Using ResizeObserver Without Debouncing
// β WRONG - Fires too frequently
const observer = new ResizeObserver(sendHeight);
observer.observe(document.body);
// β
CORRECT - Only for dynamic content with debouncing
const observer = new ResizeObserver(debounce(sendHeight, 100));
3. Multiple Message Listeners in Parent
// β WRONG - Multiple listeners can conflict
window.addEventListener('message', handler1);
window.addEventListener('message', handler2);
// β
CORRECT - Single consolidated handler
window.addEventListener('message', function(e) {
// Handle all iframe messages here
});
4. Not Validating Iframe IDs
// β WRONG - Security risk, can resize any element
if (e.data.type === 'resize-iframe') {
document.getElementById(e.data.id).style.height = e.data.height + 'px';
}
// β
CORRECT - Validate against whitelist
const validIds = ['id1', 'id2'];
if (validIds.includes(e.data.id)) {
// Safe to resize
}
5. Missing Minimum Height
// β WRONG - Can collapse to 0
iframe.style.height = e.data.height + 'px';
// β
CORRECT - Ensure minimum height
iframe.style.height = Math.max(400, e.data.height) + 'px';
Working Examples in Our Codebase
Static HTML Mockups
- Location:
/docs/10-ui-ux-wireframes/mockups/ - Pattern: Only send height on load, no resize listeners
- Files:
dashboard.html,campaign-builder.html, etc.
Dynamic Calculators
- Location:
/docs/06-business-model/calculators/ - Pattern: Send height on content change, not window resize
- Example:
content-roi-calculator.html
Testing Checklist
Verify Correct Behavior
Initial Load Test
- Iframe resizes to content height on load
- No flickering or continuous resizing
- Content is fully visible without scrollbars
Browser Resize Test
- Resize browser window
- Iframe should NOT continuously resize
- No console errors about maximum call stack
Content Change Test (for dynamic content)
- Trigger content change (e.g., click calculate)
- Iframe adjusts to new height
- Resize happens once, not continuously
Console Monitoring
- Open browser console
- No repeated "Sending height" messages
- No "Maximum call stack exceeded" errors
Debug Helper Script
// Add to HTML file temporarily for debugging
let messageCount = 0;
const originalSend = window.parent.postMessage;
window.parent.postMessage = function(data, origin) {
console.log(`Message #${++messageCount}:`, data);
if (messageCount > 10) {
console.error('TOO MANY MESSAGES - Possible infinite loop!');
return;
}
originalSend.call(window.parent, data, origin);
};
Troubleshooting
Problem: Continuous Resizing
Symptoms: Page flickers, iframes constantly change height
Solution:
- Remove ALL
resizeevent listeners from iframe HTML - Ensure you're only sending height on
loadorDOMContentLoaded - Check for ResizeObserver without debouncing
Problem: Iframe Too Short/Tall
Symptoms: Content cut off or too much whitespace
Solution:
- Add buffer pixels:
height + 20 - Use
document.documentElement.scrollHeightnotoffsetHeight - Send height multiple times with delays to catch late rendering
Problem: Height Not Updating
Symptoms: Iframe stays at minimum height
Solution:
- Verify iframe ID matches exactly in HTML and markdown
- Check browser console for errors
- Ensure parent has message listener
- Verify content is not in a cross-origin iframe (blocked by browser)
Implementation Checklist
When adding a new iframe embed:
- HTML file uses self-contained IIFE for scripts
- HTML only sends height on load (not resize) for static content
- HTML includes unique iframe ID in postMessage
- Markdown iframe ID matches HTML postMessage ID exactly
- Markdown has single consolidated message handler at end
- Handler validates iframe ID against whitelist
- Minimum height is set to prevent collapse
- Tested in browser - no continuous resizing
- No console errors about call stack or excessive messages
Quick Start Template
For Static Content (Mockups, Demos)
<!-- your-mockup.html -->
<script>
(function() {
function sendHeight() {
window.parent.postMessage({
type: 'resize-iframe',
id: 'your-mockup', // Must match iframe ID
height: document.documentElement.scrollHeight + 20
}, '*');
}
window.addEventListener('DOMContentLoaded', () => {
setTimeout(sendHeight, 200);
setTimeout(sendHeight, 800);
});
window.addEventListener('load', () => {
setTimeout(sendHeight, 100);
});
})();
</script>
For Dynamic Content (Calculators, Tools)
<!-- your-calculator.html -->
<script>
(function() {
let lastHeight = 0;
function sendHeight() {
const height = document.documentElement.scrollHeight;
if (Math.abs(height - lastHeight) > 5) {
lastHeight = height;
window.parent.postMessage({
type: 'resize-iframe',
id: 'your-calculator',
height: height + 20
}, '*');
}
}
// Initial load
window.addEventListener('load', () => setTimeout(sendHeight, 100));
// On content change only
function updateContent() {
// Your logic here
setTimeout(sendHeight, 100);
setTimeout(sendHeight, 500);
}
})();
</script>
Additional Resources
Key Takeaways
- Never listen to resize events in iframes - This is the #1 cause of infinite loops
- Static content only needs to send height once - On initial load
- Dynamic content should send height on content change - Not window resize
- Always use self-contained closures - Prevent global namespace pollution
- Validate iframe IDs - Security and correctness
- Test thoroughly - Check for continuous resizing before committing
Remember: The browser resize event is your enemy in iframes! Treat it like toxic waste and stay far away from it.