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 panel that appears above all other content, blocking interaction with the rest of the page.

Edit Profile

You can update your profile here. Hit the save button when finished.

✨ Features

  • Follows WAI-ARIA dialog and alertdialog guidelines.
  • Screen reader support with title and description.
  • Full keyboard navigation
  • Locks focus inside the modal
  • Prevents scrolling when open
  • Control the open state
  • Above other content
  • Custom backdrop dismiss

Building Blocks

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

export default component$(() => {
  return (
    <Modal.Root>
      <Modal.Trigger>Open Modal</Modal.Trigger>
      <Modal.Panel>
        <Modal.Title>Accessible Name</Modal.Title>
        <Modal.Description>Optional Description</Modal.Description>
        {/* other content */}
      </Modal.Panel>
    </Modal.Root>
  );
});

🎨 Anatomy

ComponentDescription
Modal.Root

The primary container for the modal.

Modal.Panel

The dialog element that is rendered on top of other content.

Modal.Trigger

A button that opens the modal when interacted with.

Modal.Title

An accessible name for the modal.

ModalDescription

An optional accessible description for the modal.

Modal.Close

A button that closes the modal when interacted with.

Why use a headless modal?

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.

Qwik UI builds on top of the dialog element, and solves the following issues:

Native dialog pain points

  • Unintuitive focus locking behavior
  • No scroll locking behavior
  • Unintuitive user agent styles
  • Confusing terminology (non-modal dialogs)

Dialog extendability

Edit Profile

You can update your profile here. Hit the save button when finished.

Above, we mentioned the Modal component uses the dialog element. This means you can also use the native dialog element attributes to customize the modal.

Attribute example

Edit Profile

You can update your profile here. Hit the save button when finished.

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

Programmatic behavior

To programmatically open or close the modal, we can use bind:show, a signal prop that allows us to reactively control the modal state.

Edit Profile

You can update your profile here. Hit the save button when finished.

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

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 both the Modal and Popover components of Qwik UI.

Backdrops

To add a backdrop, the ::backdrop pseudo element can be used. 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);
}

Edit Profile

You can update your profile here. Hit the save button when finished.

Dismissing Modals via Backdrop

Edit Profile

You can update your profile here. Hit the save button when finished.

By default, modals are dismissed when clicking the backdrop. To disable this behavior, set disableOnBackdropClick={true} on the <Modal.Root /> component.

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 cannot be dismissed by clicking the backdrop, regardless of whether the disableOnBackdropClick prop was set to true.

Focus Locking

Focus locking prevents focus from leaving the modal.

Edit Profile

You can update your profile here. Hit the save button when finished.

Using the Tab key to navigate through the modal will cycle the focus from the last to the first focusable element automatically.

Stripped Styles

As a headless library, Qwik UI avoids adding styles to components. However, it uses native solutions where appropriate, which may include browser default styles.

These default styles can be difficult to manage, so they have been removed from the Modal component to simplify debugging.

@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

Edit Profile

You can update your profile here. Hit the save button when finished.

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

Edit Profile

You can update your profile here. Hit the save button when finished.

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

Edit Profile

You can update your profile here. Hit the save button when finished.

Bottom Sheet

Edit Profile

You can update your profile here. Hit the save button when finished.

Bottom sheets are more prevalent in mobile applications, usually to simplify UI.

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;
}

Stacked Modals

Although this is not always a recommended pattern, modals can be stacked. Focus will be trapped within the last opened modal.

Open stacked Modal

You can open a Modal on top of another Modal.

I am a stacked Modal

You cannot interact with the other modal until you close me.

Example CSS

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

.modal-panel,
.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-trigger:hover,
.modal-close:hover {
  background-color: hsla(var(--primary) / 0.08);
}

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

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

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

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

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

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

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

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

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

.modal-panel 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-panel .modal-alert-close {
  position: absolute;
  top: 0;
  right: 0.5rem;
  transform: scale(3) rotate(45deg);
  border: none;
  outline: none;
}

/* some css fun here lol */
.modal-panel .modal-alert-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

Unfortunately, to create modals that open on page load in the web, it needs to be done eagerly on the client. (including existing implementations)

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

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

  return (
    <Modal.Root>
      <Modal.Trigger>Open Modal</Modal.Trigger>
      <Modal.Panel>
        <Modal.Title />
        <Modal.Description />
        {/* other content */}
      </Modal.Panel>
    </Modal.Root>
  );
});

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

Modal.Root

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.

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.