Custom Javascript Guide
Subscription requirement
This feature requires a Pro through Enterprise NotionApps subscription. Users on other subscriptions may not see this option in the builder.
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.
Custom JavaScript runs in the user's browser. Only add JavaScript from trusted sources, and never paste code you do not understand.
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.
Any JavaScript added through this feature can run for end users. Treat it like production client-side code.
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:
Open the app in the NotionApps builder.
Go to the app settings area.
Find the Custom JavaScript section.
Enable Custom JavaScript.

Paste your JavaScript into the editor.
Save the app.
Publish or re-publish the app.
Open the published end-user app.
Verify the script in browser DevTools.
Custom JavaScript applies to the published end-user app, not to the NotionApps builder interface.
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, andlocalStorage.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:
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.
After publishing, open the end-user app and check the browser Console. You should see:
You can also run this in the Console:
Expected result:
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.
For most scripts, use this pattern:
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:
Use this selector to find the app root:
Always check whether the element exists before using it:
Best practice: Target [data-notionapps-end-user-root="true"] instead of targeting broad page-level elements.
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:
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:
Prefer code that runs immediately and checks the current document state:
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.
This script includes a guard:
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.
Do not store sensitive information in localStorage. Users can inspect browser storage.
Example: Read URL Parameters
This example reads a query parameter from the URL and logs it.
URL:
JavaScript:
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.
You can pair this with Custom CSS:
Example: Send a Basic Analytics Event
This example sends a simple event to an analytics endpoint.
Only use endpoints that are approved by your organization.
Do not include private user data, tokens, or secrets in analytics payloads unless your organization has approved that collection.
Example: Track Button Clicks
This example listens for clicks on buttons inside the end-user app.
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.
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.
Example: Load an External Script
Sometimes a customer may need to load an approved external script, such as an analytics script.
Only load external scripts from trusted, approved domains. External scripts can access the same page context as your custom code.
Error Handling
Always handle errors so a script does not break the page.
Recommended pattern:
For network requests:
Avoid Duplicate Elements
If your script adds DOM elements, add a marker attribute and check for it before creating another element.
This prevents duplicate banners, duplicate buttons, and duplicate event widgets.
Avoid Global Variables
Avoid creating global variables like this:
Prefer scoped variables inside a wrapper:
Avoid Fragile Selectors
Avoid selectors that depend on internal generated class names:
Generated class names can change after product updates or rebuilds.
Prefer stable selectors:
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
setIntervalcalls.Avoid heavy work in
MutationObservercallbacks.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
buttonfor 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:
Verify That Custom JavaScript Worked
After saving and publishing, open the published app and use browser DevTools.
Check the Console
Add a log statement:
Then check the Console for:
Check a DOM Attribute
Add an attribute:
Then run this in the Console:
Expected result:
Check Added Elements
If your script adds an element, inspect the page and search for the marker attribute.
Example:
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.logverification message.
The Script Runs but Cannot Find the App Root
The app root may not be available yet.
Use a wait helper:
The Script Adds Duplicate Elements
Add a marker attribute and check for it:
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:
The Script Causes a Console Error
Wrap the script in try...catch:
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
Start with a verification script.
Publish and confirm the script runs.
Add the smallest useful behavior.
Wrap the script in an IIFE.
Add error handling.
Scope DOM changes to the end-user root.
Add marker attributes for injected elements.
Test on desktop and mobile.
Test after navigation or screen changes.
Re-publish and verify the final script.
JavaScript Pattern Reference
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:
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.
Last updated