Framework-agnostic TypeScript library for embedding Tally.so forms with full type safety
A lightweight, type-safe wrapper around Tally's embed script that works with any JavaScript framework (React, Vue, Svelte, Astro, vanilla JS, etc.).
Tally (tally.so) is the simplest way to create forms for free. This library provides a better developer experience when embedding Tally forms in your web applications.
- ✅ Full TypeScript support - Complete type definitions for all Tally options
- ✅ Framework agnostic - Works with React, Vue, Svelte, Astro, vanilla JS, etc.
- ✅ Zero dependencies - Only loads Tally's official embed script
- ✅ Promise-based API - Async/await support for all methods
- ✅ Error handling - Proper error messages for common mistakes
- ✅ SSR-safe - Works in server-side rendering environments
- ✅ Tree-shakeable - Import only what you need
Tally.so is a free online form builder that lets you create beautiful forms, surveys, and quizzes without any code. If you're looking for how to embed Tally forms in your website or app, this library makes it easier with TypeScript support and a better developer experience.
# Using npm
npm install tally-embed
# Using yarn
yarn add tally-embed
# Using pnpm
pnpm add tally-embed
# Using bun
bun add tally-embedimport TallyEmbed from "tally-embed";
// Open a popup form
await TallyEmbed.openPopup("YOUR_FORM_ID", {
layout: "modal",
width: 500,
onSubmit: (payload) => {
console.log("Form submitted:", payload);
},
});
// Load all embeds on the page
await TallyEmbed.loadEmbeds();
// Close a popup
await TallyEmbed.closePopup("YOUR_FORM_ID");import { useEffect } from "react";
import TallyEmbed from "tally-embed";
function App() {
useEffect(() => {
// Load embeds when component mounts
TallyEmbed.loadEmbeds();
}, []);
const openForm = async () => {
await TallyEmbed.openPopup("YOUR_FORM_ID", {
layout: "modal",
overlay: true,
onSubmit: (payload) => {
alert(`Thank you! Your response ID: ${payload.responseId}`);
},
});
};
return (
<div>
<button onClick={openForm}>Open Form</button>
{/* Or use iframe embed */}
<iframe
data-tally-src={TallyEmbed.getEmbedUrl("YOUR_FORM_ID", {
alignLeft: true,
hideTitle: true,
dynamicHeight: true,
})}
width="100%"
height="500"
title="Contact Form"
style={{ border: 0, margin: 0 }}
/>
</div>
);
}<script setup lang="ts">
import { onMounted } from "vue";
import TallyEmbed from "tally-embed";
onMounted(async () => {
await TallyEmbed.loadEmbeds();
});
const openForm = async () => {
await TallyEmbed.openPopup("YOUR_FORM_ID", {
width: 600,
alignLeft: false,
onSubmit: (payload) => {
console.log("Submitted:", payload);
},
});
};
</script>
<template>
<div>
<button @click="openForm">Open Form</button>
</div>
</template><script lang="ts">
import { onMount } from 'svelte';
import TallyEmbed from 'tally-embed';
onMount(async () => {
await TallyEmbed.loadEmbeds();
});
async function openForm() {
await TallyEmbed.openPopup('YOUR_FORM_ID', {
layout: 'default',
emoji: {
text: '👋',
animation: 'wave'
}
});
}
</script>
<button on:click={openForm}>Open Form</button>---
import TallyEmbed from 'tally-embed';
const embedUrl = TallyEmbed.getEmbedUrl('YOUR_FORM_ID', {
alignLeft: true,
hideTitle: true,
transparentBackground: true,
dynamicHeight: true
});
---
<div>
<iframe
data-tally-src={embedUrl}
width="100%"
height="500"
title="Tally Form"
style="border: 0; margin: 0;"
/>
</div>
<script>
import TallyEmbed from 'tally-embed';
// Load embeds after page loads
TallyEmbed.loadEmbeds();
</script>Load the Tally embed script. This is called automatically by other methods, but you can call it manually to preload the script.
await TallyEmbed.loadScript();Open a popup form.
await TallyEmbed.openPopup("YOUR_FORM_ID", {
layout: "modal", // 'default' | 'modal'
width: 500, // Popup width in pixels
alignLeft: true, // Align to left side
hideTitle: false, // Hide form title
overlay: true, // Show overlay background
emoji: {
// Emoji decoration
text: "👋",
animation: "wave", // 'wave' | 'heart-beat' | 'flash' | etc.
},
autoClose: 3000, // Auto-close after 3 seconds (after submit)
hiddenFields: {
// Pass hidden fields
source: "website",
campaign: "spring-2025",
},
onOpen: () => {
console.log("Form opened");
},
onClose: () => {
console.log("Form closed");
},
onPageView: (page) => {
console.log("Viewing page:", page);
},
onSubmit: (payload) => {
console.log("Submitted:", payload);
},
});Close a specific popup form.
await TallyEmbed.closePopup("YOUR_FORM_ID");Load all Tally iframe embeds on the page (elements with data-tally-src attribute).
await TallyEmbed.loadEmbeds();Generate an embed URL with query parameters.
const url = TallyEmbed.getEmbedUrl('YOUR_FORM_ID', {
alignLeft: true,
hideTitle: true,
transparentBackground: true,
dynamicHeight: true,
hiddenFields: {
source: 'landing-page'
}
});
// Use in iframe
<iframe data-tally-src={url} ... />Create a fully configured iframe element (client-side only).
const iframe = TallyEmbed.createIframe("YOUR_FORM_ID", {
alignLeft: true,
hideTitle: true,
dynamicHeight: true,
});
document.getElementById("form-container").appendChild(iframe);Initialize Tally with global configuration.
await TallyEmbed.init({
formId: "YOUR_FORM_ID", // Auto-open this form
popup: {
layout: "modal",
open: {
trigger: "time", // 'time' | 'exit' | 'scroll'
ms: 5000, // Wait 5 seconds before opening
},
},
});Check if Tally script is loaded.
if (TallyEmbed.isLoaded()) {
console.log("Tally is ready!");
}Full type definitions are included:
import type {
TallyPopupOptions,
TallyEmbedOptions,
TallyEmbedConfig,
TallySubmitPayload,
TallyLayout,
TallyEmojiAnimation,
TallyTrigger,
} from "tally-embed";await TallyEmbed.openPopup("YOUR_FORM_ID", {
showOnce: true, // Only show once (uses localStorage)
key: "my-custom-storage-key", // Optional custom key
});await TallyEmbed.openPopup("YOUR_FORM_ID", {
doNotShowAfterSubmit: true, // Don't show again if user submitted
});// Time-based trigger
await TallyEmbed.init({
formId: "YOUR_FORM_ID",
popup: {
open: {
trigger: "time",
ms: 5000, // Open after 5 seconds
},
},
});
// Exit intent trigger
await TallyEmbed.init({
formId: "YOUR_FORM_ID",
popup: {
open: {
trigger: "exit", // Open when user tries to leave
},
},
});
// Scroll-based trigger
await TallyEmbed.init({
formId: "YOUR_FORM_ID",
popup: {
open: {
trigger: "scroll",
scrollPercent: 50, // Open when user scrolls 50% down
},
},
});await TallyEmbed.openPopup("YOUR_FORM_ID", {
formEventsForwarding: true, // Forward events to Google Analytics/Facebook Pixel
onPageView: (page) => {
// Track page views in your analytics
gtag("event", "form_page_view", { page });
},
onSubmit: (payload) => {
// Track submissions
gtag("event", "form_submit", {
form_name: payload.formName,
response_id: payload.responseId,
});
},
});All methods return Promises and will throw errors if something goes wrong:
try {
await TallyEmbed.openPopup("YOUR_FORM_ID");
} catch (error) {
console.error("Failed to open form:", error);
}The library is SSR-safe and will not throw errors in server-side environments:
// Safe to call during SSR - will only execute in browser
TallyEmbed.loadEmbeds(); // No-op on serverWorks in all modern browsers that support:
- ES2020
- Promises
- fetch API
- IntersectionObserver (for lazy loading)
MIT © Farhan Syah
Contributions welcome! Please open an issue or PR on GitHub.
This library is built on top of Tally.so's official embed script. Tally is a trademark of Tally Forms Inc. This is an unofficial community library and is not affiliated with or endorsed by Tally.so.
Created by Farhan Syah.