Skip to content

Instantly share code, notes, and snippets.

@xizon
Last active November 20, 2025 03:39
Show Gist options
  • Select an option

  • Save xizon/50ab4ad0c5c815406ebcb16c341b8a5d to your computer and use it in GitHub Desktop.

Select an option

Save xizon/50ab4ad0c5c815406ebcb16c341b8a5d to your computer and use it in GitHub Desktop.

Iframe Height Auto-Adjustment Solution

This code is used for pages opened in iframes to precisely adjust the parent iframe height.

Height Calculation Problem

When using vh units inside an iframe, element heights will always equal the viewport height of the iframe.

Circular Dependency

  1. Your script detects content height changes β†’ sends postMessage to parent page to update iframe height
  2. Parent page adjusts iframe height
  3. Iframe viewport height (the calculation basis for 100vh) changes accordingly
  4. Iframe internal layout recalculates
  5. Content height changes
  6. Triggers script update again β†’ infinite loop

Solution: Preventing Infinite Loop

The following function solves the infinite loop problem:

Child Page Usage

function ifmHighlyAdaptive() {
    const THROTTLE_INTERVAL = 500; // Adjustable as needed
    const IFRAME_OFFSET = 12; // Height correction for iframe to avoid scrollbars

    let lastHeight = 0;
    let resizeObserver = null;
    let mutationObserver = null;
    let isMonitoringEnabled = true; // Whether height change monitoring is enabled
    let isFirstUpdate = true; // Flag to mark if this is the first update
    let fixedMinHeight = null; // Record the minHeight set by script (for syncing down when content shrinks)

    /**
     * Get actual content height of the document
     * 
     * The height of document.body or document.documentElement inside an iframe
     * may be constrained by the parent iframe height, no longer reflecting the
     * actual content height.
     * 
     * Solution:
     * 1. Wrap content in a fixed container (wrapper) inside the iframe
     * 2. Only monitor the height of this wrapper
     * 3. Use ResizeObserver + MutationObserver + image load listeners
     * 4. Don't rely on body/html scrollHeight, only rely on wrapper scrollHeight
     */
    function getActualContentHeight() {
        const wrapper = document.getElementById("iframe-content-wrapper");
        if (!wrapper) return 0;

        // The wrapper may have a large minHeight set, which prevents getting
        // the real height when content shrinks. Temporarily remove inline
        // minHeight to measure "actual content height".
        const prevInlineMinHeight = wrapper.style.minHeight;
        if (prevInlineMinHeight) {
            wrapper.style.minHeight = '';
        }

        const realContentHeight = wrapper.scrollHeight || wrapper.offsetHeight || 0;

        // If a fixed minHeight was previously set by script, and the current
        // content height (plus offset) is smaller than this fixed value,
        // it means content has shrunk. We need to sync down the minHeight
        // to prevent the iframe from being unable to shrink.
        if (fixedMinHeight !== null) {
            const targetHeightWithOffset = Math.ceil(realContentHeight) + IFRAME_OFFSET;
            if (targetHeightWithOffset < fixedMinHeight) {
                fixedMinHeight = targetHeightWithOffset;
                wrapper.style.minHeight = fixedMinHeight + 'px';
            } else {
                // Restore previous inline minHeight (if any), otherwise keep current fixed value
                wrapper.style.minHeight = prevInlineMinHeight || (fixedMinHeight + 'px');
            }
        } else {
            // When no script-set minHeight exists, restore original inline minHeight
            wrapper.style.minHeight = prevInlineMinHeight;
        }

        return realContentHeight;
    }

    /**
     * Throttle function (or combination of "throttle + last call debounce")
     */
    function throttle(fn, wait) {
        let lastCall = 0;
        let timer = null;
        let lastInvokeArgs = null;

        const invoke = () => {
            timer = null;
            lastCall = Date.now();
            fn();
        };

        const throttled = () => {
            const now = Date.now();
            const remaining = wait - (now - lastCall);

            if (remaining <= 0) {
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                }
                invoke();
            } else if (!timer) {
                timer = window.setTimeout(invoke, remaining);
            }
        };

        throttled.flush = () => {
            if (timer) {
                clearTimeout(timer);
                invoke();
            }
        };

        return throttled;
    }

    const scheduleUpdate = throttle(updateParentHeight, THROTTLE_INTERVAL);

    function updateParentHeight() {
        // If monitoring is disabled, don't update height
        if (!isMonitoringEnabled) {
            return;
        }

        const height = Math.ceil(getActualContentHeight()) + IFRAME_OFFSET;
        // Force update on first update, check height change on subsequent updates
        if (!isFirstUpdate && height === lastHeight) return; // Key: prevents infinite loop

        console.log('πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ»2222222222 [iframe-autosize] Initializing height setting!');

        lastHeight = height;
        isFirstUpdate = false; // After first update, mark as false

        if (window.parent && window.parent !== window) {
            window.parent.postMessage({ type: "iframeHeightUpdate", height }, "*");
        }
    }

    function startMonitoring() {
        // If already monitoring, stop first
        stopMonitoring();

        isMonitoringEnabled = true;

        // ResizeObserver monitors html and body
        if (typeof ResizeObserver !== "undefined") {
            resizeObserver = new ResizeObserver(scheduleUpdate);
            resizeObserver.observe(document.documentElement);
            resizeObserver.observe(document.body);
        }

        // MutationObserver monitors DOM changes
        if (typeof MutationObserver !== "undefined") {
            mutationObserver = new MutationObserver(scheduleUpdate);
            mutationObserver.observe(document.documentElement, {
                childList: true,
                subtree: true,
                attributes: true,
                characterData: true
            });
        }
    }

    function stopMonitoring() {
        isMonitoringEnabled = false;

        if (resizeObserver) {
            resizeObserver.disconnect();
            resizeObserver = null;
        }

        if (mutationObserver) {
            mutationObserver.disconnect();
            mutationObserver = null;
        }
    }

    function init() {
        console.log('πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ»πŸ‘‰πŸ» [iframe-autosize] Initializing!');
        isFirstUpdate = true; // Reset to first update on initialization
        startMonitoring();
        updateParentHeight(); // Immediately sync on initialization
    }

    function ready(callback) {
        if (document.readyState != 'loading') callback();
        else if (document.addEventListener) document.addEventListener('DOMContentLoaded', callback);
        else document.attachEvent('onreadystatechange', function () {
            if (document.readyState == 'complete') callback();
        });
    }

    window.addEventListener("load", () => {
        scheduleUpdate();
        scheduleUpdate.flush(); // Ensure final height after load completes
    });

    function tryParse(str) {
        try {
            return JSON.parse(str);
        } catch (e) {
            return null;
        }
    }

    window.addEventListener('message', function (e) {
        const data = (typeof e.data === 'string') ? tryParse(e.data) : e.data;
        if (!data || data.__iframeResizer !== true) return;

        if (data.type === 'parentViewport') {
            // Handle parent page viewport changes
            ready(function () {
                init();
            });

        } else if (data.type === 'parentChangedHeight') {
            // Parent page is changing height, pause monitoring
            console.log('❌❌❌❌❌❌❌❌❌❌❌ [iframe-autosize] Received pause monitoring message, stopping height monitoring');
            stopMonitoring();
        } else if (data.type === 'parentChangedHeightComplete') {
            // Parent page height change complete, resume monitoring
            console.log('πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€ [iframe-autosize] Received resume monitoring message, resuming height monitoring');
            
            // Ensure Observer is completely disconnected
            stopMonitoring();
            
            // Use requestAnimationFrame to ensure DOM operations execute in next frame,
            // at which point Observer is completely disconnected
            requestAnimationFrame(() => {
                // Get current iframe actual height and set iframe-content-wrapper to fixed height
                // This avoids content height changes caused by vh recalculation, preventing infinite loop
                const wrapper = document.getElementById("iframe-content-wrapper");
                if (wrapper) {
                    // Get current iframe actual height (from parent page or use current viewport height)
                    const currentIframeHeight = data.height;
                    // Set wrapper height to fixed value to avoid infinite loop from vh changes;
                    // Also record this fixed height for comparing and reducing minHeight when content shrinks later
                    if (currentIframeHeight > IFRAME_OFFSET) {
                        fixedMinHeight = currentIframeHeight;
                        wrapper.style.minHeight = fixedMinHeight + 'px';
                        console.log('πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€πŸͺ€ [iframe-autosize] Set wrapper height to:', currentIframeHeight);
                    }
                }
                
                // Wait one more frame to ensure minHeight is set before resuming monitoring
                requestAnimationFrame(() => {
                    // Update lastHeight to avoid immediate update trigger
                    lastHeight = Math.ceil(getActualContentHeight()) + IFRAME_OFFSET;
                    
                    // Resume monitoring
                    startMonitoring();
                    
                    // Update height once immediately after resuming
                    setTimeout(() => {
                        updateParentHeight();
                    }, 100);
                });
            });
        }
    });
}

Parent Page Usage

import React, { useEffect, useRef, useState } from "react";

type ChildProgramLoaderProps = {
    url: string
};

const IframeLoader = (props: ChildProgramLoaderProps) => {
    const {
        url
    } = props;

    const IFRAME_OFFSET = 12; // Height correction for iframe to avoid scrollbars
    const iframeRef = useRef<HTMLIFrameElement>(null);
    const lastHeightRef = useRef<number>(0);
    const isFirstHeightUpdateRef = useRef<boolean>(true); // Ensure fast height update on first entry
    const timerIdRef = useRef<any>(null);
    const [iframeUrl, setIframeUrl] = useState<string | null>(null);
    
    function postMessageToIframe(message: { type: string; height?: number }) {
        const iframe = iframeRef.current;
        if (!iframe || !iframe.contentWindow) return;
        // Send message to child page
        iframe.contentWindow.postMessage({ 
            ...message,
            __iframeResizer: true 
        }, "*");
    }

    useEffect(() => {
        // Initialize iframe height
        if (iframeRef.current) {
            iframeRef.current.style.height = '100vh';
        }

        // Debounce: only write lastHeight back to iframe height after 1 second of silence
        const debounceSetHeight = () => {
            const iframe = iframeRef.current;
            if (!iframe) return;

            console.log('πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§2222222πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§', lastHeightRef.current)

            // If timer is already running, clear it
            if (timerIdRef.current) {
                clearTimeout(timerIdRef.current);
            } else {
                // Only send pause monitoring message on first trigger
                // Tell child page that parent is changing height, pause monitoring
                postMessageToIframe({ 
                    type: "parentChangedHeight",
                    height: lastHeightRef.current
                });
                console.log('πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§ [iframe-autosize] Sending pause monitoring message');
            }

            const delay = isFirstHeightUpdateRef.current ? 120 : 1000;

            timerIdRef.current = setTimeout(() => {
                const iframeLatest = iframeRef.current;
                // Optional: limit height range
                const h = lastHeightRef.current;
                if (iframeLatest && iframeLatest.style.height !== `${h}px` && h > IFRAME_OFFSET) {
                    iframeLatest.style.height = `${h + IFRAME_OFFSET}px`;
                    console.log('πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§  [iframe-autosize] Height updated to:', h);
                }
                // After debounceSetHeight completes, tell child page it can resume monitoring
                postMessageToIframe({ 
                    type: "parentChangedHeightComplete",
                    height: lastHeightRef.current
                });
                console.log('πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§  [iframe-autosize] Sending resume monitoring message');
                timerIdRef.current = null;
                if (isFirstHeightUpdateRef.current) {
                    isFirstHeightUpdateRef.current = false;
                }
            }, delay);
        };

        // Prepare listener for auto-height messages
        function handleMessage(event: MessageEvent) {
            const data: any = event.data;

            // Handle iframeHeightUpdate message type
            if (data?.type === 'iframeHeightUpdate') {
                const iframe = iframeRef.current;
                if (iframe) {
                    const heightNum = typeof data.height === 'number' ? data.height : parseInt(data.height, 10);

                    console.log('πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§111111πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§πŸ’§', heightNum)
                    if (!Number.isNaN(heightNum) && heightNum > 0) {
                        // Update recorded value and start/refresh debounce
                        lastHeightRef.current = heightNum;
                        debounceSetHeight();
                    }
                }
                return;
            }
        }
        window.addEventListener('message', handleMessage);

        // Set iframe URL to trigger rendering
        const _url = url;
        setIframeUrl(_url);

        return () => {
            if (timerIdRef.current) clearTimeout(timerIdRef.current);
            window.removeEventListener('message', handleMessage);
            // Clear iframe URL to completely unmount iframe
            setIframeUrl(null);
        };
    }, [url]);

    // Return null if no iframe should be rendered, this completely unmounts the iframe
    if (!iframeUrl) {
        return null;
    }
    
    return (
        <iframe
            ref={iframeRef}
            src={iframeUrl}
            style={{
                width: '100%',
                height: '100vh',
                border: '0',
                display: 'block'
            }}
            onLoad={() => {
                // After iframe loads, send parentViewport message to ensure bidirectional constraints
                // Also send parentViewport on initialization to ensure bidirectional constraints take effect from the start
                // Delay sending to ensure iframe content is loaded
                setTimeout(() => {
                    postMessageToIframe({ 
                        type: "parentViewport", 
                        height: window.innerHeight 
                    });
                }, 500);
            }}
        />
    );
}

export default IframeLoader;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment