Skip to main content Skip to main content
Components

Modal dialog

Status: Experimental

Added in December 2025. Contributed by Chris Pymm in ‘MOJ Forms’.

How to use ‘experimental’ components
Anyone can add an ‘experimental’ status component to the MOJ Design System. They’re early in development and can be used as a starting point. Read about all the Design System statuses.

Contents

A window over a tinted grey background that asks the user 'Are you sure you want to delete this case?' with detail the risks in body content below. Options to continue deleting or go back are below.

Overview

The component is used to present content in a modal dialog. It is used to interrupt a user with information that must be interacted with before continuing. When the dialog is open, the rest of the page is rendered inert and focus is trapped within the dialog.

How the component is currently used

The component was based on prototypes within the GOV.UK Design System discussion backlogs, and was developed for and used within the MOJ Forms platform for live forms.

Contribute to this component

You can help develop this component by adding information to the ‘modal dialog’ Github discussion. This helps other people to use it in their service.

A Figma link was not included when this component was added.

There may be more information in the ‘modal dialog’ Github discussion. You can also view the component image in the overview.

If you have a Figma link for this component (or a component like it) you can add it to Github discussion. This helps other people to use it in their service.

Accessibility findings have been added for this component. There may be more findings in the ‘modal dialog’ Github discussion.

External audit

  • Conducted by: DAC
  • Date: 20 May 2023

Audit findings

No issues discovered

Assistive Technology testing

Date: 15 April 2023

Testing details

No issues discovered

Contribute accessibility findings

If you have accessibility findings that are relevant to this component you can add them to the Github discussion. This helps other people to use it in their service.

Code has been added for this component. There may be other code blocks in the ‘modal dialog’ Github discussion.

Code block 1: HTML


    <body>
      <div class="govuk-modal-dialogue-inert-container">
         <!-- all your page content here -->
      </div>
      <div class="govuk-modal-dialogue" data-inert-container=".govuk-modal-dialogue-inert-container">
         <div class="govuk-modal-dialogue__wrapper" >
           <dialog class="govuk-modal-dialogue__box" aria-labelledby="modal-title" aria-modal="true" role="modal" tabindex="-1">
             <div class="govuk-modal-dialogue__header">
               <button type="button" class="govuk-button govuk-modal-dialogue__close" aria-label="close" data-element="govuk-modal-dialogue-close">x</button>
             </div>
             <div class="govuk-modal-dialogue__content">
               <h2 class="govuk-modal-dialogue__heading govuk-heading-l" id="modal-title">Title</h2>
               <div class="govuk-modal-dialogue__description govuk-body">
                  Content
               </div>
             </div>
           </dialog>
         </div>
         <div class="govuk-modal-dialogue__backdrop"></div>
      </div>
    </body>
    
    

How to use the code

All page content must be wrapped with the .govuk-modal-dialogue-inert-container. The dialog must be outside of this element otherwise it will be impossible to interact with as it will be made inert.

Code block 2: JavaScript


    /*
      Modal Dialog Component
      ----------------------
       Shows content in an accessible modal dialog
    
       Instantiate
       -----------
       var dialog = new ModalDialog(element).init(options)
    
       Options
       -------
       An object which can contain the following keys:
       triggerElement: node which will have a click event listener added to trigger opening of the modal
       focusElement: element that should gain focus when the dialog opens (defaults to the dialog itself)
       onOpen: a callback function which will be invoked when the modal is opened
       onClose: a callback function which will be invoked when the modal is closed
       onDialogNotSupported: a callback function that will be invoked if there is no native <dialog> or dialog polyfill support
    
    */
    
    import { nodeListForEach } from 'govuk-frontend/govuk/common.js'
    
    function ModalDialog ($module) {
      this.$module = $module
      this.$dialogBox = $module.querySelector('dialog')
      this.$container = document.documentElement
    
      // Allowed focussable elements
      this.focussable = [
        'button',
        '[href]',
        'input',
        'select',
        'textarea'
      ]
    
      this.open = this.handleOpen.bind(this)
      this.close = this.handleClose.bind(this)
      this.focus = this.handleFocus.bind(this)
      this.focusTrap = this.handleFocusTrap.bind(this)
      this.boundKeyDown = this.handleKeyDown.bind(this)
    
      // Modal elements
      this.$closeButtons = this.$dialogBox.querySelectorAll('[data-element="govuk-modal-dialogue-close"]')
      this.$focussable = this.$dialogBox.querySelectorAll(this.focussable.toString())
      this.$focusableLast = this.$focussable[this.$focussable.length - 1]
      this.$inertContainer = document.querySelector( this.$module.dataset.inertContainer || '.govuk-modal-dialogue-inert-container' )
    }
    
    // Initialize component
    ModalDialog.prototype.init = function (options) {
    // Check for module
      if (!this.$module) {
        return
      }
    
      this.options = options || {}
    
      this.$focusElement = this.options.focusElement || this.$dialogBox
    
      // Check for native dialog (or polyfill) support
      // if no support trigger a callback allowing us to show a fallback
      if (!this.dialogSupported()) {
        if (typeof this.options.onDialogNotSupported === 'function') {
          this.options.onDialogNotSupported.call()
        }
        return
      }
    
      if (this.$dialogBox.hasAttribute('open')) {
        this.open()
      }
    
      this.initEvents()
    
      return this
    }
    
    ModalDialog.prototype.dialogSupported = function () {
      if (typeof HTMLDialogElement === 'function') {
        // Native dialog is supported by browser
        return true
      } else {
        // Native dialog is not supported by browser so use polyfill
        try {
          window.dialogPolyfill.registerDialog(this.$dialog)
          return true
        } catch (error) {
          // Doesn't support polyfill (IE8)
          return false
        }
      }
    }
    // Initialize component events
    ModalDialog.prototype.initEvents = function (options) {
      if (this.options.triggerElement) {
        this.options.triggerElement.addEventListener('click', this.open)
      }
    
      // Close dialogue on close button click
      this.$closeButtons.forEach( function(element) {
        element.addEventListener('click', this.close.bind(this));
      }.bind(this));
    }
    
    // Open modal
    ModalDialog.prototype.handleOpen = function (event) {
      if (event) {
        event.preventDefault()
      }
    
      // Save last-focussed element
      this.$lastActiveElement = document.activeElement
    
      // Disable scrolling, show wrapper
      this.$container.classList.add('govuk-!-scroll-disabled')
      this.$module.classList.add('govuk-modal-dialogue--open')
    
      //make the content of the page inert
      this.$inertContainer.inert = true
      // hide content from screen readers in browsers that do not support inert
      this.$inertContainer.setAttribute('aria-hidden', 'true')
    
      // Close on escape key, trap focus
      document.addEventListener('keydown', this.boundKeyDown, true)
    
      // Optional 'onOpen' callback
      if (typeof this.options.onOpen === 'function') {
        this.options.onOpen.call(this)
      }
    
      // Skip open if already open
      if (this.$dialogBox.hasAttribute('open')) {
        return
      }
    
      // Show modal
      this.$dialogBox.setAttribute('open', '')
    
      // Handle focus
      this.focus()
    }
    
    // Close modal
    ModalDialog.prototype.handleClose = function (event) {
      if (event) {
        event.preventDefault()
      }
    
      // Skip close if already closed
      if (!this.$dialogBox.hasAttribute('open')) {
        return
      }
    
      // Hide modal
      this.$dialogBox.removeAttribute('open')
    
      // Hide wrapper, enable scrolling
      this.$module.classList.remove('govuk-modal-dialogue--open')
      this.$container.classList.remove('govuk-!-scroll-disabled')
      // make content active again, and un-hide from AT
      this.$inertContainer.inert = false
      this.$inertContainer.setAttribute('aria-hidden', 'false')
    
      // Restore focus to last active element
      this.$lastActiveElement.focus()
    
      // Optional 'onClose' callback
      if (typeof this.options.onClose === 'function') {
        this.options.onClose.call(this)
      }
    
      // Remove escape key and trap focus listener
      document.removeEventListener('keydown', this.boundKeyDown, true)
    }
    
    ModalDialog.prototype.isOpen = function() {
      return this.$dialogBox.hasAttribute('open');
    }
    
    // Lock scroll, focus modal
    ModalDialog.prototype.handleFocus = function () {
      this.$dialogBox.scrollIntoView()
      this.$focusElement.focus({ preventScroll: true })
    }
    
    // Ensure focus stays within modal
    ModalDialog.prototype.handleFocusTrap = function (event) {
      var $focusElement
    
      // Check for tabbing outside dialog
      var hasFocusEscaped = document.activeElement !== this.$dialogBox
    
      // Loop inner focussable elements
      if (hasFocusEscaped) {
        this.$focussable.forEach( function (element) {
          // Actually, focus is on an inner focussable element
          if (hasFocusEscaped && document.activeElement === element) {
            hasFocusEscaped = false
          }
        })
    
        // Wrap focus back to first element
        $focusElement = hasFocusEscaped
          ? this.$dialogBox
          : undefined
      }
    
      // Wrap focus back to first/last element
      if (!$focusElement) {
        if ((document.activeElement === this.$focusableLast && !event.shiftKey) || !this.$focussable.length) {
          $focusElement = this.$dialogBox
        } else if (document.activeElement === this.$dialogBox && event.shiftKey) {
          $focusElement = this.$focusableLast
        }
      }
    
      // Wrap focus
      if ($focusElement) {
        event.preventDefault()
        $focusElement.focus({ preventScroll: true })
      }
    }
    
    // Listen for key presses
    ModalDialog.prototype.handleKeyDown = function (event) {
      var KEY_TAB = 9
      var KEY_ESCAPE = 27
    
      switch (event.keyCode) {
        case KEY_TAB:
          this.focusTrap(event)
          break
    
        case KEY_ESCAPE:
          this.close()
          break
      }
    }
    
    export default ModalDialog
    
    
    

How to use the code

The code uses the native dialog element. If older browser support is needed a polyfill can be used.

Code block 3: CSS


      $govuk-dialogue-width: 640px;
    
      .govuk-modal-dialogue,
      .govuk-modal-dialogue__backdrop {
        position: fixed;
        z-index: 0;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
      }
    
      // Hide dialogue when closed
      .govuk-modal-dialogue {
        display: none;
      }
    
      // Show dialogue when opened
      .govuk-modal-dialogue--open {
        display: block;
      }
    
      // Wrapper to handle overflow scrolling
      .govuk-modal-dialogue__wrapper {
        box-sizing: border-box;
        display: flex;
        height: 100%;
        @include govuk-responsive-padding(7, "top");
        @include govuk-responsive-padding(7, "bottom");
        overflow-y: auto;
        align-items: flex-start; // sass-lint:disable no-duplicate-properties
        align-items: safe center;
      }
    
      // HTML5 dialogue component
      .govuk-modal-dialogue__box {
        box-sizing: border-box;
        display: block;
        position: relative;
        z-index: 1;
        width: 90%;
        margin: auto;
        padding: 0;
        overflow-y: auto;
        border: $govuk-focus-width solid govuk-colour("black");
        background: govuk-colour("white");
    
        // Add focus outline to dialogue
        &:focus {
          outline: $govuk-focus-width solid $govuk-focus-colour;
        }
    
        // Hide browser backdrop
        &::backdrop {
          display: none;
        }
      }
    
      // Header with close button
      .govuk-modal-dialogue__header {
        @include govuk-clearfix;
        @include govuk-responsive-margin(5, "bottom");
        padding-bottom: $govuk-focus-width;
        color: govuk-colour("white");
        background-color: govuk-colour("black");
        text-align: right;
      }
    
      // Inner content
      .govuk-modal-dialogue__content {
        @include govuk-font($size: 16);
        @include govuk-responsive-padding(6);
        padding-top: 0;
      }
    
      .govuk-modal-dialogue__description {
        @include govuk-responsive-margin(4, "bottom");
      }
    
      // Remove bottom margins
      .govuk-modal-dialogue__description:last-child,
      .govuk-modal-dialogue__description > :last-child,
      .govuk-modal-dialogue__content > :last-child {
        margin-bottom: 0;
      }
    
      // Custom backdrop
      .govuk-modal-dialogue__backdrop {
        opacity: .8;
        background: govuk-colour("black");
        pointer-events: none;
        touch-action: none;
      }
    
      // Crown icon
      .govuk-modal-dialogue__crown {
        display: block;
        margin: 6px 0 0 6px;
        @include govuk-responsive-margin(5, "left");
        float: left;
      }
    
      // Heading
      .govuk-modal-dialogue__heading:last-child {
        margin-bottom: 0;
      }
    
      // Close button
      .govuk-modal-dialogue__close {
        $font-size: 36px;
        $line-height: 1;
    
        display: block;
        width: auto;
        min-width: 44px;
        margin: 0;
        padding: 2px 5px;
        float: right;
        color: govuk-colour("white");
        background-color: govuk-colour("black");
        box-shadow: none !important;
        font-size: $font-size;
        @if $govuk-typography-use-rem {
          font-size: govuk-px-to-rem($font-size);
        }
        @include govuk-typography-weight-bold;
        line-height: $line-height;
    
        &:hover {
          color: govuk-colour("black");
          background-color: govuk-colour("yellow");
        }
    
        &:active {
          top: 0;
        }
      }
    
      // New dialogue width, inline button + link
      @include govuk-media-query($from: tablet) {
        .govuk-modal-dialogue__content {
          padding-top: 0;
        }
    
        .govuk-modal-dialogue__box {
          width: percentage($govuk-dialogue-width / map-get($govuk-breakpoints, desktop));
        }
      }
    
      // Fixed width
      @include govuk-media-query($from: desktop) {
        .govuk-modal-dialogue__box {
          width: $govuk-dialogue-width;
        }
      }
    
    
    

Contribute code for this component

If you have code that is relevant to this component you can add it to the Github discussion. This helps other people to use it in their service.