Event listenersForm loadedForm page viewForm submissionPopup closedEmbedsTally.loadEmbeds()Save website page and query parametersPopupsTally.openPopup()Tally.closePopup()Save website page and query parametersCustom domain code injectionDOMContentLoaded event handlerGet an input elementDynamically set a value to an existing input elementGet previously entered answers from the browser’s local storageExamplesEmbed in a React appEmbed in Next.js appCollect the respondent’s IP address
Event listeners
If you are using the Standard embed, Popup or Custom domain code injection, you can add a JavaScript event listener to get notified on specific events sent from the form.
If you’re using an embed or popup, please add the following snippets to your website on the page where the embed or popup will appear.
Form loaded
Since the standard embed and the popup are lazy loaded, you will receive this event whenever the form is loaded i.e. shown to the respondent.
interface LoadedPayload { formId: string; } window.addEventListener('message', (e) => { if (e?.data?.includes('Tally.FormLoaded')) { const payload = JSON.parse(e.data).payload as LoadedPayload; // ... } });
Form page view
This is specifically handy if you have a form with multiple pages so you get an event each time the respondent navigates to a page.
interface PageViewPayload { formId: string; page: number; } // For embeds/popups window.addEventListener('message', (e) => { if (e?.data?.includes('Tally.FormPageView')) { const payload = JSON.parse(e.data).payload as PageViewPayload; // ... } }); // For code injection via a custom domain window.addEventListener('Tally.FormPageView', (e) => { const payload = e.detail as PageViewPayload; // ... });
Form submission
This event is triggered when the form is submitted and it contains not only some metadata, but also the form data (respondent’s answers).
interface SubmissionPayload { id: string; // submission ID respondentId: string; formId: string; formName: string; createdAt: Date; // submission date fields: Array<{ id: string; title: string; type: 'INPUT_TEXT' | 'INPUT_NUMBER' | 'INPUT_EMAIL' | 'INPUT_PHONE_NUMBER' | 'INPUT_LINK' | 'INPUT_DATE' | 'INPUT_TIME' | 'TEXTAREA' | 'MULTIPLE_CHOICE' | 'DROPDOWN' | 'CHECKBOXES' | 'LINEAR_SCALE' | 'FILE_UPLOAD' | 'HIDDEN_FIELDS' | 'CALCULATED_FIELDS' | 'RATING' | 'MULTI_SELECT' | 'MATRIX' | 'RANKING' | 'SIGNATURE' | 'PAYMENT'; answer: { value: any; raw: any; }; }>; } // For embeds/popups window.addEventListener('message', (e) => { if (e?.data?.includes('Tally.FormSubmitted')) { const payload = JSON.parse(e.data).payload as SubmissionPayload; // ... } }); // For code injection via a custom domain window.addEventListener('Tally.FormSubmitted', (e) => { const payload = e.detail as SubmissionPayload; // ... });
Popup closed
This is only for the popup and it is triggered when the popup is closed.
interface PopupClosedPayload { formId: string; } window.addEventListener('message', (e) => { if (e?.data?.includes('Tally.PopupClosed')) { const payload = JSON.parse(e.data).payload as PopupClosedPayload; // ... } });
Embeds
You can programmatically load embeds using a method from the window.Tally object.
// Include the Tally widget script in the <head> section of your page <script src="https://tally.so/widgets/embed.js"></script>
Then you would add your embed HTML somewhere on your website:
<iframe data-tally-src="https://tally.so/embed/mRoDv3?alignLeft=1&hideTitle=1&transparentBackground=1&dynamicHeight=1" loading="lazy" width="100%" height="200" frameborder="0" marginheight="0" marginwidth="0" title="Newsletter"></iframe>
Tally.loadEmbeds()
This won’t load the embed yet. To load it call this method:
Tally.loadEmbeds();
Keep in mind, all embeds are instantly loaded if they are in the viewport or 500px away from the viewport’s edges. Otherwise, an observer is set up, which will wait for the embed to satisfy the requirements mentioned above before it get loaded.
Save website page and query parameters
Your website's page and all query parameters will be automatically forwarded to the Tally embed and could be saved using hidden fields. For example, if your page's URL looks like the one below and you have hidden fields for
originPage
, ref
and email
, you will see the following in your form submissions.https://company.com/register?ref=downloads&[email protected]
originPage | /register |
ref | downloads |
email |
Popups
You can open and close popups using JavaScript via the window.Tally object. It comes in handy when you want to define your own business logic on when to open a certain popup.
// Include the Tally widget script in the <head> section of your page <script src="https://tally.so/widgets/embed.js"></script>
The Tally object exposes 2 methods, which are defined below. Both of them require the Form ID, which can be found in the URLs of your form:
https://tally.so/r/[FORM_ID] https://tally.so/embed/[FORM_ID] https://tally.so/forms/[FORM_ID]/edit
Tally.openPopup()
// Example Tally.openPopup('mRoDv3', { layout: 'modal' }); // Reference openPopup: ( formId: string, options?: { key?: string; // This is used as a unique identifier used for the "Show only once" and "Don't show after submit" functionality layout?: 'default' | 'modal'; width?: number; alignLeft?: boolean; hideTitle?: boolean; overlay?: boolean; emoji?: { text: string; animation: 'none' | 'wave' | 'tada' | 'heart-beat' | 'spin' | 'flash' | 'bounce' | 'rubber-band' | 'head-shake'; }; autoClose?: number; // in milliseconds showOnce?: boolean; doNotShowAfterSubmit?: boolean; customFormUrl?: string; // when you want to load the form via it's custom domain URL hiddenFields?: { [key: string]: any, }; onOpen?: () => void; onClose?: () => void; onPageView?: (page: number) => void; onSubmit?: (payload: SubmissionPayload) => void; } ) => void;
Tally.closePopup()
// Example Tally.closePopup('mRoDv3'); // Reference closePopup: (formId: string) => void;
Save website page and query parameters
Your website's page and all query parameters will be automatically forwarded to the Tally popup and could be saved using hidden fields. For example, if your page's URL looks like the one below and you have hidden fields for
originPage
, ref
and email
, you will see the following in your form submissions.https://company.com/register?ref=downloads&[email protected]
originPage | /register |
ref | downloads |
email |
Custom domain code injection
Refer to the Event listeners above for all support form events.
DOMContentLoaded event handler
If you want to access Tally input elements wrap your code in a DOMContentLoaded event handler.
document.addEventListener('DOMContentLoaded', function () { // Your code goes here // ... });
Get an input element
Let’s assume we want to get the email input element.
document.addEventListener('DOMContentLoaded', function () { // You can find the input's ID by inspecting your form const emailInput = document.getElementById('c1cbc8e4-b2f3-4e63-a683-ec9eadbcb022'); });
Dynamically set a value to an existing input element
In this example, we will get the provided email and use it to fetch the user’s unique identifier.
document.addEventListener('DOMContentLoaded', async function () { // We get the input element const emailInput = document.getElementById('c1cbc8e4-b2f3-4e63-a683-ec9eadbcb022'); // Get the email that was typed in const email = emailInput.value; // Do API lookup here using the email to get the user's unique identifier const response = await fetch(`https://api.example.com/getUserId?email=${email}`); const { userId } = await response.json(); // Set the userID in the input field const userIdInput = document.getElementById('184b69ee-0c9f-449a-b361-b5250ccd2cb3'); // This is necessary to bubble the event up to the input and update the React app state const nativeInputValueSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value', ).set; nativeInputValueSetter.call(userIdInput, userId); userIdInput.dispatchEvent(new Event('input', { bubbles: true })); });
Get previously entered answers from the browser’s local storage
// You can find the form ID in the URL of the form pages // Example: https://tally.so/forms/mRoDv3/share const formId = 'mRoDv3'; const data = localStorage.getItem(`FORM_DATA_${formId}`); if (data) { data = JSON.parse(data); }
Examples
Embed in a React app
import { useEffect } from 'react'; const MyComponent = () => { // The code below will load the embed useEffect(() => { const widgetScriptSrc = 'https://tally.so/widgets/embed.js'; const load = () => { // Load Tally embeds if (typeof Tally !== 'undefined') { Tally.loadEmbeds(); return; } // Fallback if window.Tally is not available document .querySelectorAll('iframe[data-tally-src]:not([src])') .forEach((iframeEl) => { iframeEl.src = iframeEl.dataset.tallySrc; }); }; // If Tally is already loaded, load the embeds if (typeof Tally !== 'undefined') { load(); return; } // If the Tally widget script is not loaded yet, load it if (document.querySelector(`script[src="${widgetScriptSrc}"]`) === null) { const script = document.createElement('script'); script.src = widgetScriptSrc; script.onload = load; script.onerror = load; document.body.appendChild(script); return; } }, []); return ( <div> <iframe data-tally-src="https://tally.so/embed/mRoDv3?alignLeft=1&hideTitle=1&transparentBackground=1&dynamicHeight=1" loading="lazy" width="100%" height="216" frameBorder={0} marginHeight={0} marginWidth={0} title="Newsletter subscribers" ></iframe> </div> ); }; export default MyComponent;
Embed in Next.js app
// pages/index.tsx import Script from 'next/script' export default function Homepage() { return ( <div> <iframe data-tally-src="https://tally.so/embed/mRoDv3?alignLeft=1&hideTitle=1&transparentBackground=1&dynamicHeight=1" loading="lazy" width="100%" height="216" frameBorder={0} marginHeight={0} marginWidth={0} title="Newsletter subscribers" ></iframe> <Script src="https://tally.so/widgets/embed.js" onLoad={() => Tally.loadEmbeds()} /> </div> ); }
Collect the respondent’s IP address
Privacy and Security: Fetching the visitor’s IP address raises privacy concerns. Make sure to inform users and obtain consent if necessary.
This example requires the use of a custom domain and code injection. Copy the code below to the Code injection box of your custom domain and replace the input’s ID with your own.
const getIPAddress = async () => { try { const response = await fetch('https://api.ipify.org?format=json'); if (!response.ok) { throw new Error('Network response was not ok'); } const data = await response.json(); return data.ip; } catch (error) { console.error('Error fetching the IP address:', error); throw error; } }; document.addEventListener('DOMContentLoaded', async () => { // Create a Short text field at the beginning of your form, we will use it to collect the IP address of the visitor // You can find the input's ID by inspecting your form in Preview mode, for example const ipInput = document.getElementById( 'c1cbc8e4-b2f3-4e63-a683-ec9eadbcb022' ); if (!ipInput) { return; } // Hide the input so it's not visible in the form ipInput.style.display = 'none'; // Get the IP address of the visitor const ip = await getIPAddress(); // This is necessary to bubble the event up to the input and update the React app state const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; nativeInputValueSetter.call(ipInput, ip); ipInput.dispatchEvent(new Event('input', { bubbles: true })); });