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.
<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.
<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.
<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.
<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"
.
Settings
This content was loaded only when you opened the slideover. This is useful for performance when you have heavy content like:
Notifications
Privacy
Appearance
<!-- 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 %>"
.
<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