# Custom Javascript Guide

{% hint style="warning" %}
**Subscription requirement**

This feature requires a Premium or Enterprise NotionApps subscription. Users on other subscriptions may not see this option in the builder.
{% endhint %}

This guide explains how to use the **Custom JavaScript** feature in NotionApps to add client-side behavior to a published app.

Custom JavaScript lets app builders run JavaScript in the end-user app after the app has loaded in the browser. You can use it for lightweight enhancements such as adding helper UI, sending analytics events, reading URL parameters, attaching event listeners, or applying small DOM-based customizations.

{% hint style="warning" %}
Custom JavaScript runs in the user's browser. Only add JavaScript from trusted sources, and never paste code you do not understand.
{% endhint %}

## Who This Guide Is For

This guide is intended for:

* App builders who want to add lightweight client-side behavior.
* Technical writers documenting NotionApps customization workflows.
* Customer success teams helping users validate custom scripts.
* Developers or advanced users who understand browser JavaScript.

## What Custom JavaScript Can Do

Custom JavaScript can be used to:

* Log a confirmation message when the end-user app loads.
* Add small UI elements, such as a footer note or help banner.
* Read query parameters from the page URL.
* Add event listeners to buttons, links, or forms.
* Send client-side analytics events.
* Modify the DOM after the app renders.
* Observe dynamic page changes with `MutationObserver`.
* Add small accessibility or usability enhancements.

Custom JavaScript should be used for small enhancements. It should not replace core app configuration, permissions, backend validation, or NotionApps product features.

## What Custom JavaScript Should Not Do

Do not use Custom JavaScript to:

* Store secrets, API keys, tokens, or private credentials.
* Hide sensitive data that should not be shown to users.
* Bypass permissions, authentication, or access controls.
* Depend on private internal class names that may change.
* Load untrusted third-party scripts.
* Collect personal data without user consent.
* Perform heavy computations in the browser.
* Make the app unusable if the script fails.

{% hint style="danger" %}
Any JavaScript added through this feature can run for end users. Treat it like production client-side code.
{% endhint %}

## Before You Begin

Before adding Custom JavaScript, make sure:

* You know the exact behavior you want to add.
* The app works correctly without the custom script.
* The script does not require private backend credentials.
* You have tested the script in a safe environment.
* You understand how to open browser DevTools and check the Console.

## Add Custom JavaScript in the Builder

To add Custom JavaScript:

1. Open the app in the NotionApps builder.
2. Go to the app settings area.
3. Find the **Custom JavaScript** section.
4. Enable Custom JavaScript.

<figure><img src="/files/GOq3gJSvD5Gijjx3wr8F" alt=""><figcaption></figcaption></figure>

5. Paste your JavaScript into the editor.
6. Save the app.
7. Publish or re-publish the app.
8. Open the published end-user app.
9. Verify the script in browser DevTools.

{% hint style="info" %}
Custom JavaScript applies to the **published end-user app**, not to the NotionApps builder interface.
{% endhint %}

## Understand Where the Script Runs

Custom JavaScript runs in the browser for the end-user app.

This means:

* It can access browser APIs such as `document`, `window`, and `localStorage`.
* It can read the current URL.
* It can modify visible page elements.
* It can attach event listeners.
* It can send network requests if allowed by browser security rules and CORS.
* It cannot directly access server-only code.
* It cannot safely store secrets because users can inspect browser code.

The supported run location is:

```
end_user_only
```

This means the script is intended to run only in the rendered end-user app.

## Start with a Verification Script

Use a simple script first to confirm that Custom JavaScript is working.

```js
console.log("NotionApps custom JavaScript loaded");

document.documentElement.setAttribute("data-custom-js-loaded", "true");
```

After publishing, open the end-user app and check the browser Console. You should see:

```
NotionApps custom JavaScript loaded
```

You can also run this in the Console:

```js
document.documentElement.getAttribute("data-custom-js-loaded");
```

Expected result:

```
true
```

## Use a Safe Script Wrapper

Wrap custom scripts in an immediately invoked function expression, also called an IIFE. This keeps variables out of the global scope and reduces the chance of naming conflicts.

```js
(function () {
  console.log("Custom JavaScript started");
})();
```

For most scripts, use this pattern:

```js
(function () {
  try {
    console.log("Custom JavaScript started");

    // Add your code here.
  } catch (error) {
    console.error("Custom JavaScript error", error);
  }
})();
```

The `try...catch` block helps prevent one script error from breaking the rest of the page.

## Target the End-User Root Element

The end-user app is wrapped in a root element with this attribute:

```html
data-notionapps-end-user-root="true"
```

Use this selector to find the app root:

```js
const root = document.querySelector('[data-notionapps-end-user-root="true"]');
```

Always check whether the element exists before using it:

```js
const root = document.querySelector('[data-notionapps-end-user-root="true"]');

if (!root) {
  console.warn("NotionApps end-user root not found");
  return;
}
```

{% hint style="success" %}
Best practice: Target `[data-notionapps-end-user-root="true"]` instead of targeting broad page-level elements.
{% endhint %}

## Wait for the App Root

The app may render dynamically. If your script runs before the app root is available, wait for it.

Use this helper:

```js
function waitForElement(selector, callback, timeoutMs) {
  const existingElement = document.querySelector(selector);

  if (existingElement) {
    callback(existingElement);
    return;
  }

  const observer = new MutationObserver(function () {
    const element = document.querySelector(selector);

    if (element) {
      observer.disconnect();
      callback(element);
    }
  });

  observer.observe(document.documentElement, {
    childList: true,
    subtree: true
  });

  window.setTimeout(function () {
    observer.disconnect();
  }, timeoutMs || 10000);
}

waitForElement('[data-notionapps-end-user-root="true"]', function (root) {
  console.log("End-user app root is ready", root);
});
```

This pattern is useful because many modern apps render or update content after the initial page load.

## Avoid Depending Only on the Load Event

The browser `load` event may have already fired by the time your custom script runs.

This can fail:

```js
window.addEventListener("load", function () {
  console.log("The page loaded");
});
```

Prefer code that runs immediately and checks the current document state:

```js
(function () {
  function run() {
    console.log("Running custom script");
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", run);
  } else {
    run();
  }
})();
```

For app elements that render later, use `MutationObserver` or a `waitForElement` helper.

## Example: Add a Footer Note

This example adds a small footer note to the end-user app.

```js
(function () {
  function addFooter(root) {
    if (root.querySelector("[data-brand-footer]")) {
      return;
    }

    const footer = document.createElement("div");
    footer.setAttribute("data-brand-footer", "true");
    footer.textContent = "Powered by Acme Client Portal";
    footer.style.cssText =
      "margin: 24px auto; text-align: center; font-size: 12px; color: #64748b;";

    root.appendChild(footer);
  }

  function waitForElement(selector, callback) {
    const existingElement = document.querySelector(selector);

    if (existingElement) {
      callback(existingElement);
      return;
    }

    const observer = new MutationObserver(function () {
      const element = document.querySelector(selector);

      if (element) {
        observer.disconnect();
        callback(element);
      }
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true
    });
  }

  waitForElement('[data-notionapps-end-user-root="true"]', addFooter);
})();
```

This script includes a guard:

```js
if (root.querySelector("[data-brand-footer]")) {
  return;
}
```

The guard prevents duplicate footers if the script runs more than once.

## Example: Add a Help Banner

This example adds a dismissible help banner above the app content.

```js
(function () {
  const storageKey = "acme-help-banner-dismissed";

  function addBanner(root) {
    if (window.localStorage.getItem(storageKey) === "true") {
      return;
    }

    if (root.querySelector("[data-help-banner]")) {
      return;
    }

    const banner = document.createElement("div");
    banner.setAttribute("data-help-banner", "true");
    banner.style.cssText =
      "display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; margin-bottom: 16px; border: 1px solid #d8ebe7; border-radius: 6px; background: #ecfdf5; color: #064e3b; font-size: 14px;";

    const message = document.createElement("span");
    message.textContent = "Need help? Contact support@example.com.";

    const button = document.createElement("button");
    button.type = "button";
    button.textContent = "Dismiss";
    button.style.cssText =
      "border: 0; background: transparent; color: #065f46; font-weight: 600; cursor: pointer;";

    button.addEventListener("click", function () {
      window.localStorage.setItem(storageKey, "true");
      banner.remove();
    });

    banner.appendChild(message);
    banner.appendChild(button);
    root.prepend(banner);
  }

  const root = document.querySelector('[data-notionapps-end-user-root="true"]');

  if (root) {
    addBanner(root);
  }
})();
```

{% hint style="warning" %}
Do not store sensitive information in `localStorage`. Users can inspect browser storage.
{% endhint %}

## Example: Read URL Parameters

This example reads a query parameter from the URL and logs it.

URL:

```
https://example.notionapps.com/?source=client-email
```

JavaScript:

```js
(function () {
  const params = new URLSearchParams(window.location.search);
  const source = params.get("source");

  if (source) {
    console.log("Traffic source:", source);
  }
})();
```

You can use this pattern for analytics labels, user education flows, or non-sensitive display changes.

## Example: Add a Class Based on a URL Parameter

This example adds a class to the app root when the URL contains `theme=client`.

```js
(function () {
  const params = new URLSearchParams(window.location.search);
  const theme = params.get("theme");
  const root = document.querySelector('[data-notionapps-end-user-root="true"]');

  if (root && theme === "client") {
    root.classList.add("client-theme");
  }
})();
```

You can pair this with Custom CSS:

```css
[data-notionapps-end-user-root="true"].client-theme {
  --brand-primary: #0f766e;
}
```

## Example: Send a Basic Analytics Event

This example sends a simple event to an analytics endpoint.

```js
(function () {
  const payload = {
    event: "app_viewed",
    path: window.location.pathname,
    timestamp: new Date().toISOString()
  };

  fetch("https://analytics.example.com/events", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(payload),
    keepalive: true
  }).catch(function (error) {
    console.warn("Analytics event failed", error);
  });
})();
```

Only use endpoints that are approved by your organization.

{% hint style="warning" %}
Do not include private user data, tokens, or secrets in analytics payloads unless your organization has approved that collection.
{% endhint %}

## Example: Track Button Clicks

This example listens for clicks on buttons inside the end-user app.

```js
(function () {
  const root = document.querySelector('[data-notionapps-end-user-root="true"]');

  if (!root) {
    return;
  }

  root.addEventListener("click", function (event) {
    const button = event.target.closest('button, [role="button"]');

    if (!button || !root.contains(button)) {
      return;
    }

    console.log("Button clicked:", button.textContent.trim());
  });
})();
```

This uses event delegation. Event delegation is useful because buttons may be added or re-rendered after the script runs.

## Example: Observe Dynamic Changes

Some app content changes after navigation, filtering, searching, or form interactions. Use `MutationObserver` to respond to those changes.

```js
(function () {
  const root = document.querySelector('[data-notionapps-end-user-root="true"]');

  if (!root) {
    return;
  }

  const observer = new MutationObserver(function () {
    console.log("The end-user app content changed");
  });

  observer.observe(root, {
    childList: true,
    subtree: true
  });
})();
```

Use observers carefully. Avoid expensive work inside the observer callback.

## Example: Add a Back-to-Top Button

This example adds a floating button that scrolls the user back to the top of the page.

```js
(function () {
  if (document.querySelector("[data-back-to-top]")) {
    return;
  }

  const button = document.createElement("button");
  button.type = "button";
  button.setAttribute("data-back-to-top", "true");
  button.textContent = "Top";
  button.style.cssText =
    "position: fixed; right: 16px; bottom: 16px; z-index: 9999; border: 0; border-radius: 6px; padding: 10px 14px; background: #0f766e; color: white; font-weight: 600; cursor: pointer;";

  button.addEventListener("click", function () {
    window.scrollTo({
      top: 0,
      behavior: "smooth"
    });
  });

  document.body.appendChild(button);
})();
```

## Example: Load an External Script

Sometimes a customer may need to load an approved external script, such as an analytics script.

```js
(function () {
  if (document.querySelector('script[data-custom-analytics="true"]')) {
    return;
  }

  const script = document.createElement("script");
  script.src = "https://analytics.example.com/client.js";
  script.async = true;
  script.setAttribute("data-custom-analytics", "true");

  script.onload = function () {
    console.log("Analytics script loaded");
  };

  script.onerror = function () {
    console.warn("Analytics script failed to load");
  };

  document.head.appendChild(script);
})();
```

{% hint style="danger" %}
Only load external scripts from trusted, approved domains. External scripts can access the same page context as your custom code.
{% endhint %}

## Error Handling

Always handle errors so a script does not break the page.

Recommended pattern:

```js
(function () {
  try {
    // Custom code goes here.
  } catch (error) {
    console.error("Custom JavaScript error", error);
  }
})();
```

For network requests:

```js
fetch("https://example.com/endpoint")
  .then(function (response) {
    if (!response.ok) {
      throw new Error("Request failed with status " + response.status);
    }

    return response.json();
  })
  .then(function (data) {
    console.log("Received data", data);
  })
  .catch(function (error) {
    console.warn("Request failed", error);
  });
```

## Avoid Duplicate Elements

If your script adds DOM elements, add a marker attribute and check for it before creating another element.

```js
if (document.querySelector("[data-custom-footer]")) {
  return;
}

const footer = document.createElement("div");
footer.setAttribute("data-custom-footer", "true");
footer.textContent = "Custom footer";
```

This prevents duplicate banners, duplicate buttons, and duplicate event widgets.

## Avoid Global Variables

Avoid creating global variables like this:

```js
var customerName = "Acme";
```

Prefer scoped variables inside a wrapper:

```js
(function () {
  var customerName = "Acme";
  console.log(customerName);
})();
```

## Avoid Fragile Selectors

Avoid selectors that depend on internal generated class names:

```js
document.querySelector(".css-1abcxyz");
```

Generated class names can change after product updates or rebuilds.

Prefer stable selectors:

```js
document.querySelector('[data-notionapps-end-user-root="true"]');
```

If you must target a specific element, inspect the page carefully and choose the least fragile selector available.

## Security Best Practices

Follow these practices when authoring Custom JavaScript:

* Review every script before publishing.
* Do not paste code from unknown sources.
* Do not include API keys, passwords, tokens, or secrets.
* Do not collect personal data without approval.
* Do not send data to unapproved third-party services.
* Do not use Custom JavaScript to hide sensitive fields.
* Do not change authentication or authorization behavior in the browser.
* Keep scripts short and focused.
* Test scripts in a non-production app first.

## Performance Best Practices

Custom JavaScript runs in the user's browser, so performance matters.

Use these guidelines:

* Keep scripts small.
* Avoid long loops over large DOM trees.
* Avoid frequent timers such as very short `setInterval` calls.
* Avoid heavy work in `MutationObserver` callbacks.
* Load external scripts only when needed.
* Use event delegation instead of adding many individual event listeners.
* Fail gracefully if an external service is unavailable.

## Accessibility Best Practices

If your script adds UI elements:

* Use semantic elements such as `button` for clickable actions.
* Make sure buttons have visible text or accessible labels.
* Ensure custom elements can be used with a keyboard.
* Use sufficient color contrast.
* Do not trap keyboard focus.
* Do not remove focus outlines unless you replace them with an accessible focus style.

Example accessible button:

```js
const button = document.createElement("button");
button.type = "button";
button.textContent = "Dismiss message";
button.setAttribute("aria-label", "Dismiss message");
```

## Verify That Custom JavaScript Worked

After saving and publishing, open the published app and use browser DevTools.

### Check the Console

Add a log statement:

```js
console.log("Custom JavaScript loaded");
```

Then check the Console for:

```
Custom JavaScript loaded
```

### Check a DOM Attribute

Add an attribute:

```js
document.documentElement.setAttribute("data-custom-js-loaded", "true");
```

Then run this in the Console:

```js
document.documentElement.getAttribute("data-custom-js-loaded");
```

Expected result:

```
true
```

### Check Added Elements

If your script adds an element, inspect the page and search for the marker attribute.

Example:

```js
document.querySelector("[data-brand-footer]");
```

If the result is an element, the script added it successfully.

## Troubleshooting

### The Script Does Not Run

Check the following:

* Custom JavaScript is enabled.
* The app was saved after editing the script.
* The app was re-published.
* The published app was hard-refreshed.
* The browser Console does not show a syntax error.
* The script contains a `console.log` verification message.

### The Script Runs but Cannot Find the App Root

The app root may not be available yet.

Use a wait helper:

```js
function waitForElement(selector, callback) {
  const element = document.querySelector(selector);

  if (element) {
    callback(element);
    return;
  }

  const observer = new MutationObserver(function () {
    const element = document.querySelector(selector);

    if (element) {
      observer.disconnect();
      callback(element);
    }
  });

  observer.observe(document.documentElement, {
    childList: true,
    subtree: true
  });
}
```

### The Script Adds Duplicate Elements

Add a marker attribute and check for it:

```js
if (document.querySelector("[data-custom-banner]")) {
  return;
}
```

### The Script Works Once but Breaks After Navigation

The app may re-render content after navigation or screen changes. Use event delegation or `MutationObserver`.

Event delegation example:

```js
const root = document.querySelector('[data-notionapps-end-user-root="true"]');

if (root) {
  root.addEventListener("click", function (event) {
    const button = event.target.closest("button");

    if (button) {
      console.log("Button clicked");
    }
  });
}
```

### The Script Causes a Console Error

Wrap the script in `try...catch`:

```js
(function () {
  try {
    // Custom code goes here.
  } catch (error) {
    console.error("Custom JavaScript error", error);
  }
})();
```

### An External Request Fails

Check:

* The URL is correct.
* The endpoint supports HTTPS.
* The endpoint allows browser requests.
* CORS is configured correctly.
* The request does not require secret credentials.

## Recommended Authoring Workflow

1. Start with a verification script.
2. Publish and confirm the script runs.
3. Add the smallest useful behavior.
4. Wrap the script in an IIFE.
5. Add error handling.
6. Scope DOM changes to the end-user root.
7. Add marker attributes for injected elements.
8. Test on desktop and mobile.
9. Test after navigation or screen changes.
10. Re-publish and verify the final script.

## JavaScript Pattern Reference

| Goal                           | Pattern                                                            |
| ------------------------------ | ------------------------------------------------------------------ |
| Verify script runs             | `console.log("Custom JavaScript loaded")`                          |
| Find app root                  | `document.querySelector('[data-notionapps-end-user-root="true"]')` |
| Avoid global scope             | `(function () { ... })();`                                         |
| Catch errors                   | `try { ... } catch (error) { ... }`                                |
| Read query params              | `new URLSearchParams(window.location.search)`                      |
| Add an element                 | `document.createElement("div")`                                    |
| Prevent duplicates             | `document.querySelector("[data-custom-element]")`                  |
| Listen for clicks              | `root.addEventListener("click", callback)`                         |
| Watch dynamic changes          | `new MutationObserver(callback)`                                   |
| Store non-sensitive preference | `window.localStorage.setItem(key, value)`                          |

## Starter Template

Use this as a starting point for most Custom JavaScript snippets:

```js
(function () {
  try {
    const rootSelector = '[data-notionapps-end-user-root="true"]';

    function run(root) {
      console.log("Custom JavaScript loaded");

      // Add custom behavior here.
    }

    const existingRoot = document.querySelector(rootSelector);

    if (existingRoot) {
      run(existingRoot);
      return;
    }

    const observer = new MutationObserver(function () {
      const root = document.querySelector(rootSelector);

      if (root) {
        observer.disconnect();
        run(root);
      }
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true
    });

    window.setTimeout(function () {
      observer.disconnect();
    }, 10000);
  } catch (error) {
    console.error("Custom JavaScript error", error);
  }
})();
```

## Summary

Custom JavaScript in NotionApps is a flexible way to add small client-side enhancements to published apps. Use it carefully, keep scripts focused, and test thoroughly before publishing. For most use cases, start with a scoped script wrapper, target the end-user root, add error handling, and verify the result in browser DevTools.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.notionapps.com/guides/custom-javascript-guide.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
