Last updated: Aug 4, 2025, 11:26 AM UTC

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:

  1. Iframe sends its height to parent via postMessage
  2. Parent resizes the iframe based on received height
  3. THE PROBLEM: Resizing the iframe triggers a resize event INSIDE the iframe
  4. Iframe's resize listener fires and sends height again
  5. 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

graph TD A[Iframe loads] --> B[Sends height to parent] B --> C[Parent resizes iframe] C --> D[Triggers resize event in iframe] D --> E[Iframe sends height again] E --> C style D fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px style E fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px

Why This Happens

  • When the parent changes the iframe's height attribute, it triggers a resize event inside the iframe
  • The iframe's JavaScript detects this resize and sends another height message
  • This creates an endless feedback loop
  • ResizeObserver can 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

  1. Initial Load Test

    • Iframe resizes to content height on load
    • No flickering or continuous resizing
    • Content is fully visible without scrollbars
  2. Browser Resize Test

    • Resize browser window
    • Iframe should NOT continuously resize
    • No console errors about maximum call stack
  3. Content Change Test (for dynamic content)

    • Trigger content change (e.g., click calculate)
    • Iframe adjusts to new height
    • Resize happens once, not continuously
  4. 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:

  1. Remove ALL resize event listeners from iframe HTML
  2. Ensure you're only sending height on load or DOMContentLoaded
  3. Check for ResizeObserver without debouncing

Problem: Iframe Too Short/Tall

Symptoms: Content cut off or too much whitespace

Solution:

  1. Add buffer pixels: height + 20
  2. Use document.documentElement.scrollHeight not offsetHeight
  3. Send height multiple times with delays to catch late rendering

Problem: Height Not Updating

Symptoms: Iframe stays at minimum height

Solution:

  1. Verify iframe ID matches exactly in HTML and markdown
  2. Check browser console for errors
  3. Ensure parent has message listener
  4. 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

  1. Never listen to resize events in iframes - This is the #1 cause of infinite loops
  2. Static content only needs to send height once - On initial load
  3. Dynamic content should send height on content change - Not window resize
  4. Always use self-contained closures - Prevent global namespace pollution
  5. Validate iframe IDs - Security and correctness
  6. 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.