> ## Documentation Index
> Fetch the complete documentation index at: https://developers.tally.so/llms.txt
> Use this file to discover all available pages before exploring further.

# JavaScript events

> React to form lifecycle events from your own JavaScript — for embeds, popups, and code injection.

Every Tally widget emits the same set of events. The transport changes depending on the
integration method:

* **Embeds and popups** send events as `postMessage` payloads. Listen for them on the
  `message` event on `window`.
* **Code injection** (running inside the form via your custom domain) receives them as
  `CustomEvent`s dispatched directly on `window`.

<Note>
  Add your event listeners to **the page hosting the embed or popup** — not to the form itself. For
  code injection, add them to the form via your custom domain settings.
</Note>

## Tally.FormLoaded

Fires when the form is rendered. Because embeds and popups are lazy-loaded, you receive
this event each time the form is actually shown.

```typescript theme={null}
interface LoadedPayload {
  formId: string;
}

window.addEventListener('message', (e) => {
  if (e?.data?.includes('Tally.FormLoaded')) {
    const payload = JSON.parse(e.data).payload as LoadedPayload;
    // ...
  }
});
```

## Tally.FormPageView

Fires every time the respondent navigates to a page of the form — handy for multi-page
forms.

```typescript theme={null}
interface PageViewPayload {
  formId: string;
  page: number;
}

// For embeds and 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;
  // ...
});
```

## Tally.FormSubmitted

Fires when the form is submitted. The payload contains the submission metadata **and**
the full set of answers.

```typescript theme={null}
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 and 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;
  // ...
});
```

## Tally.PopupClosed

Fires only for popups, when the popup is closed.

```typescript theme={null}
interface PopupClosedPayload {
  formId: string;
}

window.addEventListener('message', (e) => {
  if (e?.data?.includes('Tally.PopupClosed')) {
    const payload = JSON.parse(e.data).payload as PopupClosedPayload;
    // ...
  }
});
```

<Tip>
  Popups also accept lifecycle callbacks (`onOpen`, `onClose`, `onPageView`, `onSubmit`) directly on
  the `Tally.openPopup()` options argument. See [Popups](/widgets/popups) for the full reference.
</Tip>
