How to embed a survey in React, Angular, or plain HTML
One script tag, one iframe, zero framework lock-in. A developer-first walkthrough of embedding a survey anywhere — including the SPA gotchas nobody warns you about.
The promise of an embeddable survey is that it works the same everywhere. The reality is that “paste this snippet” quietly assumes plain HTML, and the moment you're inside React or Angular, the snippet does nothing. This is the developer's version of the guide: what the embed actually is, and exactly how to mount it in each environment.
One script tag, one iframe, no framework SDK. The trick is knowing how your framework treats a <script> in markup.
The one-tag baseline (plain HTML)
On a static page, a server-rendered template, or anywhere you control the raw HTML, this is the whole integration:
<!-- where the survey should appear --> <script src="https://saveform.io/embed.js" data-survey="YOUR_SURVEY_ID" async ></script>
The script reads its own data-survey, derives the origin from its own src (so it works on localhost and in production with no hardcoding), injects an <iframe> right after itself, and starts listening for resize messages. That's it — no build step, nothing to install.
Why it's an iframe, and why that's good
An iframe gets a bad reputation, but for third-party embeds it's the right call, and it buys you two things for free:
- Style isolation. The survey can't inherit — or be broken by — your site's CSS. No reset collisions, no
* { box-sizing }surprises, no z-index war. - Containment. The survey's scripts run in their own document, not your page's global scope. The only thing crossing the boundary is a single, origin-checked resize message.
The usual iframe complaint — a fixed box with its own scrollbar — is solved by the auto-resize below.
React: don't put the script in JSX
React doesn't execute a <script> tag you return from a component — it treats it as inert markup. So you create the element in an effect and append it to a mount node you render:
import { useEffect, useRef } from 'react';
export function SurveyEmbed() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const s = document.createElement('script');
s.src = 'https://saveform.io/embed.js';
s.async = true;
s.dataset.survey = 'YOUR_SURVEY_ID';
s.dataset.target = '#saveform-survey'; // mount the iframe here
el.appendChild(s);
return () => { el.innerHTML = ''; }; // tidy up on unmount / Fast Refresh
}, []);
return <div id="saveform-survey" ref={ref} />;
}Angular and Vue
Same principle, different lifecycle hook. Angular and Vue templates also strip or ignore inline scripts, so create the element in code once the view exists:
import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-survey-embed',
template: '<div #host id="saveform-survey"></div>',
})
export class SurveyEmbedComponent implements AfterViewInit {
@ViewChild('host') host!: ElementRef<HTMLDivElement>;
ngAfterViewInit() {
const s = document.createElement('script');
s.src = 'https://saveform.io/embed.js';
s.async = true;
s.dataset['survey'] = 'YOUR_SURVEY_ID';
s.dataset['target'] = '#saveform-survey';
this.host.nativeElement.appendChild(s);
}
}In Vue it's the same body inside onMounted. And if your framework lets you edit the served index.html directly, the plain one-line snippet dropped into the body still works — sometimes that's the simplest path of all.
Controlling where and how it mounts
Two optional attributes cover the layout cases:
data-target="#selector"— mount the iframe inside a specific element instead of right after the script.data-min-height="320"— the initial height in pixels before the first auto-resize, which reduces layout shift while the survey loads.
Auto-resize, no nested scrollbars
The survey measures its own content and posts its height to the parent; the embed script listens and sets the iframe to match. The listener is deliberately strict — it ignores any message whose origin isn't the survey's, and (since a page can embed more than one survey) any message that isn't tagged with the right survey id. The result is an iframe that grows and shrinks with the survey and never shows its own scrollbar.
Knowing which page sent a response
Because the iframe's own origin is the survey host, the embed forwards the parent page's hostname so each response can be attributed back to the site it came from. Embed the same survey on two landing pages and you can still tell which one is converting — without building a single tracking parameter yourself.
That's the entire surface area. For the non-developer framing — when a survey is the right tool and how the results come back — see adding a survey to your website, or jump straight to the survey docs.
Embed a survey in five minutes
Grab a survey id, drop in one script tag, and it injects and resizes itself on any stack. Start free — no credit card required.