Please disable your ad blocker.

The only ads on this site are discreet (sidebar on desktop, below the content on mobile).
They do not interfere with reading and may even offer you benefits (discounts, free months…).

HTML/CSS/JS: Easily embed iframes without compromising privacy

Difficulty

This beginner-friendly tutorial provides a complete system that you can copy and paste, then customize to suit your needs.

Embedding a YouTube video, a Dailymotion video, or any other external content is very simple. Doing so without compromising your visitors' privacy while remaining GDPR-compliant is already a bit more challenging.

When I redesigned breat.fr, I couldn't find any simple solution that genuinely prevented data collection before the visitor had given their consent. So I decided to build my own.

Most of the tutorials I found were hiding the problem rather than solving it. Here, the approach is different: until the visitor gives their consent, no iframe exists in the page. It is only created when they explicitly choose to display the content.

HTML

For the HTML, I took inspiration from WordPress shortcodes and configuration tables. I'm fairly lazy: the less code I have to write, the happier I am, and it also reduces the risk of forgetting something.

<div class="rgpd-embed" data-provider="youtube" data-code="VIDEO-CODE"></div>

HTML code explanation

  • class="rgpd-embed": placeholder container.
  • data-provider="youtube": specifies which service should be embedded.
  • data-code="VIDEO-CODE": identifier of the content to embed. In the case of YouTube, this is the video ID, for example IU0q-RXET8c.

That's it. Nothing else is required to embed a video from YouTube, Dailymotion, or any other service that uses a similar identifier.

Now let's look at a complete web page, such as a GitHub Pages site. This is notably what I did in this video game guide Stellaris : Technology tree :

<div class="rgpd-embed" data-fullscreen="1" data-provider="githubpages" data-src="https://example.com"></div>

Unlike YouTube or Dailymotion, GitHub Pages does not provide an identifier that can be used to automatically build the embed URL. Therefore, the URL to load is provided directly through data-src.

I also added the data-fullscreen="1" attribute to display a button that allows the iframe to be toggled to fullscreen mode. This attribute is optional and can be used with any provider if needed.

CSS

This CSS is optional and not required for the system to work. I provide it simply to make this tutorial complete and immediately usable. Feel free to modify it, adapt it to your design, or replace it entirely if you prefer.


/* ========================================================================
    GDPR Placeholder
======================================================================== */
.rgpd-embed {
    background: var(--background-color-primary);
    border-radius: var(--border-radius-4-sides);
    box-shadow: var(--shadow-4-sides);
    color: var(--text-color-primary);
    margin-bottom: 1em;
    min-height: 255.71px;
    padding: 1em;
}

.rgpd-embed h3 {
    margin-top: 0;
}

.rgpd-placeholder {
    box-sizing: border-box;
    display: grid;
    height: 100%;
    padding: 1em;
    place-items: center;
    text-align: center;
    width: 100%;
}

.rgpd-actions {
    align-items: center;
    display: flex;
    gap: 1em;
    justify-content: center;
    margin-top: .75em;
}

.rgpd-fullscreen {
    background: rgba(0, 0, 0, .65);
    border: 0;
    border-radius: var(--border-radius-4-sides);
    color: var(--text-color-primary);
    cursor: pointer;
    padding: 1em;
    position: absolute;
    right: 1em;
    top: 1em;
    z-index: 2;
}

.rgpd-load {
    background: #f1d600;
    border: 0;
    border-radius: var(--border-radius-4-sides);
    cursor: pointer;
    font-size: var(--font-size-normal);
    font-weight: 700;
    padding: .5em 1em;
}

.rgpd-remember {
    align-items: center;
    cursor: pointer;
    display: inline-flex;
    font-size: var(--font-size-normal);
    gap: .5em;
    opacity: .85;
    user-select: none;
}

.rgpd-remember:hover {
    opacity: 1;
}

.rgpd-remember>.fa-square-check {
    color: var(--success-color);
}

JS

As for the JavaScript, the core of the system, I once again kept things as simple as possible without sacrificing robustness:

// ========================================================================
// GDPR Placeholder
// ========================================================================
document.addEventListener('DOMContentLoaded', () => {
    const KEY = 'thirdparty_allow';
    const CODEPEN_USER = 'XXXXX';

    // Preferences cached in memory (avoids JSON.parse on every click)
    let prefs = (() => {
        try {
            return JSON.parse(localStorage.getItem(KEY)) || {};
        } catch {
            return {};
        }
    })();

    const savePrefs = () => localStorage.setItem(KEY, JSON.stringify(prefs));

    const PROVIDERS = {
        codepen: {
            label: 'CodePen',
            src: ({
                code,
                tab = 'result',
                editable = 'true'
            }) => `https://codepen.io/${CODEPEN_USER}/embed/${encodeURIComponent(code)}`
                + `?default-tab=${encodeURIComponent(tab)}`
                + `&editable=${encodeURIComponent(editable)}`
        },
        dailymotion: {
            label: 'Dailymotion',
            src: ({ code }) =>
                `https://geo.dailymotion.com/player.html?video=${encodeURIComponent(code)}`
        },
        githubpages: {
            label: 'GitHub Pages'
        },
        youtube: {
            label: 'YouTube',
            src: ({ code }) =>
                `https://www.youtube.com/embed/${encodeURIComponent(code)}`
        }
    };

    const getProvider = (box) => PROVIDERS[box.dataset.provider];

    const hasFullscreen = (box) => box.dataset.fullscreen === '1';

    const updateRememberUI = (box, active) => {
        const icon = box.querySelector('.rgpd-icon');

        if (!icon) return;

        icon.classList.toggle('fa-square', !active);
        icon.classList.toggle('fa-square-check', active);
    };

    const ensurePlaceholder = (box) => {
        if (box.querySelector('.rgpd-placeholder')) return;

        const p = getProvider(box);
        const label = p?.label || (box.dataset.provider || '');

        box.insertAdjacentHTML('afterbegin', `
            <div class="rgpd-placeholder">
                <h3>${t('rgpd.embed.title')} <span class="rgpd-provider-name-title">${label}</span></h3>
                <p>${t('rgpd.embed.text')}</p>

                <div class="rgpd-actions">
                    <button type="button" class="rgpd-load">${t('rgpd.embed.show')}</button>

                    <div class="rgpd-remember" role="button" tabindex="0">
                        <span aria-hidden="true" class="rgpd-icon fa-regular fa-square fa-lg"></span>
                        <span class="rgpd-text">
                            ${t('rgpd.embed.remember')}
                            <span class="rgpd-provider-name-checkbox">${label}</span>
                        </span>
                    </div>
                </div>
            </div>
        `);
    };

    // This is where the iframe is actually created
    const buildIframe = (src, providerLabel) => {
        const iframe = document.createElement('iframe');

        iframe.src = src;
        iframe.title = `${t('rgpd.embed.iframe')} ${providerLabel}`;
        iframe.loading = 'lazy';

        iframe.setAttribute(
            'allow',
            'accelerometer; encrypted-media; fullscreen; gyroscope; picture-in-picture'
        );

        return iframe;
    };

    const buildSrc = (box) => {
        if (box.dataset.src) return box.dataset.src;

        const p = getProvider(box);
        const code = box.dataset.code;

        if (!p || !code) return '';

        return p.src({
            code,
            tab: box.dataset.tab,
            editable: box.dataset.editable
        });
    };

    const updateFullscreenButtons = () => {
        document.querySelectorAll('.rgpd-fullscreen').forEach((btn) => {
            const box = btn.closest('.embed-container');
            const isFullscreen = document.fullscreenElement === box;

            btn.innerHTML = isFullscreen
                ? '<span class="fa-solid fa-2x fa-compress"></span>'
                : '<span class="fa-solid fa-2x fa-expand"></span>';

            const label = isFullscreen
                ? t('ui.fullscreen.hide')
                : t('ui.fullscreen.show');

            btn.setAttribute('aria-label', label);
            btn.setAttribute('title', label);
        });
    };

    const buildFullscreenButton = (box) => {
        const btn = document.createElement('button');

        btn.type = 'button';
        btn.className = 'rgpd-fullscreen';

        btn.addEventListener('click', () => {
            if (document.fullscreenElement === box) {
                document.exitFullscreen?.();
            } else {
                box.requestFullscreen?.();
            }
        });

        return btn;
    };

    const loadBox = (box) => {
        const p = getProvider(box);
        const src = buildSrc(box);

        if (!src) return;

        box.querySelector('.rgpd-placeholder')?.remove();

        box.classList.remove('rgpd-embed');
        box.classList.add('embed-container');

        const iframe = buildIframe(
            src,
            p?.label || box.dataset.provider || ''
        );

        box.appendChild(iframe);

        if (hasFullscreen(box)) {
            box.appendChild(buildFullscreenButton(box));
        }

        updateFullscreenButtons();
    };

    const initBox = (box) => {
        const provider = box.dataset.provider;

        if (!provider) return;

        ensurePlaceholder(box);

        const active = prefs[provider] === true;

        updateRememberUI(box, active);

        if (active) loadBox(box);
    };

    const initAll = () => {
        document.querySelectorAll('.rgpd-embed').forEach(initBox);
    };

    document.addEventListener(
        'fullscreenchange',
        updateFullscreenButtons
    );

    // The DOM is ready, initialize the placeholders
    initAll();

    // Handle clicks
    document.addEventListener('click', (e) => {
        const loadBtn = e.target.closest('.rgpd-load');
        const rememberBtn = e.target.closest('.rgpd-remember');

        if (!loadBtn && !rememberBtn) return;

        const box = e.target.closest('.rgpd-embed');

        if (!box) return;

        const provider = box.dataset.provider;

        if (!provider) return;

        if (rememberBtn) {
            prefs[provider] = !prefs[provider];
            savePrefs();
            updateRememberUI(box, prefs[provider] === true);
            return;
        }

        if (loadBtn) {
            loadBox(box);
        }
    });

    // Keyboard support for the "Always allow" option
    document.addEventListener('keydown', (e) => {
        if (e.key !== 'Enter' && e.key !== ' ') return;

        const rememberBtn = e.target.closest?.('.rgpd-remember');

        if (!rememberBtn) return;

        const box = rememberBtn.closest('.rgpd-embed');

        if (!box) return;

        e.preventDefault();

        const provider = box.dataset.provider;

        if (!provider) return;

        prefs[provider] = !prefs[provider];
        savePrefs();

        updateRememberUI(box, prefs[provider] === true);
    });
});

JS explanation

  • PROVIDERS: lists the supported services (CodePen, Dailymotion, GitHub Pages, YouTube, etc.) as well as the logic used to build their embed URLs.
  • ensurePlaceholder(): dynamically creates the placeholder displayed before the user gives consent.
  • buildSrc(): automatically builds the embed URL from the information provided in the HTML. This makes it possible to use a simplified syntax such as data-code="IU0q-RXET8c" instead of having to write the full URL.
  • buildIframe(): creates the <iframe> element. This is where the main advantage of the system lies: as long as this function is not called, no iframe exists in the page.
  • loadBox(): replaces the placeholder with the iframe after the user takes action.
  • prefs, savePrefs() and localStorage: store the visitor's preferences to avoid asking for consent again on every visit when the Always allow the service... option is enabled.
  • data-fullscreen="1": adds a button that allows the iframe to be displayed in fullscreen mode. This feature is optional and independent of the provider being used.

The most important thing to remember is that the iframe is never present in the initial HTML code. It is created only after an explicit action by the visitor. This guarantees that no request is sent to the third-party service before consent is given.

At the moment, my script only supports four third-party services, but you can easily add more to suit your needs.

This script uses my translation system. If your website only supports a single language, you can replace the t() calls with plain text. The translation keys used by the script are provided below:

Internationalization (i18n)

{
    "en": {
        "rgpd.embed.iframe": "Displaying external content hosted by",
        "rgpd.embed.remember": "Always allow the service",
        "rgpd.embed.show": "Show content",
        "rgpd.embed.text": "To respect your privacy, click “Show content” to load it.",
        "rgpd.embed.title": "External content hosted by",

        "ui.fullscreen.hide": "Exit fullscreen",
        "ui.fullscreen.show": "Show in fullscreen"
    },
    "fr": {
        "rgpd.embed.iframe": "Affichage du contenu externe hébergé par",
        "rgpd.embed.remember": "Toujours autoriser le service",
        "rgpd.embed.show": "Afficher le contenu",
        "rgpd.embed.text": "Pour respecter votre confidentialité, cliquez sur “Afficher le contenu” pour le charger.",
        "rgpd.embed.title": "Contenu externe hébergé par",

        "ui.fullscreen.hide": "Quitter le plein écran",
        "ui.fullscreen.show": "Afficher en plein écran"
    }
}

The result

Once the system is in place, visitors see a placeholder instead of the external content. The iframe is only created after an explicit action on their part. Feel free to verify this using your browser's developer tools (DevTools); I would even encourage you to do so:

YouTube example

GitHub Pages example

This example also uses the data-fullscreen="1" attribute, which adds a button allowing the content to be displayed in fullscreen mode.