Most embeddable widgets are surveillance with rounded corners.
You paste one script tag, it opens a socket back to someone else's server, drops analytics, fingerprints the page, and turns your article into their funnel.
I wanted the opposite.
I had built a small screen-time calculator for an iPhone side project. You enter daily phone hours, how much of that time you'd actually want back, and your age. It returns the number not just as hours per year, but as waking years of the life you have left.
The surprising part was not the maths. The surprising part was that the calculator itself was the first marketing asset I had built that people might reasonably link to.
So the next step was obvious: make it embeddable.
Constraints
I gave myself four rules:
- No tracking script
- No backend callback
- No cookie or storage requirement
- Useful standalone, but with a real reason to click through
That ruled out the normal widget pattern immediately.
I did not want a script that asks the host page for DOM access. I did not want the embed to send typed values back to me. And I did not want to bolt analytics onto a tool whose whole public claim is "nothing leaves your device".
So the widget became a single static iframe page.
The embed snippet
This is the whole thing:
<iframe
src="https://shantj.github.io/sproutguard/embed.html"
width="100%"
height="620"
style="border:0;max-width:600px"
loading="lazy"
title="Screen time calculator"
></iframe>
<p style="font-size:13px;opacity:.7;margin:6px 0 0">
<a href="https://shantj.github.io/sproutguard/screen-time-calculator.html?ct=embed-credit">
Screen Time Calculator
</a>
— free, no signup, runs in your browser.
</p>
No JavaScript include. No SDK. No npm package. Just an iframe and a credit link.
The iframe points at a page that contains the calculator UI and the arithmetic. Because it is a static page, the host site never has to trust my script with its DOM.
The actual calculator logic
The core number is intentionally boring:
const LIFE = 80;
const WAKE = 16;
const perYear = dailyHours * 365;
const yearsLeft = Math.max(LIFE - age, 0);
const wakingYearsLost = (perYear * reclaimableShare * yearsLeft) / (365 * WAKE);
The only modelling choice that really matters is measuring the result in waking years rather than calendar years.
Two hours a day sounds dismissible if you compare it with 24. It lands very differently when you compare it with the roughly 16 hours you are actually conscious.
I also added a second slider for "what share of this would you actually take back?" because the usual calculator lie is pretending every hour on a phone is equally wasted. Maps, calls, music and work are not doomscrolling. The tool is more credible if it admits that.
The part that broke first: height
My first version used a fixed iframe height copied from the desktop render.
That was wrong.
On a narrow phone viewport, the longest verdict string wrapped further, which pushed the footer down, which clipped the only link back to the full calculator. A widget that hides its own attribution on mobile is not a clever growth hack; it is just broken.
The fix was to let the iframe report its height to the parent page:
function postHeight() {
const h = document.documentElement.scrollHeight;
if (window.parent && window.parent !== window) {
window.parent.postMessage({ sproutguardEmbedHeight: h }, "*");
}
}
postHeight();
window.addEventListener("resize", postHeight);
The host page can listen for event.data.sproutguardEmbedHeight and resize the iframe if it wants to. If it does nothing, the default 620px still works.
That gave me a widget that behaves like a normal static embed instead of a cut-off screenshot pretending to be interactive.
The second part that mattered: state handoff
A lot of calculators die at the exact moment they become shareable.
You copy a result, someone opens the link, and they land on the default inputs, not the numbers that made you share it in the first place.
So both the full calculator and the widget rebuild links with the current inputs:
link.href =
"https://shantj.github.io/sproutguard/screen-time-calculator.html" +
"?h=" + hours +
"&w=" + reclaimPercent +
"&a=" + age +
"&ct=embed";
That does two things:
- the recipient lands on the exact numbers being discussed
- I can still distinguish widget-origin visits from the normal calculator page with a simple campaign token
No account required, no cookies required, no analytics SDK required.
Why an iframe instead of a script
A script embed would have been easier to make look native.
I still chose the iframe for three reasons:
- Isolation: the host page does not grant my code access to its DOM
- Privacy: no callback path is needed for the widget to function
- Maintenance: I can change the calculator without asking anyone embedding it to update code
The trade-off is styling control. The widget looks like my component, not the host site's component. For this case that is fine. The point is a working tool, not visual camouflage.
The real lesson
The code was not the interesting part.
The interesting part was learning that if you are building for a privacy-sensitive niche, the distribution mechanism has to obey the same principles as the product itself. A "privacy app" advertised by a widget that phones home would collapse the whole story on contact.
So the embed is deliberately limited:
- one number, not the entire full-page experience
- no tracking
- no callback
- no lock-in
It is useful on its own, and if someone wants the formulas, the shareable result, or the full breakdown, they click through to the main page.
If you want to see the live version, the full calculator is here:
And yes, it was built as part of marketing SproutGuard, my free iPhone screen-time blocker. But the widget works perfectly well whether you install anything or not.
Top comments (0)