Edit

Create modal dialogs in extensions

Azure DevOps Services | Azure DevOps Server | Azure DevOps Server 2022

Modal dialogs provide a powerful way to create focused user experiences in Azure DevOps extensions. The dialog service lets you present a modal dialog that blocks user interaction with the entire Azure DevOps interface until the dialog gets dismissed. This action ensures that users complete important tasks or provide required information.

Use modal dialogs in your extensions to:

  • Collect user input through forms
  • Display confirmation messages for critical actions
  • Show detailed information that requires user attention
  • Guide users through multi-step processes

Important

When you create modal dialogs, they block interaction with the entire Azure DevOps page, not just your extension. This approach provides a true modal experience but you should use it thoughtfully to avoid disrupting the user workflow.

Prerequisites

Category Requirement Details
Extension setup Working extension project A valid vss-extension.json manifest file
Marketplace registration Extension registered with Visual Studio Marketplace for testing and deployment
Development knowledge Understanding of Azure DevOps extension development basics
Development environment Node.js and npm Node.js version 14 or later with npm installed
Code editor Visual Studio Code or other IDE recommended
Azure DevOps access Access to an Azure DevOps organization for testing
Required packages Extension SDK Install: npm install azure-devops-extension-sdk
Extension API Install: npm install azure-devops-extension-api
Extension permissions Manifest scopes Include appropriate scopes in vss-extension.json, for example: "vso.work", "vso.project"
SDK imports Required modules Import SDK: import * as SDK from "azure-devops-extension-sdk" and services: import { CommonServiceIds, IHostPageLayoutService } from "azure-devops-extension-api"

Dialog contents

To start, declare a contribution of type ms.vss-web.control in your extension manifest. This contribution represents the content displayed within the dialog.

    {
        "id": "registration-form",
        "type": "ms.vss-web.control",
        "description": "The content to be displayed in the dialog",
        "targets": [],
        "properties": {
            "uri": "registration-form.html"
        }
    }

The uri property references a page that is rendered within the content area of the dialog:

<!DOCTYPE html>
<html>
    <head></head>
    <body>
        <h2 id="header">Register now</h2>
        <p>
            <label>Name:</label>
            <input id="inpName" />
        </p>
        <p>
            <label>Date of birth:</label>
            <input id="inpDob" />
        </p>
        <p>
            <label>Email address:</label>
            <input id="inpEmail" />
        </p>
        <div id="root"></div>
        <script type="text/javascript" src="registration-form.js" charset="utf-8"></script>
    </body>
</html>

The corresponding JavaScript file (registration-form.js) initializes the SDK, retrieves any configuration passed from the host, and calls SDK.notifyLoadSucceeded() when ready:

import * as SDK from "azure-devops-extension-sdk";

SDK.init();
SDK.ready().then(() => {
    // Retrieve configuration passed via the dialog options
    const config = SDK.getConfiguration();
    console.log("Dialog configuration:", config);

    SDK.notifyLoadSucceeded();
});

Note

Bundle your JavaScript with a tool like webpack so the import statement resolves correctly at runtime. The HTML page references the bundled output file (for example, registration-form.js).

Show the dialog

To show the dialog (for example, when a user selects an action on a toolbar or menu), call the openCustomDialog method on the IHostPageLayoutService:

import * as SDK from "azure-devops-extension-sdk";
import { CommonServiceIds, IHostPageLayoutService } from "azure-devops-extension-api";

const dialogService = await SDK.getService<IHostPageLayoutService>(CommonServiceIds.HostPageLayoutService);
const extensionCtx = SDK.getExtensionContext();

// Build absolute contribution ID for dialog content
const contributionId = `${extensionCtx.publisherId}.${extensionCtx.extensionId}.registration-form`;

// Show dialog
dialogService.openCustomDialog(contributionId, {
    title: "My Dialog",
    configuration: {
        // Pass initial data to the dialog content
        message: "Please fill out the registration form."
    },
    onClose: (result) => {
        if (result !== undefined) {
            console.log("Dialog result:", result);
        }
    }
});

Handle dialog results

The onClose callback in the dialog options receives the result when the dialog is dismissed. Use the configuration option to pass initial data to the dialog content.

In this example, the host page opens a custom dialog and handles the result when the dialog closes:

import * as SDK from "azure-devops-extension-sdk";
import { CommonServiceIds, IHostPageLayoutService } from "azure-devops-extension-api";

const dialogService = await SDK.getService<IHostPageLayoutService>(CommonServiceIds.HostPageLayoutService);
const extensionCtx = SDK.getExtensionContext();
const contributionId = `${extensionCtx.publisherId}.${extensionCtx.extensionId}.registration-form`;

dialogService.openCustomDialog(contributionId, {
    title: "Registration Form",
    configuration: {
        defaultName: "Contoso User"
    },
    onClose: (result) => {
        if (result !== undefined) {
            // User completed the dialog
            console.log("Registration data:", JSON.stringify(result));
        } else {
            // User dismissed the dialog without completing
            console.log("Dialog was dismissed.");
        }
    }
});

In the dialog content page, use SDK.getConfiguration() to retrieve the data passed from the host. The dialog content page can't directly communicate back to the host except through the onClose result:

import * as SDK from "azure-devops-extension-sdk";

SDK.init();
SDK.ready().then(() => {
    const config = SDK.getConfiguration();
    
    // Use configuration values passed from the host
    if (config.defaultName) {
        document.getElementById("inpName").value = config.defaultName;
    }

    SDK.notifyLoadSucceeded();
});

Show a message dialog

For simple confirmation dialogs with OK and Cancel buttons, use openMessageDialog instead of a custom dialog:

import * as SDK from "azure-devops-extension-sdk";
import { CommonServiceIds, IHostPageLayoutService } from "azure-devops-extension-api";

const dialogService = await SDK.getService<IHostPageLayoutService>(CommonServiceIds.HostPageLayoutService);

dialogService.openMessageDialog("Are you sure you want to proceed?", {
    title: "Confirm Action",
    showCancel: true,
    onClose: (result) => {
        // result is true if OK was selected, false if Cancel
        if (result) {
            console.log("User confirmed.");
        } else {
            console.log("User canceled.");
        }
    }
});

Pass values to the dialog

Pass initial values to dialog content by using the configuration option. The dialog content page retrieves these values with SDK.getConfiguration().

// Host page: pass configuration when opening the dialog
dialogService.openCustomDialog(contributionId, {
    title: "My Dialog Title",
    configuration: {
        itemId: 42,
        timestamp: new Date().getTime()
    },
    onClose: (result) => {
        if (result !== undefined) {
            console.log("Dialog result:", result);
        }
    }
});
// Dialog content page: retrieve configuration
import * as SDK from "azure-devops-extension-sdk";

SDK.init();
SDK.ready().then(() => {
    const config = SDK.getConfiguration();
    console.log("Item ID:", config.itemId);
    console.log("Timestamp:", config.timestamp);

    SDK.notifyLoadSucceeded();
});

Customize dialog buttons

For message dialogs, the okText and cancelText properties specify alternate titles for the OK and Cancel buttons:

dialogService.openMessageDialog("Do you want to save changes?", {
    title: "Save Changes",
    showCancel: true,
    okText: "Yes",
    cancelText: "No",
    onClose: (result) => {
        if (result) {
            console.log("User selected Yes.");
        }
    }
});

Enable light dismiss

For custom dialogs, set lightDismiss to true so that clicking outside the dialog closes it:

dialogService.openCustomDialog(contributionId, {
    title: "My Dialog Title",
    lightDismiss: true,
    onClose: (result) => {
        console.log("Dialog closed.");
    }
});

Note

Light dismiss is enabled by default (true). Set it to false to require the user to explicitly close the dialog.

If you have a question or are looking for more information, consider going to one of the following areas: