Slideover Components

Slide-in panels that overlay content from the edge of the screen. Perfect for navigation menus, forms, filters, and contextual information.

Installation

1. Stimulus Controller Setup

Start by adding the following controller to your project:

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["dialog", "template"];
  static values = {
    open: { type: Boolean, default: false }, // Whether the slideover is open
    lazyLoad: { type: Boolean, default: false }, // Whether to lazy load the slideover content
    turboFrameSrc: { type: String, default: "" }, // URL for the turbo frame
  };

  connect() {
    if (this.openValue) this.open();
    this.boundBeforeCache = this.beforeCache;
    document.addEventListener("turbo:before-cache", this.boundBeforeCache);
    this.contentLoaded = false;

    // Add event listener for when dialog is closed by any means (including Escape key)
    this.dialogTarget.addEventListener("close", this.handleDialogClose.bind(this));
  }

  disconnect() {
    document.removeEventListener("turbo:before-cache", this.boundBeforeCache);
    this.dialogTarget.removeEventListener("close", this.handleDialogClose.bind(this));
  }

  async open() {
    // If lazy loading is enabled and content hasn't been loaded yet, load it now
    if (this.lazyLoadValue && !this.contentLoaded) {
      await this.#loadTemplateContent();
      this.contentLoaded = true;
    }

    // Calculate and apply scrollbar compensation
    const scrollbarWidth = this.getScrollbarWidth();
    if (scrollbarWidth > 0) {
      document.body.style.paddingRight = `${scrollbarWidth}px`;
    }

    this.dialogTarget.showModal();
  }

  close() {
    this.dialogTarget.setAttribute("closing", "");

    Promise.all(this.dialogTarget.getAnimations().map((animation) => animation.finished)).then(() => {
      this.dialogTarget.removeAttribute("closing");
      this.dialogTarget.close();

      // Remove scrollbar compensation
      document.body.style.paddingRight = "";
    });
  }

  backdropClose(event) {
    if (event.target.nodeName == "DIALOG") this.close();
  }

  show() {
    this.open();
  }

  hide(event) {
    if (event) event.preventDefault();
    this.close();
  }

  beforeCache() {
    this.close();
  }

  // Calculate actual scrollbar width
  getScrollbarWidth() {
    // Create a temporary div to measure scrollbar width
    const outer = document.createElement("div");
    outer.style.visibility = "hidden";
    outer.style.overflow = "scroll";
    outer.style.msOverflowStyle = "scrollbar"; // Force scrollbars on IE/Edge
    document.body.appendChild(outer);

    const inner = document.createElement("div");
    outer.appendChild(inner);

    const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
    outer.parentNode.removeChild(outer);

    return scrollbarWidth;
  }

  async #loadTemplateContent() {
    // Find the container in the dialog to append content to
    const container = this.dialogTarget.querySelector("[data-slideover-content]") || this.dialogTarget;

    // Check if we should use Turbo Frame lazy loading
    if (this.turboFrameSrcValue) {
      // Look for a turbo-frame in the container
      let turboFrame = container.querySelector("turbo-frame");

      if (!turboFrame) {
        // Create a turbo-frame if it doesn't exist
        turboFrame = document.createElement("turbo-frame");
        turboFrame.id = "slideover-lazy-content";

        // Clear any loading indicators or placeholder content
        container.innerHTML = "";
        container.appendChild(turboFrame);
      }

      // Set the src to trigger the lazy load
      turboFrame.src = this.turboFrameSrcValue;

      // Wait for the turbo-frame to load
      return new Promise((resolve) => {
        const handleLoad = () => {
          turboFrame.removeEventListener("turbo:frame-load", handleLoad);
          resolve();
        };

        turboFrame.addEventListener("turbo:frame-load", handleLoad);

        // Fallback timeout in case the frame doesn't load
        setTimeout(() => {
          turboFrame.removeEventListener("turbo:frame-load", handleLoad);
          resolve();
        }, 5000);
      });
    } else if (this.hasTemplateTarget) {
      // Use template-based lazy loading
      const templateContent = this.templateTarget.content.cloneNode(true);

      // Clear any loading indicators or placeholder content
      container.innerHTML = "";

      // Append the template content
      container.appendChild(templateContent);
    }
  }

  // Handle cleanup when dialog is closed by any means
  handleDialogClose() {
    // Remove scrollbar compensation
    document.body.style.paddingRight = "";

    // Ensure the closing attribute is removed
    this.dialogTarget.removeAttribute("closing");
  }
}

2. Custom CSS

Here are the custom CSS classes that we used on Rails Blocks to style the slideover components. You can copy and paste these into your own CSS file to style & personalize your slideovers.

/* Dialog */

/* Firefox has a bug with backdrop, so we can use a box-shadow instead */
dialog.modal {
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.5);
}

dialog.slideover {
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.5);
}

dialog.max-w-full {
  box-shadow: none;
}

dialog::backdrop {
  background: none;
}

/* Modal animations */
dialog.modal:not(.max-w-full)[open] {
  animation: fadeIn 200ms forwards, scaleIn 200ms forwards;
}

dialog.modal:not(.max-w-full)[closing] {
  animation: fadeOut 200ms forwards, scaleOut 200ms forwards;
}

/* Fullscreen modal animations - fade only */
dialog.modal.max-w-full[open] {
  animation: fadeIn 200ms forwards;
}

dialog.modal.max-w-full[closing] {
  animation: fadeOut 200ms forwards;
}

/* Center modals */
dialog.modal {
  margin: auto;
  position: fixed;
  inset: 0;
  align-items: center;
  justify-content: center;
}

/* Slideover animations */
dialog.slideover[open] {
  animation: fadeIn 200ms forwards ease-in-out, slide-in-from-right 200ms forwards ease-in-out;
}

dialog.slideover[closing] {
  pointer-events: none;
  animation: fadeOut 200ms forwards ease-in-out, slide-out-to-right 200ms forwards ease-in-out;
}

/* Slideover animations for top */
dialog.slideover-top[open] {
  animation: fadeIn 200ms forwards ease-in-out, slide-in-from-top 200ms forwards ease-in-out;
}

dialog.slideover-top[closing] {
  animation: fadeOut 200ms forwards ease-in-out, slide-out-to-top 200ms forwards ease-in-out;
}

/* Slideover animations for bottom */
dialog.slideover-bottom[open] {
  animation: fadeIn 200ms forwards ease-in-out, slide-in-from-bottom 200ms forwards ease-in-out;
}

dialog.slideover-bottom[closing] {
  animation: fadeOut 200ms forwards ease-in-out, slide-out-to-bottom 200ms forwards ease-in-out;
}

/* Slideover animations for left */
dialog.slideover-left[open] {
  animation: fadeIn 200ms forwards ease-in-out, slide-in-from-left 200ms forwards ease-in-out;
}

dialog.slideover-left[closing] {
  animation: fadeOut 200ms forwards ease-in-out, slide-out-to-left 200ms forwards ease-in-out;
}

/* Slideover animations for right */
dialog.slideover-right[open] {
  animation: fadeIn 200ms forwards ease-in-out, slide-in-from-right 200ms forwards ease-in-out;
}

dialog.slideover-right[closing] {
  animation: fadeOut 200ms forwards ease-in-out, slide-out-to-right 200ms forwards ease-in-out;
}

body {
  scrollbar-gutter: stable;
  overflow-y: scroll;
}

/* Prevent scrolling while dialog is open */
body:has(dialog.modal[open]) {
  overflow: hidden;
}

body:has(dialog.slideover[open]) {
  overflow: hidden;
}

dialog.modal {
  cursor: auto;
}

/* Keyframes for fade animations */
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

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

/* Keyframes for new animations */
@keyframes slide-in-from-top {
  from {
    transform: translateY(-100%);
  }
  to {
    transform: translateY(0);
  }
}

@keyframes slide-out-to-top {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(-100%);
  }
}

@keyframes slide-in-from-bottom {
  from {
    transform: translateY(100%);
  }
  to {
    transform: translateY(0);
  }
}

@keyframes slide-out-to-bottom {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(100%);
  }
}

@keyframes slide-in-from-left {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes slide-out-to-left {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-100%);
  }
}

@keyframes slide-in-from-right {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes slide-out-to-right {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(100%);
  }
}

dialog[data-floating-select-target="menu"] {
  opacity: 0;
}

dialog[data-floating-select-target="menu"][open] {
  opacity: 1;
}

/* Add new keyframes for scale animations */
@keyframes scaleIn {
  from {
    transform: scale(0.95);
  }
  to {
    transform: scale(1);
  }
}

@keyframes scaleOut {
  from {
    transform: scale(1);
  }
  to {
    transform: scale(0.95);
  }
}

/* Add specific box-shadow handling for slideover directions */
dialog.slideover-top,
dialog.slideover-bottom {
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.6);
}

Examples

Right Slideover

A slideover panel that slides in from the right edge of the screen.

Right Slideover

This is a right slideover panel that slides in from the right edge of the screen.

Perfect for forms, details views, or secondary navigation.

<div data-controller="slideover">
  <button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200" data-action="slideover#show:prevent">Open Right Slideover</button>

  <dialog data-slideover-target="dialog" data-action="click->slideover#backdropClose" class="slideover slideover-right fixed inset-0 m-0 ml-auto h-dvh max-h-full border-l border-neutral-950/10 outline-none dark:border-white/10">
    <div class="inset-y-0 flex h-dvh w-80 flex-col bg-white shadow-lg outline-none sm:w-96 lg:w-[420px] dark:bg-neutral-800 dark:text-neutral-100 dark:shadow-neutral-950">
      <button type="button" data-action="slideover#hide:prevent" class="ring-offset-background absolute top-4 right-4 mt-1 rounded-full p-1.5 opacity-70 transition-opacity hover:cursor-pointer hover:bg-neutral-500/15 hover:opacity-100 focus:outline-none active:bg-neutral-500/25 disabled:pointer-events-none md:top-5">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
          <line x1="18" x2="6" y1="6" y2="18"></line>
          <line x1="6" x2="18" y1="6" y2="18"></line>
        </svg>
        <span class="sr-only">Close</span>
      </button>

      <h4 class="mt-0 border-b font-semibold border-neutral-100 p-5 pr-8 text-lg text-neutral-900 md:p-6 md:text-xl dark:border-white/5 dark:text-neutral-100">Right Slideover</h4>
      <div class="flex grow flex-col overflow-y-auto p-5 md:p-6">
        <div class="flex flex-col items-center justify-center gap-5 rounded-lg border-2 border-dashed border-neutral-200 p-5 dark:border-neutral-700/75">
          <p class="text-neutral-600 dark:text-neutral-400">This is a right slideover panel that slides in from the right edge of the screen.</p>
          <p class="text-sm text-neutral-500 dark:text-neutral-500">Perfect for forms, details views, or secondary navigation.</p>
        </div>
      </div>
      <div class="flex flex-none items-center justify-start gap-2 border-t border-neutral-100 p-5 md:p-6 md:px-7 dark:border-white/5">
        <button type="button" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200">Submit</button>
        <button data-action="slideover#hide:prevent" type="button" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200">Cancel</button>
      </div>
    </div>
  </dialog>
</div>

Left Slideover

A slideover panel that slides in from the left edge of the screen.

Left Slideover

This is a left slideover panel that slides in from the left edge of the screen.

Great for navigation menus, filters, or settings panels.

<div data-controller="slideover">
  <button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200" data-action="slideover#show:prevent">Open Left Slideover</button>

  <dialog data-slideover-target="dialog" data-action="click->slideover#backdropClose" class="slideover slideover-left fixed inset-0 m-0 mr-auto h-dvh max-h-full border-r border-neutral-950/10 outline-none dark:border-white/10">
    <div class="inset-y-0 flex h-dvh w-80 flex-col bg-white shadow-lg outline-none sm:w-96 lg:w-[420px] dark:bg-neutral-800 dark:text-neutral-100 dark:shadow-neutral-950">
      <button type="button" data-action="slideover#hide:prevent" class="ring-offset-background absolute top-4 right-4 mt-1 rounded-full p-1.5 opacity-70 transition-opacity hover:cursor-pointer hover:bg-neutral-500/15 hover:opacity-100 focus:outline-none active:bg-neutral-500/25 disabled:pointer-events-none md:top-5">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
          <line x1="18" x2="6" y1="6" y2="18"></line>
          <line x1="6" x2="18" y1="6" y2="18"></line>
        </svg>
        <span class="sr-only">Close</span>
      </button>

      <h4 class="mt-0 border-b font-semibold border-neutral-100 p-5 pr-8 text-lg text-neutral-900 md:p-6 md:text-xl dark:border-white/5 dark:text-neutral-100">Left Slideover</h4>
      <div class="flex grow flex-col overflow-y-auto p-5 md:p-6">
        <div class="flex flex-col items-center justify-center gap-5 rounded-lg border-2 border-dashed border-neutral-200 p-5 dark:border-neutral-700/75">
          <p class="text-neutral-600 dark:text-neutral-400">This is a left slideover panel that slides in from the left edge of the screen.</p>
          <p class="text-sm text-neutral-500 dark:text-neutral-500">Great for navigation menus, filters, or settings panels.</p>
        </div>
      </div>
      <div class="flex flex-none items-center justify-start gap-2 border-t border-neutral-100 p-5 md:p-6 md:px-7 dark:border-white/5">
        <button type="button" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200">Submit</button>
        <button data-action="slideover#hide:prevent" type="button" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200">Cancel</button>
      </div>
    </div>
  </dialog>
</div>

Top Slideover

A slideover panel that slides down from the top of the screen.

Top Slideover

This is a top slideover panel that slides down from the top of the screen.

Ideal for notifications, search interfaces, or quick actions.

<div data-controller="slideover">
  <button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200" data-action="slideover#show:prevent">Open Top Slideover</button>

  <dialog data-slideover-target="dialog" data-action="click->slideover#backdropClose" class="slideover slideover-top fixed inset-0 m-0 mb-auto w-full max-w-full border-b border-neutral-950/10 outline-none dark:border-white/10">
    <div class="inset-x-0 flex h-fit flex-col bg-white shadow-lg outline-none dark:bg-neutral-800 dark:text-neutral-100 dark:shadow-neutral-950">
      <button type="button" data-action="slideover#hide:prevent" class="ring-offset-background absolute top-4 right-4 mt-1 rounded-full p-1.5 opacity-70 transition-opacity hover:cursor-pointer hover:bg-neutral-500/15 hover:opacity-100 focus:outline-none active:bg-neutral-500/25 disabled:pointer-events-none md:top-5">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
          <line x1="18" x2="6" y1="6" y2="18"></line>
          <line x1="6" x2="18" y1="6" y2="18"></line>
        </svg>
        <span class="sr-only">Close</span>
      </button>

      <h4 class="mt-0 border-b font-semibold border-neutral-100 p-5 pr-8 text-lg text-neutral-900 md:p-6 md:text-xl dark:border-white/5 dark:text-neutral-100">Top Slideover</h4>
      <div class="flex grow flex-col overflow-y-auto p-5 md:p-6">
        <div class="flex flex-col items-center justify-center gap-5 rounded-lg border-2 border-dashed border-neutral-200 p-5 dark:border-neutral-700/75">
          <p class="text-neutral-600 dark:text-neutral-400">This is a top slideover panel that slides down from the top of the screen.</p>
          <p class="text-sm text-neutral-500 dark:text-neutral-500">Ideal for notifications, search interfaces, or quick actions.</p>
        </div>
      </div>
      <div class="flex flex-none items-center justify-start gap-2 border-t border-neutral-100 p-5 md:p-6 md:px-7 dark:border-white/5">
        <button type="button" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200">Submit</button>
        <button data-action="slideover#hide:prevent" type="button" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200">Cancel</button>
      </div>
    </div>
  </dialog>
</div>

Bottom Slideover

A slideover panel that slides up from the bottom of the screen.

Bottom Slideover

This is a bottom slideover panel that slides up from the bottom of the screen.

Perfect for mobile-style action sheets, product details, or contextual options.

<div data-controller="slideover">
  <button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200" data-action="slideover#show:prevent">Open Bottom Slideover</button>

  <dialog data-slideover-target="dialog" data-action="click->slideover#backdropClose" class="slideover slideover-bottom fixed inset-0 m-0 mt-auto w-full max-w-full border-t border-neutral-950/10 outline-none dark:border-white/10">
    <div class="inset-x-0 flex h-fit flex-col bg-white shadow-lg outline-none dark:bg-neutral-800 dark:text-neutral-100 dark:shadow-neutral-950">
      <button type="button" data-action="slideover#hide:prevent" class="ring-offset-background absolute top-4 right-4 mt-1 rounded-full p-1.5 opacity-70 transition-opacity hover:cursor-pointer hover:bg-neutral-500/15 hover:opacity-100 focus:outline-none active:bg-neutral-500/25 disabled:pointer-events-none md:top-5">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
          <line x1="18" x2="6" y1="6" y2="18"></line>
          <line x1="6" x2="18" y1="6" y2="18"></line>
        </svg>
        <span class="sr-only">Close</span>
      </button>

      <h4 class="mt-0 border-b font-semibold border-neutral-100 p-5 pr-8 text-lg text-neutral-900 md:p-6 md:text-xl dark:border-white/5 dark:text-neutral-100">Bottom Slideover</h4>
      <div class="flex grow flex-col overflow-y-auto p-5 md:p-6">
        <div class="flex flex-col items-center justify-center gap-5 rounded-lg border-2 border-dashed border-neutral-200 p-5 dark:border-neutral-700/75">
          <p class="text-neutral-600 dark:text-neutral-400">This is a bottom slideover panel that slides up from the bottom of the screen.</p>
          <p class="text-sm text-neutral-500 dark:text-neutral-500">Perfect for mobile-style action sheets, product details, or contextual options.</p>
        </div>
      </div>
      <div class="flex flex-none items-center justify-start gap-2 border-t border-neutral-100 p-5 md:p-6 md:px-7 dark:border-white/5">
        <button type="button" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200">Submit</button>
        <button data-action="slideover#hide:prevent" type="button" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200">Cancel</button>
      </div>
    </div>
  </dialog>
</div>

Lazy Loading Slideover

A slideover that loads its content only when opened, improving performance for heavy content. Uses data-slideover-lazy-load-value="true".

Loading settings...

<!-- Lazy loading slideover example -->
<div data-controller="slideover" data-slideover-lazy-load-value="true">
  <button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200" data-action="slideover#show:prevent">
    Lazy Loaded Slideover
  </button>

  <!-- Template that contains content to be loaded only when slideover is opened -->
  <template data-slideover-target="template">
    <div class="inset-y-0 flex h-dvh w-80 flex-col bg-white shadow-lg outline-none sm:w-96 lg:w-[420px] dark:bg-neutral-800 dark:text-neutral-100 dark:shadow-neutral-950">
      <button type="button" data-action="slideover#hide:prevent" class="ring-offset-background absolute top-4 right-4 mt-1 rounded-full p-1.5 opacity-70 transition-opacity hover:cursor-pointer hover:bg-neutral-500/15 hover:opacity-100 focus:outline-none active:bg-neutral-500/25 disabled:pointer-events-none md:top-5">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
          <line x1="18" x2="6" y1="6" y2="18"></line>
          <line x1="6" x2="18" y1="6" y2="18"></line>
        </svg>
        <span class="sr-only">Close</span>
      </button>

      <h4 class="mt-0 border-b font-semibold border-neutral-100 p-5 pr-8 text-lg text-neutral-900 md:p-6 md:text-xl dark:border-white/5 dark:text-neutral-100">Settings</h4>

      <div class="flex grow flex-col overflow-y-auto p-5 md:p-6">
        <p class="mb-4 text-sm text-neutral-600 dark:text-neutral-400">
          This content was loaded only when you opened the slideover. This is useful for performance when you have heavy content like:
        </p>

        <div class="space-y-6">
          <div>
            <h5 class="font-medium mb-3 text-neutral-900 dark:text-white">Notifications</h5>
            <div class="space-y-3">
              <label class="group flex items-center cursor-pointer justify-between w-full">
                <span class="text-sm text-neutral-700 dark:text-neutral-300">Email notifications</span>
                <div class="relative">
                  <input checked type="checkbox" class="sr-only peer" name="tailwind_switch" id="tailwind_switch">
                  <!-- Background element -->
                  <div class="w-10 h-6 bg-neutral-200 border border-black/10 rounded-full transition-all duration-150 ease-in-out cursor-pointer group-hover:bg-[#dcdcdb] peer-checked:bg-[#404040] peer-checked:group-hover:bg-neutral-600 peer-checked:border-white/10 dark:bg-neutral-700 dark:border-white/10 dark:group-hover:bg-neutral-600 dark:peer-checked:bg-neutral-50 dark:peer-checked:group-hover:bg-neutral-100 dark:peer-checked:border-black/20 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-neutral-600 dark:peer-focus-visible:outline-neutral-200"></div>
                  <!-- Round element with icons inside -->
                  <div class="absolute top-[3px] left-[3px] w-[18px] h-[18px] bg-white rounded-full shadow-sm transition-all duration-150 ease-in-out peer-checked:translate-x-4 flex items-center justify-center dark:bg-neutral-200 dark:peer-checked:bg-neutral-800">
                    <!-- X icon for unchecked state -->
                    <svg class="w-3 h-3 text-neutral-400 transition-all duration-150 ease-in-out group-has-[:checked]:opacity-0 dark:text-neutral-500" fill="none" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                      <path d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                    <!-- Checkmark icon for checked state -->
                    <svg class="absolute w-3 h-3 text-neutral-700 transition-all duration-150 ease-in-out opacity-0 group-has-[:checked]:opacity-100 dark:text-neutral-100" fill="currentColor" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                      <path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" fill="currentColor"/>
                    </svg>
                  </div>
                </div>
              </label>
              <label class="group flex items-center cursor-pointer justify-between w-full">
                <span class="text-sm text-neutral-700 dark:text-neutral-300">Push notifications</span>
                <div class="relative">
                  <input type="checkbox" class="sr-only peer" name="tailwind_switch" id="tailwind_switch">
                  <!-- Background element -->
                  <div class="w-10 h-6 bg-neutral-200 border border-black/10 rounded-full transition-all duration-150 ease-in-out cursor-pointer group-hover:bg-[#dcdcdb] peer-checked:bg-[#404040] peer-checked:group-hover:bg-neutral-600 peer-checked:border-white/10 dark:bg-neutral-700 dark:border-white/10 dark:group-hover:bg-neutral-600 dark:peer-checked:bg-neutral-50 dark:peer-checked:group-hover:bg-neutral-100 dark:peer-checked:border-black/20 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-neutral-600 dark:peer-focus-visible:outline-neutral-200"></div>
                  <!-- Round element with icons inside -->
                  <div class="absolute top-[3px] left-[3px] w-[18px] h-[18px] bg-white rounded-full shadow-sm transition-all duration-150 ease-in-out peer-checked:translate-x-4 flex items-center justify-center dark:bg-neutral-200 dark:peer-checked:bg-neutral-800">
                    <!-- X icon for unchecked state -->
                    <svg class="w-3 h-3 text-neutral-400 transition-all duration-150 ease-in-out group-has-[:checked]:opacity-0 dark:text-neutral-500" fill="none" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                      <path d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                    <!-- Checkmark icon for checked state -->
                    <svg class="absolute w-3 h-3 text-neutral-700 transition-all duration-150 ease-in-out opacity-0 group-has-[:checked]:opacity-100 dark:text-neutral-100" fill="currentColor" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                      <path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" fill="currentColor"/>
                    </svg>
                  </div>
                </div>
              </label>
              <label class="group flex items-center cursor-pointer justify-between w-full">
                <span class="text-sm text-neutral-700 dark:text-neutral-300">SMS notifications</span>
                <div class="relative">
                  <input type="checkbox" class="sr-only peer" name="tailwind_switch" id="tailwind_switch">
                  <!-- Background element -->
                  <div class="w-10 h-6 bg-neutral-200 border border-black/10 rounded-full transition-all duration-150 ease-in-out cursor-pointer group-hover:bg-[#dcdcdb] peer-checked:bg-[#404040] peer-checked:group-hover:bg-neutral-600 peer-checked:border-white/10 dark:bg-neutral-700 dark:border-white/10 dark:group-hover:bg-neutral-600 dark:peer-checked:bg-neutral-50 dark:peer-checked:group-hover:bg-neutral-100 dark:peer-checked:border-black/20 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-neutral-600 dark:peer-focus-visible:outline-neutral-200"></div>
                  <!-- Round element with icons inside -->
                  <div class="absolute top-[3px] left-[3px] w-[18px] h-[18px] bg-white rounded-full shadow-sm transition-all duration-150 ease-in-out peer-checked:translate-x-4 flex items-center justify-center dark:bg-neutral-200 dark:peer-checked:bg-neutral-800">
                    <!-- X icon for unchecked state -->
                    <svg class="w-3 h-3 text-neutral-400 transition-all duration-150 ease-in-out group-has-[:checked]:opacity-0 dark:text-neutral-500" fill="none" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                      <path d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                    <!-- Checkmark icon for checked state -->
                    <svg class="absolute w-3 h-3 text-neutral-700 transition-all duration-150 ease-in-out opacity-0 group-has-[:checked]:opacity-100 dark:text-neutral-100" fill="currentColor" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                      <path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" fill="currentColor"/>
                    </svg>
                  </div>
                </div>
              </label>
            </div>
          </div>

          <div>
            <h5 class="font-medium mb-3 text-neutral-900 dark:text-white">Privacy</h5>
            <div class="space-y-3">
              <label class="group flex items-center cursor-pointer justify-between w-full">
                <span class="text-sm text-neutral-700 dark:text-neutral-300">Make profile public</span>
                <div class="relative">
                  <input checked type="checkbox" class="sr-only peer" name="tailwind_switch" id="tailwind_switch">
                  <!-- Background element -->
                  <div class="w-10 h-6 bg-neutral-200 border border-black/10 rounded-full transition-all duration-150 ease-in-out cursor-pointer group-hover:bg-[#dcdcdb] peer-checked:bg-[#404040] peer-checked:group-hover:bg-neutral-600 peer-checked:border-white/10 dark:bg-neutral-700 dark:border-white/10 dark:group-hover:bg-neutral-600 dark:peer-checked:bg-neutral-50 dark:peer-checked:group-hover:bg-neutral-100 dark:peer-checked:border-black/20 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-neutral-600 dark:peer-focus-visible:outline-neutral-200"></div>
                  <!-- Round element with icons inside -->
                  <div class="absolute top-[3px] left-[3px] w-[18px] h-[18px] bg-white rounded-full shadow-sm transition-all duration-150 ease-in-out peer-checked:translate-x-4 flex items-center justify-center dark:bg-neutral-200 dark:peer-checked:bg-neutral-800">
                    <!-- X icon for unchecked state -->
                    <svg class="w-3 h-3 text-neutral-400 transition-all duration-150 ease-in-out group-has-[:checked]:opacity-0 dark:text-neutral-500" fill="none" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                      <path d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                    <!-- Checkmark icon for checked state -->
                    <svg class="absolute w-3 h-3 text-neutral-700 transition-all duration-150 ease-in-out opacity-0 group-has-[:checked]:opacity-100 dark:text-neutral-100" fill="currentColor" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                      <path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" fill="currentColor"/>
                    </svg>
                  </div>
                </div>
              </label>
              <label class="group flex items-center cursor-pointer justify-between w-full">
                <span class="text-sm text-neutral-700 dark:text-neutral-300">Show online status</span>
                <div class="relative">
                  <input checked type="checkbox" class="sr-only peer" name="tailwind_switch" id="tailwind_switch">
                  <!-- Background element -->
                  <div class="w-10 h-6 bg-neutral-200 border border-black/10 rounded-full transition-all duration-150 ease-in-out cursor-pointer group-hover:bg-[#dcdcdb] peer-checked:bg-[#404040] peer-checked:group-hover:bg-neutral-600 peer-checked:border-white/10 dark:bg-neutral-700 dark:border-white/10 dark:group-hover:bg-neutral-600 dark:peer-checked:bg-neutral-50 dark:peer-checked:group-hover:bg-neutral-100 dark:peer-checked:border-black/20 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-neutral-600 dark:peer-focus-visible:outline-neutral-200"></div>
                  <!-- Round element with icons inside -->
                  <div class="absolute top-[3px] left-[3px] w-[18px] h-[18px] bg-white rounded-full shadow-sm transition-all duration-150 ease-in-out peer-checked:translate-x-4 flex items-center justify-center dark:bg-neutral-200 dark:peer-checked:bg-neutral-800">
                    <!-- X icon for unchecked state -->
                    <svg class="w-3 h-3 text-neutral-400 transition-all duration-150 ease-in-out group-has-[:checked]:opacity-0 dark:text-neutral-500" fill="none" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                      <path d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                    <!-- Checkmark icon for checked state -->
                    <svg class="absolute w-3 h-3 text-neutral-700 transition-all duration-150 ease-in-out opacity-0 group-has-[:checked]:opacity-100 dark:text-neutral-100" fill="currentColor" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                      <path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" fill="currentColor"/>
                    </svg>
                  </div>
                </div>
              </label>
              <label class="group flex items-center cursor-pointer justify-between w-full">
                <span class="text-sm text-neutral-700 dark:text-neutral-300">Allow friend requests</span>
                <div class="relative">
                  <input checked type="checkbox" class="sr-only peer" name="tailwind_switch" id="tailwind_switch">
                  <!-- Background element -->
                  <div class="w-10 h-6 bg-neutral-200 border border-black/10 rounded-full transition-all duration-150 ease-in-out cursor-pointer group-hover:bg-[#dcdcdb] peer-checked:bg-[#404040] peer-checked:group-hover:bg-neutral-600 peer-checked:border-white/10 dark:bg-neutral-700 dark:border-white/10 dark:group-hover:bg-neutral-600 dark:peer-checked:bg-neutral-50 dark:peer-checked:group-hover:bg-neutral-100 dark:peer-checked:border-black/20 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-neutral-600 dark:peer-focus-visible:outline-neutral-200"></div>
                  <!-- Round element with icons inside -->
                  <div class="absolute top-[3px] left-[3px] w-[18px] h-[18px] bg-white rounded-full shadow-sm transition-all duration-150 ease-in-out peer-checked:translate-x-4 flex items-center justify-center dark:bg-neutral-200 dark:peer-checked:bg-neutral-800">
                    <!-- X icon for unchecked state -->
                    <svg class="w-3 h-3 text-neutral-400 transition-all duration-150 ease-in-out group-has-[:checked]:opacity-0 dark:text-neutral-500" fill="none" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                      <path d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                    <!-- Checkmark icon for checked state -->
                    <svg class="absolute w-3 h-3 text-neutral-700 transition-all duration-150 ease-in-out opacity-0 group-has-[:checked]:opacity-100 dark:text-neutral-100" fill="currentColor" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                      <path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" fill="currentColor"/>
                    </svg>
                  </div>
                </div>
              </label>
            </div>
          </div>

          <div>
            <h5 class="font-medium mb-3 text-neutral-900 dark:text-white">Appearance</h5>
            <div class="space-y-3">
              <div>
                <label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Theme</label>
                <select class="w-full rounded-md border-neutral-300 dark:border-neutral-600 dark:bg-neutral-700">
                  <option>System</option>
                  <option>Light</option>
                  <option>Dark</option>
                </select>
              </div>
              <div>
                <label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Language</label>
                <select class="w-full rounded-md border-neutral-300 dark:border-neutral-600 dark:bg-neutral-700">
                  <option>English</option>
                  <option>Spanish</option>
                  <option>French</option>
                  <option>German</option>
                </select>
              </div>
            </div>
          </div>
        </div>
      </div>

      <div class="flex flex-none items-center justify-start gap-2 border-t border-neutral-100 p-5 md:p-6 md:px-7 dark:border-white/5">
        <button type="button" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200">
          Save Changes
        </button>
        <button data-action="slideover#hide:prevent" type="button" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200">
          Cancel
        </button>
      </div>
    </div>
  </template>

  <!-- The dialog element that will be populated from the template -->
  <dialog data-slideover-target="dialog" data-action="click->slideover#backdropClose" class="slideover slideover-right fixed inset-0 m-0 ml-auto h-dvh max-h-full border-l border-neutral-950/10 outline-none dark:border-white/10">
    <!-- Content placeholder that will be replaced by template contents -->
    <div data-slideover-content>
      <div class="inset-y-0 flex h-dvh w-80 flex-col bg-white shadow-lg outline-none sm:w-96 lg:w-[420px] dark:bg-neutral-800 dark:text-neutral-100 dark:shadow-neutral-950">
        <div class="flex items-center justify-center h-full">
          <div class="text-center">
            <svg class="animate-spin h-8 w-8 mx-auto mb-4 text-neutral-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
              <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
              <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
            </svg>
            <p class="text-neutral-600 dark:text-neutral-400">Loading settings...</p>
          </div>
        </div>
      </div>
    </div>
  </dialog>
</div>

Turbo Slideover

A modal that loads content from the server using Turbo Frames. Uses data-modal-lazy-load-value="true" & data-modal-turbo-frame-src-value="<%= modal_content_path %>".

Loading from server...

<div data-controller="slideover" data-slideover-turbo-frame-src-value="<%= slideover_content_path %>" data-slideover-lazy-load-value="true">
  <button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200" data-action="slideover#show:prevent">
    Shopping Cart
  </button>

  <!-- Dialog to be populated via Turbo Frame -->
  <dialog data-slideover-target="dialog" data-action="click->slideover#backdropClose" class="slideover slideover-right fixed inset-0 m-0 ml-auto h-dvh max-h-full border-l border-neutral-950/10 outline-none dark:border-white/10">
    <div data-slideover-content>
      <div class="inset-y-0 flex h-dvh w-80 flex-col bg-white shadow-lg outline-none sm:w-96 lg:w-[420px] dark:bg-neutral-800 dark:text-neutral-100 dark:shadow-neutral-950">
        <div class="flex items-center justify-center h-full">
          <div class="text-center">
            <svg class="animate-spin h-8 w-8 mx-auto mb-4 text-neutral-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
              <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
              <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
            </svg>
            <p class="text-neutral-600 dark:text-neutral-400">Loading from server...</p>
          </div>
        </div>
      </div>
    </div>
  </dialog>
</div>
# Slideover content route for Turbo Frame lazy loading
get "/slideover_content", to: "pages#slideover_content", as: "slideover_content"
# Slideover content action for Turbo Frame lazy loading
def slideover_content
  render partial: "components/slideover/slideover_content", layout: false
end

Configuration

The slideover component is powered by a Stimulus controller that provides smooth animations, backdrop click handling, and Turbo integration.

Controller Setup

Basic slideover structure with required data attributes:

<div data-controller="slideover">
  <button data-action="slideover#show:prevent">Open Slideover</button>

  <dialog data-slideover-target="dialog" data-action="click->slideover#backdropClose" class="slideover slideover-right">
    <div class="slideover-content">
      <button data-action="slideover#hide:prevent">Close</button>
      <!-- Content -->
    </div>
  </dialog>
</div>

Lazy Loading

Enable lazy loading to improve performance by loading content only when the slideover is opened:

Template-based Lazy Loading

<div data-controller="slideover"
     data-slideover-lazy-load-value="true">
  <button data-action="slideover#show:prevent">Open Slideover</button>

  <!-- Template with content to load -->
  <template data-slideover-target="template">
    <div class="slideover-content">
      <!-- Your heavy content here -->
    </div>
  </template>

  <dialog data-slideover-target="dialog" class="slideover slideover-right">
    <div data-slideover-content>
      <!-- Loading placeholder -->
      <div class="p-8 text-center">Loading...</div>
    </div>
  </dialog>
</div>

Turbo Frame Lazy Loading

<div data-controller="slideover"
     data-slideover-lazy-load-value="true"
     data-slideover-turbo-frame-src-value="/slideover/content">
  <button data-action="slideover#show:prevent">Open Slideover</button>

  <dialog data-slideover-target="dialog" class="slideover slideover-right">
    <div data-slideover-content>
      <!-- Turbo frame will be created automatically -->
      <div class="p-8 text-center">Loading from server...</div>
    </div>
  </dialog>
</div>

Configuration Values

Prop Description Type Default
open
Controls whether the slideover is open on initial load Boolean false
lazyLoad
Whether to load slideover content only when opened Boolean false
turboFrameSrc
URL for Turbo Frame lazy loading String ""

Targets

Target Description Required
dialog
The dialog element that contains the slideover panel Required
template
Template element for lazy loading content Optional

Actions

Action Description Usage
show
Opens the slideover panel data-action="slideover#show:prevent"
hide
Closes the slideover panel with animations data-action="slideover#hide:prevent"
backdropClose
Closes the slideover when clicking outside the panel data-action="click->slideover#backdropClose"
open
Alias for show method data-action="slideover#open:prevent"
close
Alias for hide method data-action="slideover#close:prevent"

CSS Classes

Position the slideover panel using these CSS classes on the dialog element:

Direction Class Description
Right (Default) slideover-right Slides in from the right edge of the screen
Left slideover-left Slides in from the left edge of the screen
Top slideover-top Slides down from the top of the screen
Bottom slideover-bottom Slides up from the bottom of the screen

Key Features

  • Native Dialog Element: Uses the HTML dialog element for better accessibility and browser support
  • Smooth Animations: CSS-based slide and fade animations with closing state handling
  • Backdrop Click: Closes when clicking outside the panel content
  • Turbo Integration: Automatically closes before Turbo caches the page
  • Body Scroll Lock: Prevents body scrolling when slideover is open
  • Lazy Loading: Load content only when slideover is opened for better performance
  • Turbo Frame Support: Load slideover content from server using Turbo Frames

Accessibility Features

  • Focus Management: Uses native dialog focus trapping
  • Screen Reader Support: Proper ARIA attributes with close button labels
  • Keyboard Navigation: ESC key closes the slideover (native dialog behavior)

Usage Notes

  • The dialog element must have both slideover and a direction class (e.g., slideover-right)
  • Use :prevent modifier on actions to prevent default link/button behavior
  • The controller handles animation cleanup to ensure smooth closing transitions
  • Content inside the dialog should be wrapped in a container div for proper styling

Table of contents