Modals

Modals are overlay dialogs that require user interaction before returning to the main content. They focus attention on critical information, decisions, or forms.


Installation

Copy the modal CSS from styles/docs.css or include the Standard stylesheet:

<link rel="stylesheet" href="standard.min.css">

Basic modal structure:

<div class="Modal-overlay">
  <div class="Modal">
    <header class="Modal-header">
      <h2 class="Modal-title">Modal Title</h2>
      <button class="Button Button--icon Button--ghost" aria-label="Close">
        <i class="ph ph-x"></i>
      </button>
    </header>
    <div class="Modal-body">
      <p>Modal content goes here.</p>
    </div>
    <footer class="Modal-footer">
      <button class="Button Button--tertiary">Cancel</button>
      <button class="Button Button--primary">Confirm</button>
    </footer>
  </div>
</div>

Usage

Modals use a two-part structure: .Modal-overlay for the backdrop and .Modal for the dialog itself. Add .active or .Modal-overlay--active to show the modal.


Examples

Basic

A simple modal with header, body, and footer actions.

Confirmation

Use danger buttons for destructive actions. Clear messaging helps users understand consequences.

Form Modal

Modals work great for focused form inputs. Keep forms short — complex flows belong on dedicated pages.

Fullscreen

Fullscreen modals take over the entire viewport. Use for immersive experiences or complex multi-step flows.

Slideout / Drawer

Slide-in panels from the side work well for settings, filters, or detailed information.

Nested

Open a modal from within another modal. The second modal uses .Modal-overlay--nested for higher z-index.

Image Lightbox

Display images in a focused, centered view with minimal chrome.

Mountain landscape

Video Modal

Embed video content with playback controls.


Sizes

Modals come in three sizes: small, medium (default), and large. Fullscreen is also available.


Common Patterns

Confirmation Before Delete

Success Feedback Modal


Customization

Override modal styles using CSS custom properties:

/* Custom modal width */
.Modal--custom {
  max-width: 600px;
}

/* Custom overlay color */
.Modal-overlay--branded {
  background-color: oklch(25% 0.05 250 / 0.8);
}

/* Remove backdrop blur for performance */
.Modal-overlay--no-blur {
  backdrop-filter: none;
}

/* Slide-in from side animation */
.Modal-overlay--slideout .Modal {
  position: fixed;
  right: 0;
  top: 0;
  bottom: 0;
  max-width: 400px;
  height: 100%;
  border-radius: 0;
  transform: translateX(100%);
}

.Modal-overlay--slideout.active .Modal {
  transform: translateX(0);
}

/* Centered content variant */
.Modal--centered .Modal-header {
  text-align: center;
  justify-content: center;
}

.Modal--centered .Modal-body {
  text-align: center;
}

.Modal--centered .Modal-footer {
  justify-content: center;
}

API Reference

Overlay Classes

Class Description
.Modal-overlay Full-screen backdrop with centering
.Modal-overlay.active Shows the modal (visible, interactive)
.Modal-overlay--active Alternative active class
.Modal-overlay--nested Higher z-index for stacked modals
Class Description
.Modal Modal container (required)
.Modal--small Small modal (360px max-width)
.Modal--large Large modal (720px max-width)
.Modal--fullscreen Full viewport modal
.Modal--centered Center-aligned header and content

Structure Classes

Class Description
.Modal-header Header area with title and close button
.Modal-title Modal heading text
.Modal-body Main content area (scrollable)
.Modal-body--scrollable Explicit scrollable body
.Modal-footer Action buttons area

CSS Reference

/* Modal Overlay */
.Modal-overlay {
  position: fixed;
  inset: 0;
  background: oklch(0% 0 0 / 0.5);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s, visibility 0.2s;
}

.Modal-overlay.active,
.Modal-overlay--active {
  opacity: 1;
  visibility: visible;
}

.Modal-overlay--nested {
  z-index: 1001;
}

/* Modal Container */
.Modal {
  background: var(--bg);
  border: 1px solid var(--bd);
  border-radius: var(--r-l);
  box-shadow: 0 8px 32px oklch(0% 0 0 / 0.15);
  max-width: 480px;
  width: 100%;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  transform: translateY(-8px);
  transition: transform 0.2s;
}

.Modal-overlay.active .Modal {
  transform: translateY(0);
}

/* Sizes */
.Modal--small { max-width: 360px; }
.Modal--large { max-width: 720px; }
.Modal--fullscreen {
  max-width: 100%;
  max-height: 100%;
  height: 100vh;
  border-radius: 0;
}

/* Header */
.Modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--bd);
}

.Modal-title {
  font-size: 1.1rem;
  font-weight: 600;
  margin: 0;
}

/* Body */
.Modal-body {
  padding: var(--space-4) var(--space-5);
  flex: 1;
  overflow-y: auto;
}

.Modal-body p {
  margin: 0 0 var(--space-3);
  line-height: 1.5;
  color: var(--fg-3);
}

.Modal-body p:last-child {
  margin-bottom: 0;
}

/* Footer */
.Modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-top: 1px solid var(--bd);
}

/* Centered variant */
.Modal--centered .Modal-header { text-align: center; justify-content: center; }
.Modal--centered .Modal-body { text-align: center; }
.Modal--centered .Modal-footer { justify-content: center; }

Accessibility

Proper modal accessibility ensures all users can interact with your dialogs, including those using keyboards and screen readers.

Focus Management

<!-- Modal should trap focus when open -->
<div class="Modal-overlay active" role="dialog" aria-modal="true" aria-labelledby="modal-title">
  <div class="Modal">
    <header class="Modal-header">
      <h2 class="Modal-title" id="modal-title">Accessible Modal</h2>
      <button class="Button Button--icon Button--ghost" aria-label="Close modal">
        <i class="ph ph-x"></i>
      </button>
    </header>
    <div class="Modal-body">
      <p>First focusable element receives focus on open.</p>
      <input type="text" class="Input" placeholder="Focus starts here">
    </div>
    <footer class="Modal-footer">
      <button class="Button Button--tertiary">Cancel</button>
      <button class="Button Button--primary">Submit</button>
    </footer>
  </div>
</div>

Focus Trap

Keep focus within the modal while open. Tab cycles through focusable elements without escaping to background content.

// Basic focus trap implementation
function trapFocus(modal) {
  const focusable = modal.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  modal.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;

    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  });
}

Escape Key

Allow users to close the modal by pressing Escape.

function handleEscapeKey(modal, closeCallback) {
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && modal.classList.contains('active')) {
      closeCallback();
    }
  });
}

ARIA Attributes

Attribute Element Description
role="dialog" .Modal-overlay Identifies as a dialog
aria-modal="true" .Modal-overlay Indicates modal behavior
aria-labelledby .Modal-overlay Points to title ID
aria-describedby .Modal-overlay Points to description (optional)
aria-label Close button Describes the button action

Screen Reader Announcements

<!-- Use aria-live for dynamic content -->
<div class="Modal-body">
  <div aria-live="polite" aria-atomic="true">
    <!-- Dynamic messages announced to screen readers -->
    <p class="Alert Alert--success">Changes saved successfully!</p>
  </div>
</div>

Keyboard Support

Key Action
Tab Move focus to next focusable element
Shift + Tab Move focus to previous focusable element
Escape Close the modal
Enter Activate focused button

Return Focus

When the modal closes, return focus to the element that triggered it.

let triggerElement = null;

function openModal(modal, trigger) {
  triggerElement = trigger;
  modal.classList.add('active');
  modal.querySelector('input, button')?.focus();
}

function closeModal(modal) {
  modal.classList.remove('active');
  triggerElement?.focus();
  triggerElement = null;
}

Best Practices

Do

  • Use clear, specific titles — “Delete Project?” not “Confirm”
  • Keep content concise — Modals interrupt flow, respect users’ time
  • Provide obvious exit paths — Close button, Cancel action, Escape key
  • Trap focus — Keep keyboard navigation within the modal
  • Return focus — On close, focus returns to the trigger element
  • Prevent background scroll — Body shouldn’t scroll when modal is open
  • Match button order to reading flow — Primary action on the right
  • Use danger styling — Red buttons for destructive actions

Don’t

  • Stack too many modals — Two max; consider page navigation instead
  • Use for complex flows — Long forms belong on dedicated pages
  • Auto-open modals on page load — Let users initiate the interaction
  • Hide the close button — Users must always be able to dismiss
  • Put critical content in modals — Important info should be on the page
  • Use vague actions — “OK” and “Cancel” don’t describe what happens
  • Forget mobile — Ensure modals work on small screens
  • Block background interaction permanently — Always provide an exit