javascriptreactjsuser-interfacefrontend

how to build a modal / full screen component that will always be fixed relative to the viewport?


I have a custom modal / full screen react component that uses position: fixed to ensure that it always take up the full screen.

However, this component will break if one of its ancestor components has a css property that breaks the position: fixed. For example a transform, etc.

How do I build modal / full screen component that is guaranteed to always be position: fixed relative to the viewport regardless of any ancestor component?


Solution

  • The specification CSS Positioned Layout Module Level 3 states for position: fixed:

    [The] box is positioned and sized relative to a fixed positioning containing block ... .

    Further:

    If the box has position: fixed:

    The containing block is established by the nearest ancestor box that establishes an fixed positioning containing block, ... .

    Meaning: For "pure" fixed positioning, you cannot simply ignore the nearest fixed positioning containg block.

    Top layer

    But you can add elements to the top layer of the document. These render "as if they were siblings of the root element".

    You should check browser support for the individual features.

    Popover API

    You can use the Popover API:

    1. Declare an element as a popover element by adding the popover attribute.
    2. Show the popover element, e.g. via the HTMLElement.showPopover() JavaScript method, or via the activation behaviour through adding the popovertarget HTML attribute.

    By default, the popover element is relative to the viewport (see overlay property). Each top-level element has its own ::backdrop pseudo-element.

    Example:

    #my-popover::backdrop {
      background-color: rgba(0,0,0, .2);
    }
    <button popovertarget="my-popover">Open popover</button>
    
    <div id="my-popover" popover>
      <p>Some popover paragraph.</p>
    </div>

    Note: Popovers are not modals; they do not obstruct interaction with the underlying document.

    The <dialog> element

    You can also use the <dialog> element. It is more complex compared to the Popover API, but allows for native modals.

    Note: Use this element for building dialogs or modals, but do not simply abuse it for its functionality.

    Modals are dialogs that...

    1. Trap focus and
    2. Prevent interactions (e.g. pointer events) with the underlying document.

    Similar to popover elements, a dialog is relative to the viewport and—only as a modal— has its own ::backdrop pseudo-element.

    Important: A dialog should always have an explicit closing button.

    Example:

    const dialog = document.querySelector("dialog");
    
    // Add close-by-JS functionality
    dialog.querySelector("#close-js")
        .addEventListener("click", () => dialog.close());
    
    // Add open-as-(non-)modal functionalities
    document.getElementById("show-dialog")
        .addEventListener("click", () => dialog.show());
    document.getElementById("show-modal")
        .addEventListener("click", () => !dialog.open && dialog.showModal());
    dialog::backdrop {
      background-color: rgba(255,0,0, .2);
    }
    <button id="show-dialog">Show dialog</button>
    <button id="show-modal">Show dialog as modal</button>
    
    <dialog>
      <!--Add close-by-form functionality-->
      <form method="dialog">
        <button id="close-form">Close by form</button>
      </form>
      <button id="close-js">Close by JavaScript</button>
      
      <p>Some dialog paragraph.</p>
    </dialog>


    Note: A dialog is not a simple popover: It should not (and therefore cannot) be dismissed, e.g. by clicking outside or pressing Escape. However, should you desire so regardless: (Discouraged example)

    const dialog = document.querySelector("dialog");
    
    dialog.querySelector("#close-js")
        .addEventListener("click", () => dialog.close());
    
    document.getElementById("show-dialog")
        .addEventListener("click", evt => dialog.show());
    document.getElementById("show-modal") // Now closes dialog before this event-handler fires
        .addEventListener("click", evt => dialog.showModal());
    
    // Add close-by-clicking-outside functionality
    document.addEventListener(
      "click",
      evt => {
        if (dialog.open && !dialog.contains(evt.target)) {
          dialog.close();
        }
      },
      /* Capture event to close dialog, before event propagates
       * to any other event-handlers.
       */
      { capture: true }
    );
    
    // Add close-by-escape functionality
    document.addEventListener(
      "keydown",
      evt => evt.code === "Escape" && dialog.close()
    );
    ::backdrop {
      /* Prevent ::backdrop from receiving pointer events;
       * allows close-by-clicking-outside functionality for modal dialogs.
       */
      pointer-events: none;
    }
    <button id="show-dialog">Show dialog</button>
    <button id="show-modal">Show dialog as modal</button>
    
    <dialog>
      <form method="dialog">
        <button id="close-form">Close by form</button>
      </form>
      <button id="close-js">Close by JavaScript</button>
      
      <p>Some dialog paragraph.</p>
    </dialog>