Slide-Out Panel

A full-height modal variant that slides in from the right side of the screen. Extends modal behavior with slide animation, responsive sizing, and structured layout.

View DESIGN.md View Lookbook

Examples

Third Screen (33% on desktop, 50% on tablet)

Half Screen (50% on tablet+) — Default

Full Screen (100% on all viewports)

HTML Structure

Basic Panel Template

<!-- Trigger Button -->
<button
  data-panel="unique-panel-id"
  role="button"
  aria-haspopup="dialog"
  aria-expanded="false"
  aria-controls="unique-panel-id">
  Open Panel
</button>

<!-- Panel Dialog -->
<dialog
  id="unique-panel-id"
  class="elv-slide-out-panel elv-slide-out-panel--half"
  aria-modal="true"
  role="dialog">

  <div class="elv-slide-out-panel__inner">

    <!-- Header -->
    <div class="elv-slide-out-panel__header">
      <div class="elv-slide-out-panel__header-top">
        <div>
          <h2 class="elv-slide-out-panel__title">Panel Title</h2>
          <p class="elv-slide-out-panel__subtitle">Optional subtitle or description</p>
        </div>
        <button
          class="elv-slide-out-panel__close"
          aria-label="Close panel">
          <span class="material-symbols-outlined">close</span>
        </button>
      </div>
    </div>

    <!-- Body -->
    <div class="elv-slide-out-panel__body">
      <p>Panel content goes here. This area is scrollable when content overflows.</p>
    </div>

    <!-- Footer (optional) -->
    <div class="elv-slide-out-panel__footer">
      <button class="btn btn--secondary">Cancel</button>
      <button class="btn btn--primary">Save Changes</button>
    </div>

  </div>
</dialog>

Size Variants

<!-- Third Screen: 33% on desktop, 50% on tablet, 100% on mobile -->
<dialog class="elv-slide-out-panel elv-slide-out-panel--third">...</dialog>

<!-- Half Screen (default): 50% on tablet+, 100% on mobile -->
<dialog class="elv-slide-out-panel elv-slide-out-panel--half">...</dialog>

<!-- Full Screen: 100% on all viewports -->
<dialog class="elv-slide-out-panel elv-slide-out-panel--full">...</dialog>

JavaScript Controller

Vanilla JavaScript implementation with focus trap, focus restoration, and accessibility features.

/**
 * SlideOutPanel Controller
 *
 * Features:
 * - Native <dialog> element with showModal() API
 * - Slide-in animation from right (500ms ease-in-out)
 * - Focus trap (tab cycles within panel)
 * - Focus restoration to trigger element
 * - Click outside to close
 * - ESC key to close (native)
 * - Body scroll lock when open
 * - ARIA state management
 */
class SlideOutPanel {
  constructor(dialogElement) {
    this.dialog = dialogElement;
    this.triggerElement = null;
    this.focusableElements = [];
    // Check if backdrop click should close panel (default: true)
    this.dismissOnBackdropClick = this.dialog.getAttribute('data-dismiss-on-backdrop-click') !== 'false';

    this.init();
  }

  init() {
    // Add body scroll lock class (activates when any dialog[open] exists)
    if (!document.body.classList.contains('elv-has-dialog-scroll-lock')) {
      document.body.classList.add('elv-has-dialog-scroll-lock');
    }

    // Bind event listeners
    this.dialog.addEventListener('click', this.handleBackdropClick.bind(this));
    this.dialog.addEventListener('keydown', this.handleKeyDown.bind(this));

    // Find and setup close buttons
    const closeButtons = this.dialog.querySelectorAll('.elv-slide-out-panel__close');
    closeButtons.forEach(btn => {
      btn.addEventListener('click', () => this.close());
    });
  }

  open(triggerElement) {
    // Store trigger for focus restoration
    this.triggerElement = triggerElement;

    // Update trigger ARIA state
    if (this.triggerElement) {
      this.triggerElement.setAttribute('aria-expanded', 'true');
    }

    // Close any other open dialogs
    document.querySelectorAll('dialog[open]').forEach(d => {
      if (d !== this.dialog) {
        d.close();
      }
    });

    // Open the dialog
    this.dialog.showModal();

    // Setup focus trap
    this.setupFocusTrap();

    // Focus first element
    requestAnimationFrame(() => {
      this.focusFirstElement();
    });
  }

  close() {
    // Close the dialog
    this.dialog.close();

    // Update trigger ARIA state
    if (this.triggerElement) {
      this.triggerElement.setAttribute('aria-expanded', 'false');
      this.triggerElement.focus();
    }
  }

  setupFocusTrap() {
    // Get all focusable elements within dialog
    const focusableSelector =
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
    this.focusableElements = Array.from(
      this.dialog.querySelectorAll(focusableSelector)
    );
  }

  focusFirstElement() {
    if (this.focusableElements.length > 0) {
      this.focusableElements[0].focus();
    }
  }

  handleBackdropClick(event) {
    // Close if click is directly on dialog element (backdrop), not on content
    // Only if dismissOnBackdropClick is enabled
    if (event.target === this.dialog && this.dismissOnBackdropClick) {
      this.close();
    }
  }

  handleKeyDown(event) {
    // Focus trap: cycle focus within panel
    if (event.key === 'Tab' && this.focusableElements.length > 0) {
      const firstElement = this.focusableElements[0];
      const lastElement = this.focusableElements[this.focusableElements.length - 1];

      if (event.shiftKey) {
        // Shift+Tab: if on first element, jump to last
        if (document.activeElement === firstElement) {
          event.preventDefault();
          lastElement.focus();
        }
      } else {
        // Tab: if on last element, jump to first
        if (document.activeElement === lastElement) {
          event.preventDefault();
          firstElement.focus();
        }
      }
    }
  }
}

// Initialize all panels on page load
document.addEventListener('DOMContentLoaded', () => {
  // Create panel instances
  const panels = new Map();

  document.querySelectorAll('.elv-slide-out-panel').forEach(dialog => {
    panels.set(dialog.id, new SlideOutPanel(dialog));
  });

  // Setup trigger buttons
  document.querySelectorAll('[data-panel]').forEach(trigger => {
    trigger.addEventListener('click', (e) => {
      const panelId = trigger.getAttribute('data-panel');
      const panel = panels.get(panelId);
      if (panel) {
        panel.open(trigger);
      }
    });

    // Keyboard support for triggers
    trigger.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        trigger.click();
      }
    });
  });
});

Usage Notes

Accessibility Requirements

Behavior Specifications

Configuration Options

Responsive Sizing

Design Specifications

Browser Support

Uses native <dialog> element (Chrome 37+, Firefox 98+, Safari 15.4+) and CSS :has() selector (Chrome 105+, Firefox 121+, Safari 15.4+). For older browsers, consider a polyfill or fallback implementation.

Third Screen Panel

33% on desktop, 50% on tablet, 100% on mobile

This panel uses the elv-slide-out-panel--third size variant.

It adapts to screen size:

  • Desktop (1024px+): 33.33% width
  • Tablet (768px-1023px): 50% width
  • Mobile (<768px): 100% width

Perfect for lightweight contextual information, compact forms, or quick actions.

Try it: Resize your browser window to see the responsive behavior. The panel maintains full height but adjusts width based on viewport size.

Half Screen Panel (Default)

50% on tablet+, 100% on mobile

This is the default panel size using elv-slide-out-panel--half.

When to use Half Screen:

  • Standard forms and data entry
  • Content editing workflows
  • Multi-field forms
  • Detail views with moderate content

Scrollable Content

The body section is scrollable when content exceeds the viewport height. The header and footer remain fixed while the body scrolls.

Scroll down to see more content...

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Keep scrolling...

Almost there...

You've reached the bottom of this demo content!

Notice how the header and footer stayed in place while you scrolled.

Full Screen Panel

100% width on all viewports

This panel uses elv-slide-out-panel--full to occupy the entire viewport width.

When to use Full Screen:

  • Complex multi-step workflows
  • Rich content editing (WYSIWYG editors, etc.)
  • Data-heavy interfaces (tables, charts)
  • Immersive experiences requiring maximum space

Key Features

Slide Animation

Smoothly slides in from the right edge over 500ms with ease-in-out easing.

Focus Trap

Tab key cycles through focusable elements within the panel. Try it by pressing Tab!

Keyboard Navigation

Press ESC to close. Focus automatically returns to the trigger button.

Click Outside to Close

Clicking the semi-transparent backdrop closes the panel.