Webflow Integration Guidelines
How to embed AI Risk Navigator visualizations on a Webflow site so they auto-resize, don't make the page jump when users click controls, and scrolling works naturally.
!NOTE This doc only covers Webflow-side configuration. For the list of embed routes and the URL parameters each one accepts, browse/admin?tab=embed-paramson a running deployment, or fetchGET /api/embedsfor the JSON shape. Each embed's schema lives insrc/app/embed/<route>/params.ts.
As of writing, look at the Delphi survey landing and visualization pages for examples of properly configured pages with manually/CMS-populated embeddings. A few other things to note:
- The Webflow MCP is unable to edit the contents of Code Embeds itself, so you’ll need to do it manually. Provided with the Navigator documentation, Claude should be able to help you set it up or debug as necessary.
- Some of the custom JS doesn’t quite work when in Preview mode — you’ll need to publish in order to check that it works.
Instructions
All embeddings on Webflow need to be put in Code Embed blocks, rather than their native iframe component. The code snippet is below. As you may have guessed, you put the source embedding URL where {{EMBED_URL}} is. This can either be directly coded in, or programmatically added from a CMS variable using the “+ Add field” button in the top right of the Code Embed editor.
On pages where there’s only a single visualization, you can ignore the -{{SLUG}} portion of the IDs. The custom CSS and JS could also be added to the page’s <head>, but IMO it's better to keep everything in one place. If there are multiple embeds on a page, you’ll have to manually make each of those ID pairs unique so the reporting stays separate.
<div class="iframe-shell" id="chart-shell-{{SLUG}}">
<iframe id="chart-frame-{{SLUG}}" src="{{EMBED_URL}}" loading="lazy" referrerpolicy="no-referrer" scrolling="no"></iframe>
</div>
<style>
.iframe-shell {
--scale: 0.85;
width: 100%;
overflow: hidden;
position: relative;
}
.iframe-shell iframe {
width: calc(100% / var(--scale));
height: 1200px;
border: 0;
display: block;
transform: scale(var(--scale));
transform-origin: top left;
overflow: hidden;
}
</style>
<script>
(function () {
const shell = document.getElementById('chart-shell-{{SLUG}}');
const frame = document.getElementById('chart-frame-{{SLUG}}');
let rafId = null;
let settleTimer = null;
let lastHeight = 0;
function getScale() {
const value = getComputedStyle(shell).getPropertyValue('--scale').trim();
return parseFloat(value) || 1;
}
function applyHeight(rawHeight) {
const scale = getScale();
const height = Math.round(Number(rawHeight));
if (!height || height < 100) return;
if (height === lastHeight) return;
lastHeight = height;
frame.style.height = height + 'px';
shell.style.height = Math.ceil(height * scale) + 'px';
requestAnimationFrame(removeInternalScrollbarIfPossible);
}
function removeInternalScrollbarIfPossible() {
try {
const doc = frame.contentDocument || frame.contentWindow.document;
if (!doc) return;
const scrollingEl = doc.scrollingElement || doc.documentElement || doc.body;
if (!scrollingEl) return;
const scale = getScale();
const contentHeight = Math.ceil(scrollingEl.scrollHeight);
const visibleHeight = Math.ceil(frame.getBoundingClientRect().height / scale);
if (contentHeight > visibleHeight) {
const extra = contentHeight - visibleHeight;
const newHeight = Math.ceil((parseInt(frame.style.height, 10) || 0) + extra + 2);
frame.style.height = newHeight + 'px';
shell.style.height = Math.ceil(newHeight * scale) + 'px';
lastHeight = newHeight;
}
} catch (err) {
/* cross-origin, cannot inspect */
}
}
function scheduleHeight(rawHeight) {
if (rafId) cancelAnimationFrame(rafId);
if (settleTimer) clearTimeout(settleTimer);
rafId = requestAnimationFrame(function () {
applyHeight(rawHeight);
});
settleTimer = setTimeout(function () {
applyHeight(rawHeight);
}, 80);
}
window.addEventListener('message', function (event) {
if (event.source !== frame.contentWindow) return;
let data = event.data;
if (!data) return;
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (_) {
return;
}
}
if (data.type !== 'embed-height') return;
scheduleHeight(data.height);
});
frame.addEventListener('load', function () {
setTimeout(removeInternalScrollbarIfPossible, 300);
setTimeout(removeInternalScrollbarIfPossible, 800);
});
})();
</script>