
Use cases
Most raw HTML element implementations combine several capabilities: rendering custom HTML/CSS UI, running custom logic, animations, or timers, saving values to hidden fields, and more. Here are the most common visual experience categories you can build:Learn how to use the Fox API inside raw HTML elements to capture emails from URLs, calculate values from user inputs, route users with dynamic navigation logic, and send custom tracking events.
Create raw HTML
Add HTML code
Go to the Element tab and add your code in the HTML Content editor. It supports full HTML5 syntax with embedded styles and scripts.
Report incorrect code
Copy
Ask AI
<script>
const url = new URL(window.location.href);
const emailParamValue = url.searchParams.get('email');
if (!!emailParamValue) {
fox.inputs.setEmail(emailParamValue);
}
</script>
Due to element limitations:
- No server-side execution: All code runs in the browser.
- No file system access: Can’t read or write local files.
- No Node.js: Browser JavaScript only.
Examples
BMI calculator with typed hidden field write
BMI calculator with typed hidden field write
Subscribes to weight and height inputs, recalculates BMI on every change, and writes the result both as a named variable and as a typed number write to a hidden field. Handles metric and imperial unit systems, converts inputs to a common base, and clears the output for unrealistic values.
BMI calculator
Report incorrect code
Copy
Ask AI
<script>
const BMI_HIDDEN_ID = 'el_FnbIP'; // your hidden input id
const WATCH_INPUTS = [
'measurement_system',
'current_weight_kgs',
'current_weight_lbs',
'height_cm',
'height_ft',
'height_in',
];
fox.inputs.subscribe((name) => {
if (WATCH_INPUTS.includes(name)) updateBMI();
});
updateBMI();
function toMetricWeight(weight, system) {
const w = parseFloat(weight);
if (!Number.isFinite(w)) return null;
return system === 'imperial' ? w * 0.453592 : w;
}
function toMetricHeight(height, inches, system) {
if (system === 'imperial') {
const ft = parseFloat(height);
const inch = parseFloat(inches);
const totalIn = (Number.isFinite(ft) ? ft : 0) * 12 + (Number.isFinite(inch) ? inch : 0);
if (totalIn <= 0) return null;
return totalIn * 2.54; // cm
} else {
const cm = parseFloat(height);
return Number.isFinite(cm) && cm > 0 ? cm : null;
}
}
function calcBMI(kg, cm) {
const m = cm / 100;
if (!Number.isFinite(kg) || !Number.isFinite(m) || m <= 0) return null;
return Math.round((kg / (m * m)) * 10) / 10; // 1 decimal
}
function setBMIValue(val) {
// keep both primitive + typed write
fox.inputs.set('bmi', val === null ? '' : val);
fox.inputs.set(BMI_HIDDEN_ID, val === null ? '' : val);
}
function updateBMI() {
const system = fox.inputs.get('measurement_system')?.value === 'imperial' ? 'imperial' : 'metric';
const weightRaw = system === 'metric'
? fox.inputs.get('current_weight_kgs')?.value
: fox.inputs.get('current_weight_lbs')?.value;
const heightRaw = system === 'metric'
? fox.inputs.get('height_cm')?.value
: fox.inputs.get('height_ft')?.value;
const inchesRaw = fox.inputs.get('height_in')?.value;
const kg = toMetricWeight(weightRaw, system);
const cm = toMetricHeight(heightRaw, inchesRaw, system);
if (!kg || !cm) {
setBMIValue(null);
return;
}
const bmi = calcBMI(kg, cm);
// guardrails: unrealistic values -> clear
if (!bmi || bmi < 10 || bmi > 80) {
setBMIValue(null);
return;
}
setBMIValue(bmi);
fox.inputs.set(BMI_HIDDEN_ID, { value: bmi, type: 'number' }); // typed write for hidden field
}
</script>
Geolocation-based personalization
Geolocation-based personalization
Fetches the visitor’s country from the ipinfo.io API and renders the country name inline using
Intl.DisplayNames. If the request fails or returns no data, falls back to a CSS-blurred placeholder so the screen still looks intentional.Geolocation personalization
Report incorrect code
Copy
Ask AI
<span id="country" style="color:#2DD285;font-family:Roboto, sans-serif;font-size:18px;font-weight:500;line-height:150%;">...</span>
<style>
.ffx-blur {
filter: blur(var(--ffx-blur, 6px));
transition: filter .2s;
}
</style>
<script>
(async function () {
const el = document.getElementById("country");
try {
const res = await fetch("https://ipinfo.io/json?token=YOUR_IPINFO_TOKEN", { headers: { "Accept": "application/json" } });
if (!res.ok) throw new Error("HTTP " + res.status);
const data = await res.json();
const code = (data && data.country) || "";
if (code) {
const name = new Intl.DisplayNames(["en"], { type: "region" }).of(code) || code;
el.textContent = name;
return;
}
} catch (e) {
console.warn("ipinfo country error:", e);
}
// fallback — show blurred placeholder text
el.innerHTML = '<span class="ffx-blur" style="--ffx-blur:6px">location</span>';
})();
</script>
<style>
.ffx-blur { filter: blur(var(--ffx-blur, 6px)); transition: filter .2s; }
</style>
<p> <span class="ffx-blur" style="--ffx-blur:6px">location</span>
</p>
Signature pad with commitment popup and screen jump
Signature pad with commitment popup and screen jump
Renders a canvas-based signature pad with clear and confirm buttons. Once the user draws their signature, a celebration popup appears. Confirming the popup navigates to a specific screen using
fox.navigation.goToId. Replace screen_lbtuWI7r with your target screen ID.Signature pad
Report incorrect code
Copy
Ask AI
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign Your Commitment</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
background: linear-gradient(135deg, #FFF5E6 0%, #FFE4B5 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
}
.container {
max-width: 500px;
width: 100%;
text-align: center;
}
.headline {
font-size: 2rem;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 0.5rem;
}
.subtext {
font-size: 1.1rem;
color: #666;
margin-bottom: 3rem;
}
.signature-container {
background: white;
border-radius: 16px;
padding: 2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
position: relative;
}
.instruction {
font-size: 0.9rem;
color: #999;
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.pen-icon {
font-size: 1.2rem;
animation: float 2s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
#signature-canvas {
border: 2px dashed #ddd;
border-radius: 12px;
cursor: crosshair;
touch-action: none;
width: 100%;
max-width: 400px;
height: 200px;
display: block;
margin: 0 auto;
background: #fafafa;
}
.buttons {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 1.5rem;
}
.btn {
padding: 0.75rem 2rem;
border: none;
border-radius: 50px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-clear {
background: #f0f0f0;
color: #666;
}
.btn-clear:hover {
background: #e0e0e0;
}
.btn-continue {
background: #6B7FFF;
color: white;
opacity: 0.5;
pointer-events: none;
}
.btn-continue.active {
opacity: 1;
pointer-events: all;
}
.btn-continue.active:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(107, 127, 255, 0.3);
}
/* Popup overlay */
.popup-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s;
}
.popup-overlay.show {
display: flex;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.popup-content {
background: white;
border-radius: 20px;
padding: 2rem;
max-width: 400px;
width: 90%;
text-align: center;
animation: slideUp 0.3s;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.popup-image {
width: 100%;
max-width: 300px;
height: auto;
border-radius: 12px;
margin: 1rem 0;
}
.popup-text {
font-size: 1.2rem;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 1.5rem;
}
.popup-btn {
background: #6B7FFF;
color: white;
padding: 1rem 3rem;
border: none;
border-radius: 50px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.popup-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(107, 127, 255, 0.3);
}
@media (max-width: 480px) {
.headline {
font-size: 1.6rem;
}
.subtext {
font-size: 1rem;
}
#signature-canvas {
height: 150px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="signature-container">
<div class="instruction">
<span class="pen-icon">✍️</span>
<span>Draw your signature below</span>
</div>
<canvas id="signature-canvas"></canvas>
<div class="buttons">
<button class="btn btn-clear" id="clear-btn">Clear</button>
<button class="btn btn-continue" id="continue-btn">I'm Committed</button>
</div>
</div>
</div>
<!-- Popup -->
<div class="popup-overlay" id="popup">
<div class="popup-content">
<img
src="https://assets.fnlfx.com/01K7QWS36ZFETYPT7QCXWCHP2H/AOzXXTWT.webp"
alt="Commitment celebration"
class="popup-image"
/>
<p class="popup-text">Your journey to sobriety starts now! 🌻</p>
<button class="popup-btn" id="close-popup">Continue to Your Plan</button>
</div>
</div>
<script>
const canvas = document.getElementById('signature-canvas');
const ctx = canvas.getContext('2d');
const clearBtn = document.getElementById('clear-btn');
const continueBtn = document.getElementById('continue-btn');
const popup = document.getElementById('popup');
const closePopupBtn = document.getElementById('close-popup');
// Set canvas size
function resizeCanvas() {
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Drawing variables
let isDrawing = false;
let hasDrawn = false;
// Drawing settings
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// Get coordinates
function getCoordinates(e) {
const rect = canvas.getBoundingClientRect();
if (e.touches) {
return {
x: e.touches[0].clientX - rect.left,
y: e.touches[0].clientY - rect.top
};
}
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}
// Start drawing
function startDrawing(e) {
isDrawing = true;
hasDrawn = true;
const coords = getCoordinates(e);
ctx.beginPath();
ctx.moveTo(coords.x, coords.y);
// Enable continue button
continueBtn.classList.add('active');
}
// Draw
function draw(e) {
if (!isDrawing) return;
e.preventDefault();
const coords = getCoordinates(e);
ctx.lineTo(coords.x, coords.y);
ctx.stroke();
}
// Stop drawing
function stopDrawing() {
isDrawing = false;
}
// Mouse events
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
// Touch events
canvas.addEventListener('touchstart', startDrawing);
canvas.addEventListener('touchmove', draw);
canvas.addEventListener('touchend', stopDrawing);
// Clear button
clearBtn.addEventListener('click', () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
hasDrawn = false;
continueBtn.classList.remove('active');
});
// Continue button - show popup
continueBtn.addEventListener('click', () => {
if (hasDrawn) {
popup.classList.add('show');
}
});
// Close popup and navigate to next screen
closePopupBtn.addEventListener('click', () => {
popup.classList.remove('show');
// Navigate using fox.navigation
if (typeof fox !== 'undefined' && fox.navigation) {
fox.navigation.goToId('screen_lbtuWI7r');
} else {
console.error('Fox navigation not available');
alert('Navigation would go to: screen_lbtuWI7r');
}
});
// Close popup on overlay click
popup.addEventListener('click', (e) => {
if (e.target === popup) {
popup.classList.remove('show');
}
});
</script>
</body>
</html>
Video carousel with scroll-snap, dots, and active slide observer
Video carousel with scroll-snap, dots, and active slide observer
Builds a circular video carousel using CSS scroll-snap for smooth swiping. An
IntersectionObserver detects the centered slide, marks it active, and pauses any video that scrolls out of view. Navigation dots are generated automatically and scroll the matching slide into view on click.Video carousel
Report incorrect code
Copy
Ask AI
<style>
/* Main container */
.mv-carousel-container {
width: 100%;
max-width: 600px;
margin: 20px auto;
position: relative;
font-family: sans-serif;
}
/* Scroll viewport */
.mv-carousel-viewport {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
gap: 25px;
/* Centering: 30px top/bottom, 12.5% sides (to center the 75% wide slide) */
padding: 30px 12.5%;
box-sizing: border-box;
-ms-overflow-style: none;
scrollbar-width: none;
align-items: center;
touch-action: pan-x pan-y;
}
.mv-carousel-viewport::-webkit-scrollbar {
display: none;
}
/* Player card */
.mv-player {
position: relative;
flex: 0 0 75%;
aspect-ratio: 1 / 1;
border-radius: 50%;
scroll-snap-align: center;
overflow: hidden;
background: #000; /* Background while video loads */
transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1), opacity 0.4s ease;
transform: scale(0.85);
opacity: 0.5;
display: flex;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
/* Active slide */
.mv-player.is-active {
transform: scale(1);
opacity: 1;
z-index: 2;
}
/* Video */
.mv-video {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
pointer-events: none;
background-color: #000;
}
/* Play button */
.mv-play-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
cursor: pointer;
transition: opacity 0.3s;
z-index: 3;
pointer-events: none;
}
.mv-player:not(.is-playing) .mv-play-overlay {
pointer-events: none;
}
.mv-player.is-playing .mv-play-overlay {
opacity: 0;
}
.mv-play-circle {
width: 70px;
height: 70px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
border: 2px solid rgba(255,255,255,0.1);
}
.mv-play-triangle {
width: 0;
height: 0;
border-left: 22px solid #fff;
border-top: 14px solid transparent;
border-bottom: 14px solid transparent;
margin-left: 6px;
}
/* Dots */
.mv-dots {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 20px;
}
.mv-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #ddd;
border: none;
padding: 0;
cursor: pointer;
transition: all 0.3s ease;
}
.mv-dot.active {
background: #222;
transform: scale(1.4);
}
</style>
<div class="mv-carousel-container">
<div class="mv-carousel-viewport" id="carouselViewport">
<div class="mv-player">
<video class="mv-video" src="https://github.com/abogatykh/ff-animations/raw/refs/heads/main/GEN012_CH_Start_Reading2.mp4#t=0.001" playsinline loop preload="metadata"></video>
<div class="mv-play-overlay"><span class="mv-play-circle"><span class="mv-play-triangle"></span></span></div>
</div>
<div class="mv-player">
<video class="mv-video" src="https://github.com/abogatykh/ff-animations/raw/refs/heads/main/3333.mp4#t=0.001" playsinline loop preload="metadata"></video>
<div class="mv-play-overlay"><span class="mv-play-circle"><span class="mv-play-triangle"></span></span></div>
</div>
<div class="mv-player">
<video class="mv-video" src="https://github.com/abogatykh/ff-animations/raw/refs/heads/main/333321.mp4#t=0.001" playsinline loop preload="metadata"></video>
<div class="mv-play-overlay"><span class="mv-play-circle"><span class="mv-play-triangle"></span></span></div>
</div>
</div>
<div class="mv-dots" id="dotsContainer"></div>
</div>
<script>
(function () {
const viewport = document.getElementById('carouselViewport');
const players = document.querySelectorAll('.mv-player');
const dotsContainer = document.getElementById('dotsContainer');
// 1. Create dots
players.forEach((_, i) => {
const dot = document.createElement('button');
dot.classList.add('mv-dot');
if (i === 0) dot.classList.add('active');
dot.addEventListener('click', () => {
players[i].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
});
dotsContainer.appendChild(dot);
});
const dots = document.querySelectorAll('.mv-dot');
// 2. Video playback controls
players.forEach((player) => {
const video = player.querySelector('.mv-video');
const togglePlay = () => {
if (video.paused) {
video.muted = false;
video.play().catch(e => console.log('Autoplay prevented', e));
} else {
video.pause();
}
};
player.addEventListener('click', (e) => {
togglePlay();
});
video.addEventListener('play', () => player.classList.add('is-playing'));
video.addEventListener('pause', () => player.classList.remove('is-playing'));
});
// 3. IntersectionObserver
const observerOptions = {
root: viewport,
threshold: 0.65
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const index = Array.from(players).indexOf(entry.target);
const video = entry.target.querySelector('.mv-video');
const dot = dots[index];
if (entry.isIntersecting) {
entry.target.classList.add('is-active');
if(dot) dot.classList.add('active');
} else {
entry.target.classList.remove('is-active');
if(dot) dot.classList.remove('active');
video.pause();
entry.target.classList.remove('is-playing');
// Optionally reset to start when slide scrolls out (uncomment for cleaner re-entry)
// video.currentTime = 0;
}
});
}, observerOptions);
players.forEach(player => observer.observe(player));
})();
</script>
Next steps
- Custom Code & Fox API - Complete Fox API reference
