Edit Profile

Dark Mode

Copy config

Copy and paste the following code into your global.css file to apply the styles.

DISCLAIMER: This component is in
Beta
status. That means that it is ready for production, but the API might change.

Modal

A window overlaid on either the primary window or another dialog window. Modal content is placed in the top layer, rendering the underneath content as inert / non-interactive.

The modal makes use of the HTML dialog element, which is supported in every major browser.

Modals are used when an important choice needs to be made in the application. The rest of the content isn't interactive until a certain action has been performed.

✨ Features

  • Follows the WAI ARIA Dialog & Alertdialog design patterns.
  • Managed focus order
  • Scroll locking
  • Closes on escape
  • Toggle backdrop dismiss
  • Animation support
  • Transition support
  • Backdrop animations

Building Blocks

import { component$, useSignal } from '@builder.io/qwik';
import { Modal, ModalDescription, ModalTitle } from '@qwik-ui/headless';

export default component$(() => {
  const isOpen = useSignal(false);

  return (
    <Modal bind:show={isOpen}>
      <ModalTitle>Accessible Name</ModalTitle>
      <ModalDescription>Optional Description</ModalDescription>
      {/* other content */}
    </Modal>
  );
});

🎨 Anatomy

ComponentDescription
Modal

The root component that contains the dialog element.

ModalTitle

An accessible name for the modal.

ModalDescription

An optional accessible description for the modal.

Dialog extendability

The Qwik UI Modal component is built on top of the dialog element. To see additional capabilities of the dialog, take a look at the offical MDN page.

Our goal is to fill existing gaps, ensuring the modal is a11y compliant and enriched with HTML spec features, reducing reliance on Qwik UI maintainers.

Attribute example

Above is an example of the autofocus attribute, used to focus the 2nd input when the dialog is opened.

Open or close the modal

To open or close the modal, we can use bind:show, a custom signal bind that leverages the bind syntax in Qwik.

showSig is a signal that we pass into our Modal component to customize when the modal can be open or closed.

The example above opens the modal when the Open Modal button is clicked and closes it when a button inside the modal is clicked.

Custom signal binds are like a remote control for components, controlling states like opening or closing a modal.

The Top Layer

In Chrome and Edge, a top layer UI button is shown in the dev tools. This means the element content is placed above any other content on the page, preventing any overflow or style issues.

The top layer is used in Qwik UI whenever a modal or popover is used in supported browsers. Popovers can be applied to any semantic element, modals in Qwik UI are tied to the dialog element.

Backdrops

To add a modal backdrop, the ::backdrop pseudo element can be utilized. Backdrops are right underneath top layer elements.

By default, the dialog element comes with a subtle backdrop, below is a snippet customizing the backdrop background along with the backdrop filter css property.

.modal-backdrop::backdrop {
  /* changing background */
  background: rgba(0, 0, 0, 0.4);

  /* providing multiple filters */
  backdrop-filter: grayscale(90%) blur(10px);
}

Dismissing Modals via Backdrop

Modals in Qwik UI can be dismissed by default when the backdrop is clicked. However, this behavior can be modified using the closeOnBackdropClick property.

The example above demonstrates a modal where the closeOnBackdropClick property is set to false. As a result, clicking on the backdrop does not dismiss the modal.

Alertdialog

An alert dialog is a modal that interrupts user workflow to convey critical information and obtain a response. It's typically used for confirmations, error messages, or destructive actions.

Deactive Account

Are you sure you want to deactivate your account?

By adding the alert prop to our component, we adhere to the WAI ARIA Alertdialog specification, enabling assistive technologies to distinguish alert dialogs for special handling.

Alertdialogs do not allow the backdrop to be closed when clicked, regardless of whether the closeOnBackdropClick prop was set to true.

Focus Locking in Modals

Qwik UI incorporates a feature known as focus locking, which confines the focus within the modal component when it is open.

This means that if a user is navigating through the modal using the Tab key, reaching the last focusable element and pressing Tab again will cycle the focus back to the first focusable element within the modal.

Stripped Styles

As a headless library, we intentionally try not to add any styles to the components.

However, because Qwik UI builds on top of native solutions when they are well-supported, feasible, and performant, some of the widgets may inclue browser user-agent styles.

These styles can be unintuitive tricky to debug. Which has been the case with Qwik UI's own docs site. As a result, we have stripped these styles from the Modal component.

@layer qwik-ui {
  /* browsers automatically set an interesting max-width and max-height for dialogs 
      https://twitter.com/t3dotgg/status/1774350919133691936
  */
  dialog:modal {
    max-width: unset;
    max-height: unset;
  }
}

While in most cases, this would be up to a consumer's CSS reset to solve, in this case we are stripping the max-width and max-height styles on the dialog element under the hood.

This is done in a separate layer so that styles are easily overridable in consumer facing applications.

Animations

Animating things to display none has historically been a significant challenge on the web. This is because display none is a discrete property, and is unanimatable.

Our current approach

Qwik UI automatically detects any animation or transition declarations under the hood and waits for them to finish before closing the modal. If there is no animation, then it will close normally.

Adding a transition

To add an transition, use the data-open, data-closing and data-closed data attributes. Above is a snippet where we transition both the modal and backdrop's opacity.

Adding an animation

To add an animation, it's the same as with transitions, using the data-open and data-closing data attributes. Below is a snippet of the animation example above.

Backdrop animations

Backdrop animations have also made significant progress in the last year, with support provided in over half of the major browsers, and close to 70% of users.

To add a backdrop animation, make sure to use the ::backdrop pseudo selector to the end of the data-closing or data-open classes.

Sheets

Sheets are a type of modal/overlay used to provide temporary access to important information, while also being easily dismissible.

Side Sheet

Bottom Sheet

Bottom sheets are more prevalent in mobile applications, usually to simplify UI. That said, feel free to use them wherever fits best!

Animations / Transitions CSS

:root {
  --modal-animation: forwards cubic-bezier(0.6, 0.6, 0, 1);
}

@keyframes modalFadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes modalFadeOut {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

.modal-animation::backdrop,
.sheet::backdrop,
.bottom-sheet::backdrop {
  animation: modalFadeIn 0.75s var(--modal-animation);
}

.modal-animation[data-closing]::backdrop,
.sheet[data-closing]::backdrop,
.bottom-sheet[data-closing]::backdrop {
  animation: modalFadeOut 0.35s var(--modal-animation);
}

.modal-animation::backdrop {
  background: hsla(0, 0%, 0%, 0.5);
}

@keyframes modalOpen {
  from {
    opacity: 0;
    transform: scale(150%);
  }
  to {
    opacity: 1;
    transform: scale(100%);
  }
}

@keyframes modalClose {
  from {
    opacity: 1;
    transform: translateY(0%);
  }
  to {
    opacity: 0;
    transform: translateY(-200%);
  }
}

.modal-animation {
  animation: modalOpen 0.75s var(--modal-animation);
}

.modal-animation[data-closing] {
  animation: modalClose 0.35s var(--modal-animation);
}

@keyframes sheetOpen {
  from {
    opacity: 0;
    transform: translateX(100%);
  }
  to {
    opacity: 1;
    transform: translateX(0%);
  }
}

@keyframes sheetClose {
  from {
    opacity: 1;
    transform: translateX(0%);
  }
  to {
    opacity: 0;
    transform: translateX(100%);
  }
}

.sheet::backdrop {
  background: hsla(0, 0%, 0%, 0.5);
}

.sheet {
  right: 0;
  margin-right: 0;
  height: 100vh;
  animation: sheetOpen 0.75s var(--modal-animation);
}

.sheet[data-closing] {
  animation: sheetClose 0.35s var(--modal-animation);
}

@keyframes bottomSheetOpen {
  from {
    opacity: 0;
    transform: translateY(100%);
  }
  to {
    opacity: 1;
    transform: translateY(0%);
  }
}

@keyframes bottomSheetClose {
  from {
    opacity: 1;
    transform: translateY(0%);
  }
  to {
    opacity: 0;
    transform: translateY(100%);
  }
}

.bottom-sheet {
  height: auto;
  top: unset;
  bottom: 0;
  margin-bottom: 0;
  animation: bottomSheetOpen 0.75s var(--modal-animation);
}

.bottom-sheet[data-closing] {
  animation: bottomSheetClose 0.35s var(--modal-animation);
}

.modal-transition {
  opacity: 0;
  transition: opacity 350ms ease;
}

.modal-transition[data-open] {
  opacity: 1;
}

Open on page load

There might come a time where a modal is needed open as soon as the page loads. Unfortunately, to create modals a client-side API is currently needed regardless of implementation.

Example CSS

The example for the CSS used in all of the examples above can be found below.

.modal,
.modal-trigger {
  border-radius: calc(var(--border-radius));
}

.modal-trigger {
  padding: 0.5rem;
  width: 100%;
  border: 2px dotted hsla(var(--foreground));
  height: 44px;
  width: fit-content;
}

.modal {
  padding: 1rem;
  gap: 0.5rem;
  border: 2px dotted hsla(var(--primary));
  color: hsla(var(--foreground));
  background-color: hsl(var(--background));
  max-width: 28rem;
  overflow: hidden;
}

.modal::backdrop {
  backdrop-filter: blur(3px) brightness(50%);
}

.modal > * {
  padding-block: 0.5rem;
}

.modal input {
  margin-bottom: 1rem;
  border-radius: calc(var(--border-radius));
  background: hsla(var(--foreground) / 0.05);
}

.modal h2 {
  font-size: 1.5rem;
  font-weight: 800;
  padding: 0;
}

.modal input {
  width: 100%;
  padding: 0.5rem;
  border-radius: calc(var(--border-radius));
  border: 2px dotted hsla(var(--ring));
}

.modal footer {
  padding-top: 0;
  display: flex;
  gap: 0.5rem;
}

.modal button {
  padding: 0.5rem;
  border-radius: calc(var(--border-radius));
  border: 2px dotted hsla(var(--muted));
}

.modal button:first-of-type {
  border-color: hsla(var(--foreground));
}

.modal button:last-of-type {
  border-color: hsla(var(--primary));
}

.modal-backdrop::backdrop {
  /* changing background */
  background: rgba(0, 0, 0, 0.4);

  /* providing multiple filters */
  backdrop-filter: grayscale(90%) blur(10px);
}

.modal .modal-close {
  position: absolute;
  top: 0;
  right: 0.5rem;
  transform: scale(3) rotate(45deg);
  border: none;
  outline: none;
}

/* some css fun here lol */
.modal .modal-close:focus-visible::after {
  content: '';
  position: absolute;
  left: 0.35rem;
  right: 0.55rem;
  top: 0.7rem;
  bottom: 0.7rem;
  border: 1px solid hsla(var(--foreground));
  transform: rotate(-45deg);
  pointer-events: none;
}

SSR dilemma

Qwik is the first framework with an SSR portal implementation. SSR Portal capabilities are unique to Qwik City, because the alternative frameworks require a client-side API for portal functionality, such as document.appendChild.

The problem with rendering the modal in the server, is that we lose some critical behavior:

  • The content behind the dialog is not inert (non-modal)
  • Focus and scroll lock customizations break
  • Will not work with meta frameworks like Astro.js

The current solution across framework ecosystems, is to open the Modal eagerly on the client to keep the proper functionality.

import { component$, useSignal } from '@builder.io/qwik';
import { Modal, ModalDescription, ModalTitle } from '@qwik-ui/headless';

export default component$(() => {
  const showSig = useSignal(false);

  // uncomment this
  // useVisibleTask$(
  //   () => {
  //     showSig.value = true;
  //   },
  //   { strategy: 'document-ready' },
  // );

  return (
    <Modal bind:show={showSig}>
      <ModalTitle />
      <ModalDescription />
      {/* other content */}
    </Modal>
  );
});

Keyboard interaction

Key

Description

Escape
Closes the dialog.
Tab
Moves focus to the next focusable item in the modal. If none, then loops back to the last item.
Shift + Tab
Moves focus to the previous focusable item in the modal. If none, then loops back to the last item.

API

PropTypeDescription
bind:show
Signal

Toggle between showing or hiding the modal.

closeOnBackdropClick
boolean

A way to tell the modal to not hide when the ::backdrop is clicked.

<dialog>-Attributes
Qwik

A way to configure the modal with all native attributes the HTMLDialog defines.

onClose()$
function

An event hook that gets notified whenever the modal gets closed.

onShow()$
function

An event hook that gets notified whenever the modal shows up.

data-open
attribute

A data attribute used for styling open modals and entry animations.

data-closing
attribute

A data attribute used for exit animations.

data-closed
attribute

A data attribute used for closed modals.