Add Modal Dialogs

Learn how to create a Modal Dialog as a user interface for Commands or as an additional UI for Panels

Overview

Modal dialogs are temporary windows that appear on top of the main application interface, demanding user attention before they can continue working. Unlike panels, which can be docked alongside Premiere's workspace, modal dialogs block interaction with the host application until dismissed—making them ideal for critical decisions, input forms, or alerts that require immediate user response.

In UXP, modal dialogs serve two primary purposes:

  1. Command UIs: Provide an interface for command entrypoints that don't need a persistent panel but require user input or display information.
  2. Panel dialogs: Launch additional interfaces from panels to gather input, show settings, or display detailed information without cluttering the main panel.

You'll learn how to:

  • Create modal dialogs triggered by command entrypoints
  • Display modal dialogs from panel entrypoints
  • Control dialog size and behavior
  • Handle user input and dialog results

Key Concepts

Modal dialogs share the same HTML document as your plugin, similar to multiple panels, but they appear as floating windows that must be closed before the user can interact with Premiere again. This makes them perfect for workflows that require focused user attention—like configuration wizards, confirmation prompts, or data entry forms.

In their simplest form, modal dialogs are just a <dialog> element in the HTML document launched with a uxpShowModal() method.

Modal Dialog

The code below shows a simple modal dialog triggered by a button from the main panel.

Copied to your clipboard
<!DOCTYPE html>
<html>
<head>
<script src="main.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- Panel content -->
<sp-heading>Open Modal Dialog</sp-heading>
<sp-button id="openDialogBtn">Click</sp-button>
<!-- Modal dialog content (hidden by default) -->
<dialog>
<sp-heading>👋 Hello Modal Dialog!</sp-heading>
<sp-divider size="L"></sp-divider>
<sp-body>Modal body content</sp-body>
</dialog>
</body>
</html>

The uxpShowModal() method is asynchronous, it returns a Promise that resolves with the value passed to dialog.close(), and accepts an options object with the following properties.

PropertyTypeDescription
title
string
The title of the dialog
titleVisibility
"show" | "none"
Whether to show the title
resize
"none" | "both" | "horizontal" | "vertical"
Whether to allow the user to resize the dialog
size
{ width: number; height: number; }
The size of the dialog
minSize
{ width: number; height: number; }
The minimum size of the dialog
maxSize
{ width: number; height: number; }
The maximum size of the dialog

Multiple Modal Dialogs

Given how modal dialogs are implemented, nothing prevents you from adding multiple <dialog> elements in your HTML document, each with its own content; you can reference them by id and open them independently using the uxpShowModal() method.

Multiple Modal Dialogs

As they're originated from the same Panel and are modal (blocking) dialogs, only one can be open at a time.

Copied to your clipboard
<!DOCTYPE html>
<html>
<head>
<script src="main.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<sp-heading>Open Modal Dialog</sp-heading>
<sp-button-group>
<sp-button id="openFirstDialogBtn">Open First Dialog</sp-button>
<sp-button id="openSecondDialogBtn">Open Second Dialog</sp-button>
</sp-button-group>
<!-- first modal -->
<dialog id="modal1">
<sp-heading>👋 Hello Modal Dialog!</sp-heading>
<sp-divider size="L"></sp-divider>
<sp-body>Modal body content 1</sp-body>
</dialog>
<!-- second modal -->
<dialog id="modal2">
<sp-heading>👋 Hello Another Modal Dialog!</sp-heading>
<sp-divider size="L"></sp-divider>
<sp-body>Modal body content 2</sp-body>
</dialog>
</body>
</html>

Additionally, you can chain modal dialogs by opening a modal dialog from another modal dialog. Just add the relevant UI elements and event handlers to the modal dialog you want to open from. Being chained, both dialogs are open at the same time, but the first one will be blocked until the second is closed.

add modal dialogs  chained modals

Dialog Lifecycle and Event Handling

Modal dialogs can be programmatically closed by calling the close() method on the dialog element itself. This method takes an optional value that is returned to the code that called uxpShowModal() when the Promise resolves. You can use this value to pass data back to the caller or to signal how the dialog was dismissed.

Example: Close a Modal Dialog and return a value

In this example, we'll create a modal dialog with two buttons: OK and Cancel. When the user clicks either button, the dialog closes and returns the corresponding value—"ok" or "cancel".

Modal Dialog - close event

Please note that, in order to capture the returned value, you need to use the await keyword when calling uxpShowModal() and the "click" event handler on the buttons must be async.

Copied to your clipboard
<!DOCTYPE html>
<html>
<head>
<script src="main.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<sp-heading>Open Modal Dialog</sp-heading>
<sp-button id="openDialogBtn">Click</sp-button>
<dialog>
<sp-heading>👋 Hello Modal Dialog!</sp-heading>
<sp-divider size="L"></sp-divider>
<sp-body>Modal body content</sp-body>
<sp-button-group>
<sp-button id="closeDialogBtn">OK</sp-button>
<sp-button id="cancelDialogBtn">Cancel</sp-button>
</sp-button-group>
</dialog>
</body>
</html>

Modal dialogs are perfect to show a simple interface for a command, when the built-in alert(), prompt() and confirm() methods are not enough. The example below shows how to create a dialog for an About command.

Modal Dialog - About dialog

These are often very simple UIs, used to show information about the plugin or copyright data.

Example: About dialog

Copied to your clipboard
const { entrypoints, host, versions } = require("uxp");
const manifest = require("./manifest.json");
const os = require("os");
entrypoints.setup({
commands: {
"about-command": async () => {
// Create the dialog dynamically (or load from an HTML file)
const dialog = document.createElement("dialog");
dialog.innerHTML = `
<sp-heading>Clip Mixer</sp-heading>
<sp-divider size="L"></sp-divider>
<sp-body>🙌 Thanks for using Clip Mixer v${manifest.version}!</sp-body>
<sp-body><b>Application:</b> ${host.name} v${
host.version
} (${os.platform()})</sp-body>
<sp-body><b>UXP Runtime:</b> ${versions.uxp} - <b>Plugin Version:</b> ${
versions.plugin
}</sp-body>
`.trim();
// ☝ trim is a safety measure to avoid whitespace issues
document.body.appendChild(dialog);
// Add styles programmatically using element.style
dialog.style.color = "white";
dialog.style.padding = "16px";
dialog.querySelector("sp-divider").style.margin = "0 0 16px 0";
dialog.querySelector("sp-heading").style.margin = "0 0 16px 0";
// Show modal
await dialog.uxpShowModal({
title: "Command Modal Dialog",
resize: "none",
size: { width: 300, height: 200 },
});
},
},
});

In the example above, the about-command is defined in the entrypoints array of the manifest.json file and implemented via entrypoints.setup() in the main.js file—see this guide if you need a refresher on Commands concepts, and this tutorial for implementation examples.

Also note that the entire UI is created with a mix of JavaScript methods (for the parent <dialog> element and the styling), and Template Literals for easier readability.

Load external HTML files

To keep your code organized, you can load the HTML content from an external file. In UXP, it is extremely easy to do so using the fetch() method; just make sure to handle the response as a text string and remember that the methods are asynchronous. Using the same structure from the previous example, we can load the HTML content from an external file and use it to fill the dialog.innerHTML property.

Copied to your clipboard
const { entrypoints, host, versions } = require("uxp");
const manifest = require("./manifest.json");
const os = require("os");
entrypoints.setup({
commands: {
"about-command": async () => {
// Load the HTML content from an external file
// In this case, the _dialog.html file is located in the
// same directory as the main.js file
const dialogHtml = await fetch("./_dialog.html") // 👈
.then((res) => res.text()) // 👈 handle the response as text
console.log("About command");
const dialog = document.createElement("dialog");
dialog.innerHTML = dialogHtml.trim();
// ...
// Replace the placeholders with the actual values. It is the
// equivalent of using Template Literals in the previous example
dialog.querySelector("#version").textContent = manifest.version;
dialog.querySelector("#app-name").textContent = host.name;
dialog.querySelector("#app-version").textContent = host.version;
dialog.querySelector("#platform").textContent = os.platform();
dialog.querySelector("#uxp-version").textContent = versions.uxp;
dialog.querySelector("#plugin-version").textContent = versions.plugin;
// ...
document.body.appendChild(dialog);
// ...
const result = await dialog.uxpShowModal({ /* ... */ });
}
}
});

Compared to the previous example, in this one it is slightly more tedious to fill the dialog with values: with Template Literals you can use the ${} syntax to insert the values directly into the string, but with external HTML files you need to use the querySelector() method to select the <span> elements and set the values programmatically.

Please note that the "main" property in the manifest.json file is still set to "main.js", a JavaScript file—even if we are loading the dialog content from an external .html file.

Singleton pattern for Modal Dialogs

For complex dialogs, we recommend using a Singleton pattern to encapsulate the dialog logic and UI. This approach lets you separate dialog creation, initialization, and running logic in a way that's both maintainable and safe against handler duplication when your script is re-evaluated.

Copied to your clipboard
class ModalDialog {
// Singleton support
static #instance;
// Private state
#dialog;
#params; // (optional) store validated values collected from the UI
// Enforce the singleton: return the existing instance if present
constructor() {
if (ModalDialog.#instance) return ModalDialog.#instance;
ModalDialog.#instance = this;
}
// get (or lazily create) the singleton instance
static getInstance() {
if (!ModalDialog.#instance) ModalDialog.#instance = new ModalDialog();
return ModalDialog.#instance;
}
// Build the dialog UI and assign to #dialog
async createDialog() { /* ... */ }
// Set defaults and wire listeners
initDialog() { /* ... */ }
// Show the dialog and return a Promise with the result
async runDialog() { /* ... */ }
// Private: execute Host App DOM logic as needed
async #runRoutine() { /* ... */ }
}
// Example usage
try {
const modalDialog = ModalDialog.getInstance();
await modalDialog.createDialog();
modalDialog.initDialog();
const res = await modalDialog.runDialog();
res;
} catch (error) {
console.error("Argh!", error);
}

A brief explanation of the properties and methods is as follows.

Property/MethodDescription
static #instance
Private static field holding the singleton instance. Ensures only one dialog object exists and is reused.
#dialog
Private reference to the <dialog> element; used internally to show/close the modal.
#params
Private state container for validated values gathered from the UI (optional but useful for passing data to routines).
constructor()
Enforces the singleton by returning the existing instance when one already exists.
static getInstance()
Convenience accessor that returns (or lazily creates) the singleton instance. Prefer this over new in calling code.
async createDialog()
Builds the dialog UI and assigns it to #dialog. Designed to be called once and then reused on subsequent opens.
initDialog()
Initializes the dialog with default values and attaching event listeners (e.g., button clicks, form changes, focus/blur events, etc.)
async runDialog()
Displays the modal and resolves when the dialog closes.
async #runRoutine()
Private method to run any Host App DOM code using the data collected in the dialog.

Example: real-world Dialog

Let's see a practical example of how to use the Singleton pattern to create a modal dialog opened by a Panel. The files are organized as follows:

Copied to your clipboard
📁 . Plugin root directory
├── 📂 fragments Dialog-specific files
│ ├── 📄 dialog.html Modal dialog UI markup
│ └── 🎨 styles.css Dialog-specific styles
├── 📄 index.html Main panel UI
├── 🎨 style.css Main panel styles
├── ⚙️ main.js Main panel and Dialog logic
└── 📋 manifest.json Plugin configuration

From the files tree alone, you can see that we'll make use of fragments to keep the dialog UI and styles separate and avoid using Template Literals or, worse, create all the elements programmatically in the main.js file.

Modal Dialog - Singleton class

The dialog we'll create will be a fictitious "Set sequence dimensions" tool, that allows the user to set the width and height of the sequence. It's stripped from Premiere's API code, but we've kept the logic and the UI as close as possible to the real thing.

Copied to your clipboard
<!DOCTYPE html>
<html>
<head>
<script src="main.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<sp-heading>Open Modal Dialog</sp-heading>
<sp-button id="openDialogBtn">Open Dialog (singleton)</sp-button>
</body>
</html>

Implementation Details

This example demonstrates a production-ready approach to modal dialogs, combining several best practices into a cohesive pattern. Let's examine each file and understand why the code is structured this way.

1. The Panel UI: index.html and style.css

The main panel is intentionally minimal, just a heading and a button. Nothing different from what we've seen so far.

2. The Manifest: manifest.json

The manifest is standard for a panel entrypoint, with the notable inclusion of the enableAlerts feature flag, required to use the alert() method we call in initDialog() when validation fails.

3. The Dialog UI: fragments/dialog.html and fragments/styles.css

The dialog markup lives in a separate fragments/ directory—a pattern borrowed from web development that keeps the dialog UI portable and maintainable. Please note that the whole dialog.html content is wrapped with a <div> element, as the <dialog> element itself will be created dynamically in the main.js file.

The styles use a scoping technique to prevent conflicts with the panel styles; in fact, the <dialog> element will added a modal-dialog class, so the styles will be scoped to the elements it contains and won't leak out. Without it, generic class names like .wrapper or .row would clash between your dialog and panel styles.

4. The Core Logic: main.js

This is where the real work happens. Let's break down the implementation piece by piece.

Global Configuration Object

The G object centralizes all configuration in one place, and contains title, size, default values, and validation ranges.

Singleton Pattern

The Singleton pattern ensures only one instance of the dialog class exists throughout the plugin's lifetime. Without the singleton, you'd create duplicate dialog elements and duplicate event listeners every time the script runs—leading to memory leaks and unpredictable behavior.

The static #instance field is private to the class (note the # prefix). When you call new ModalDialog(), the constructor checks if an instance already exists, and returns that instance instead of creating a new one. The getInstance() static method provides a cleaner API—it's the preferred way to get the singleton and makes the intent explicit.

The #dialog field stores a reference to the actual <dialog> DOM element, while #params holds the validated user input (width and height values in our case) once the user clicks OK. Both are private fields that don't need to be accessed externally.

createDialog()

This method does the heavy lifting of constructing the dialog UI; it's async because it uses fetch() to load external files. Notice that before creating anything it checks if the element already exists in the DOM using document.querySelector(). This is critical for performance and correctness—you don't want multiple copies of the same dialog cluttering the DOM.

The method injects the CSS into a <style> element in the <head>. It assigns an id to the style element (#modal-dialog-styles) so subsequent calls can detect it and skip re-injection. The dialog element itself gets both an id (modal-dialog) and a class (modal-dialog). The ID is for unique identification and DOM queries, while the class is for CSS scoping—remember those dialog.modal-dialog selectors in styles.css.

The else branch handles the case where the dialog already exists (because the singleton was reused). It simply grabs the existing reference instead of creating a new one.

initDialog()

Once the dialog exists in the DOM, initDialog() sets default values and wires up event listeners. This method splits into two logical sections:

  1. State reset.
  2. Event listener attachment.

The first part always runs when you call initDialog()—it resets the input fields to their default values. This ensures a clean slate every time the dialog opens. The second part—the event listener setup—is guarded by a conditional check. It leverages HTML5 data-* attributes to track whether listeners have been attached. If they haven't, the method attaches them and sets the flag to "true". This prevents listener duplication when the dialog is opened multiple times—a common source of bugs.

Several helper functions and event handlers are defined:

  • sanitizeInput(): This placeholder function would normally filter out invalid characters or format the input as the user types.
  • validateAndGetParams(): This function validates the user's input and returns either an object with validated values or null if validation fails. It converts the string values to integers, checks for NaN (which happens if the user enters non-numeric text).
  • OK button handler: When clicked, it calls validateAndGetParams(). If validation succeeds, it stores the validated parameters in this.#params (so they're available to other methods) and closes the dialog with the value "ok". If validation fails, it shows an alert to inform the user. Never silently fail validation, as it creates confusion.
  • Cancel button handler: Simply closes the dialog with the value "cancel".
  • Enter key handler: Provides keyboard accessibility by allowing users to press Enter to submit the dialog instead of clicking OK.

Note that the Enter key handler doesn't close the dialog on invalid input. This is intentional: pressing Enter shouldn't do anything if the input is invalid, forcing the user to either correct the input or explicitly cancel. This prevents accidental submissions.

runDialog()

This method displays the modal and handles its result. It awaits uxpShowModal(), which blocks execution until the dialog is dismissed. The return value (rv) is whatever was passed to dialog.close() in the event handlers—either "ok", "cancel", or "reasonCanceled" (when the user closes the dialog via the title bar or Esc key).

The method uses exceptions for control flow: if the user cancels, it throws "cancel" rather than returning a value. This forces the calling code to handle cancellation explicitly in a catch block, which is a common pattern for operations that can be aborted. If the dialog closes with OK, it calls the private #runRoutine() method to perform the actual work (Premiere API calls, in our case).

#runRoutine()

This private method is where you'd perform any Host Application DOM operations using the collected parameters. The #runRoutine() call is also error-aware: if it returns true, everything succeeded. If it returns anything else (presumably an error message or error object), runDialog() re-throws that error to bubble it up to the caller. This layered error handling ensures that failures at any level—validation, user cancellation, or routine execution—are properly communicated.

getParams()

The final method provides controlled access to the dialog's internal state. Because #params is a private field, external code can't access it directly. This method provides a read-only view by returning a shallow copy of the parameters object (using the spread operator ...). This prevents external code from accidentally (or maliciously) mutating the internal state of the dialog.

Summary

Modal dialogs are temporary, blocking windows that demand user attention before they can continue working with Premiere—ideal for critical decisions, input forms, or alerts.

Key Concepts:

  1. Basic structure: Modal dialogs use a <dialog> HTML element launched with the uxpShowModal() method, which returns a Promise that resolves when the dialog is closed.
  2. Two primary use cases:
    • Command UIs: Provide interfaces for command entrypoints without needing persistent panels.
    • Panel dialogs: Launch additional interfaces from panels to gather input or display information.
  3. Multiple and chained dialogs: One panel can launch multiple independent modal dialogs (only one open at a time), and dialogs can be chained by opening one from another.
  4. Dialog lifecycle: Use dialog.close(value) to dismiss dialogs and return values to the calling code. Handle the "reasonCanceled" return value when users close via the title bar or Esc key.
  5. Best practices: For complex dialogs, use the Singleton pattern to prevent duplicate DOM elements and event listeners. Load external HTML files with fetch() and scope CSS to prevent style conflicts.
  • Privacy
  • Terms of Use
  • Do not sell or share my personal information
  • AdChoices
Copyright © 2025 Adobe. All rights reserved.