Multi-Direction Offcanvas Drawers with Smooth Sliding Animations

Multi-Direction Offcanvas Drawers with Smooth Sliding Animations
Multi-Direction Offcanvas Drawers with Smooth Sliding Animations
A lightweight CSS/JavaScript solution that enables you to create responsive offcanvas drawers with smooth slide animations.

It supports four directions (top, right, bottom, left) and helps you display content like navigation menus, sidebar panels or cookie consent banners compactly.

Features:

  • CSS keyframe-based transitions with easing controls
  • Responsive sizing via Tailwind utility classes
  • Overlay dimming effect with fade transitions
  • Built-in keyboard and screen reader accessibility
  • Customizable animation durations and delays

See
it in action:

How to use it:

1. Code the HTML structure for your panel elements. The script will use .panel class and the data-direction attribute to determine which animation to use. Here are examples for all four directions. Note that the styling uses TailwindCSS, but you can replace those with your own classes.

<!-- Top To Bottom -->
<div id="panel-top" class="panel" data-direction="top">
  <div class="panel-content max-w-2xl mx-auto text-center">
    ... Any Content Here ...
    <button class="close-btn bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded">Close</button>
  </div>
</div>

<!-- Right To Left -->
<div id="panel-right" class="panel" data-direction="right">
  <div class="panel-content flex flex-col h-full">
    ... Anyt Content Here ...
    <div class="mt-auto">
      <button class="close-btn w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-4 rounded">Close</button>
    </div>
  </div>
</div>

<!-- Bottom to Top -->
<div id="panel-bottom" class="panel bg-gray-800 text-white" data-direction="bottom">
  <div class="panel-content max-w-4xl mx-auto flex flex-col sm:flex-row items-center justify-between text-center sm:text-left">
    ... Any Content Here ...
    <button class="close-btn bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-6 rounded-lg flex-shrink-0">Close</button>
  </div>
</div>

<!-- Left to Right -->
<div id="panel-left" class="panel" data-direction="left">
  <div class="panel-content flex flex-col h-full">
    ... Any Content Here ...
    <div class="mt-auto">
      <button class="close-btn w-full bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-3 px-4 rounded">Close</button>
    </div>
  </div>
</div>

2. Create the buttons that will open each panel. The data-target-panel attribute must match the id of the panel you want to open.

<button data-target-panel="panel-top" class="bg-gradient-to-br from-purple-500 to-indigo-600 text-white font-bold py-3 px-6 rounded-lg shadow-lg transform hover:scale-105 transition-transform duration-200">
  From Top
</button>

<button data-target-panel="panel-right" class="bg-gradient-to-br from-green-500 to-teal-600 text-white font-bold py-3 px-6 rounded-lg shadow-lg transform hover:scale-105 transition-transform duration-200">
  From Right
</button>

<button data-target-panel="panel-bottom" class="bg-gradient-to-br from-orange-500 to-red-600 text-white font-bold py-3 px-6 rounded-lg shadow-lg transform hover:scale-105 transition-transform duration-200">
  From Bottom
</button>

<button data-target-panel="panel-left" class="bg-gradient-to-br from-blue-500 to-cyan-600 text-white font-bold py-3 px-6 rounded-lg shadow-lg transform hover:scale-105 transition-transform duration-200">
  From Left
</button>

3. Create an overlay element that dims the whole page when the panel is opened.

<div id="overlay" class="overlay"></div>
4. The necessary CSS/CSS3. It defines the slide-in and slide-out animations using @keyframes, styles the base panel and overlay, and uses helper classes to apply the animations.
  • Panel Animations: slideInFromRight, slideOutToRight (and variants for all directions)
  • Panel States: .panel.is-open for visibility control
  • Overlay States: .overlay.is-open for backdrop visibility
  • Content Animations: Staggered fade-in effects for panel children
/* --- Keyframe Animations for Panel Sliding --- */
@keyframes slideInFromRight {
  from {
    transform: translateX(100%);
  }

  to {
    transform: translateX(0);
  }
}

@keyframes slideOutToRight {
  from {
    transform: translateX(0);
  }

  to {
    transform: translateX(100%);
  }
}

@keyframes slideInFromLeft {
  from {
    transform: translateX(-100%);
  }

  to {
    transform: translateX(0);
  }
}

@keyframes slideOutToLeft {
  from {
    transform: translateX(0);
  }

  to {
    transform: translateX(-100%);
  }
}

@keyframes slideInFromTop {
  from {
    transform: translateY(-100%);
  }

  to {
    transform: translateY(0);
  }
}

@keyframes slideOutToTop {
  from {
    transform: translateY(0);
  }

  to {
    transform: translateY(-100%);
  }
}

@keyframes slideInFromBottom {
  from {
    transform: translateY(100%);
  }

  to {
    transform: translateY(0);
  }
}

@keyframes slideOutToBottom {
  from {
    transform: translateY(0);
  }

  to {
    transform: translateY(100%);
  }
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* --- Panel Base Styling --- */
.panel {
  position: fixed;
  background-color: white;
  box-shadow: 0 0 30px rgba(0, 0, 0, 0.2);
  z-index: 1000;
  visibility: hidden;
  /* Hidden by default */
}

.panel.is-open {
  visibility: visible;
}

.panel.is-open .panel-content>* {
  animation: fadeIn 0.5s both;
}

.panel.is-open .panel-content>*:nth-child(1) {
  animation-delay: 0.3s;
}

.panel.is-open .panel-content>*:nth-child(2) {
  animation-delay: 0.4s;
}

.panel.is-open .panel-content>*:nth-child(3) {
  animation-delay: 0.5s;
}

.panel.is-open .panel-content>*:nth-child(4) {
  animation-delay: 0.6s;
}

.panel.is-open .panel-content>*:nth-child(5) {
  animation-delay: 0.7s;
}

/* --- Overlay Styling --- */
.overlay {
  position: fixed;
  inset: 0;
  /* Shorthand for top: 0; right: 0; bottom: 0; left: 0; */
  background-color: rgba(0, 0, 0, 0.6);
  /* Semi-transparent black */
  z-index: 999;
  /* Below panels */
  opacity: 0;
  /* Hidden by default */
  visibility: hidden;
  /* Hidden by default */
  transition: opacity 0.4s ease-out, visibility 0.4s ease-out;
  /* Smooth transition */
}

.overlay.is-open {
  opacity: 1;
  visibility: visible;
}

/* --- Panel Animation Classes --- */
.panel.animate-in-right {
  animation: slideInFromRight 0.4s forwards ease-out;
}

.panel.animate-out-right {
  animation: slideOutToRight 0.3s forwards ease-in;
}

.panel.animate-in-left {
  animation: slideInFromLeft 0.4s forwards ease-out;
}

.panel.animate-out-left {
  animation: slideOutToLeft 0.3s forwards ease-in;
}

.panel.animate-in-top {
  animation: slideInFromTop 0.4s forwards ease-out;
}

.panel.animate-out-top {
  animation: slideOutToTop 0.3s forwards ease-in;
}

.panel.animate-in-bottom {
  animation: slideInFromBottom 0.4s forwards ease-out;
}

.panel.animate-out-bottom {
  animation: slideOutToBottom 0.3s forwards ease-in;
}

5. The script orchestrates everything. It listens for clicks on the trigger buttons and then applies or removes CSS classes to show and hide the panels.

document.addEventListener('DOMContentLoaded', () => {

  // Get references to the overlay element
  const overlay = document.getElementById('overlay');

  // Variables to keep track of the currently open panel and the element that had focus before opening the panel
  let openPanel = null;
  let lastFocusedElement = null;

  /**
   * Opens a given panel.
   * @param {HTMLElement} panel - The panel element to open.
   */
  const open = (panel) => {
    // If no panel is provided or a panel is already open, do nothing
    if (!panel || openPanel) return;
    // Get the direction for the animation from the panel's data attribute
    const direction = panel.dataset.direction;
    // Store the currently focused element to restore focus later
    lastFocusedElement = document.activeElement;
    // Reset panel classes and add 'is-open' and the appropriate 'animate-in' class
    panel.className = 'panel'; // Resets all classes except 'panel'
    panel.classList.add('is-open', `animate-in-${direction}`);
    // Apply specific Tailwind classes based on the panel's direction for positioning and sizing
    if (direction === 'right' || direction === 'left') {
      // For side panels: full height, responsive width, and padding
      panel.classList.add('top-0', 'h-full', 'w-11/12', 'sm:max-w-md', 'p-6', 'sm:p-8');
      panel.classList.add(direction === 'right' ? 'right-0' : 'left-0');
    } else {
      // For top/bottom panels: full width, auto height, and padding
      panel.classList.add('left-0', 'w-full', 'h-auto', 'p-6', 'sm:p-8');
      panel.classList.add(direction === 'bottom' ? 'bottom-0' : 'top-0');
    }
    // Show the overlay
    overlay.classList.add('is-open');
    // Set the currently open panel
    openPanel = panel;
    // Prevent body scrolling when a panel is open
    document.body.style.overflow = 'hidden';
    // Focus on the close button for accessibility
    panel.querySelector('.close-btn').focus();
  };

  /**
   * Closes the currently open panel.
   */
  const close = () => {
    // If no panel is open, do nothing
    if (!openPanel) return;
    // Get the direction for the animation
    const direction = openPanel.dataset.direction;
    // Remove the 'animate-in' class and add the appropriate 'animate-out' class
    openPanel.classList.remove(`animate-in-${direction}`);
    openPanel.classList.add(`animate-out-${direction}`);
    // Hide the overlay
    overlay.classList.remove('is-open');
    // Add an event listener to run code after the animation ends
    openPanel.addEventListener('animationend', () => {
      // Remove the 'is-open' and 'animate-out' classes to fully hide the panel
      openPanel.classList.remove('is-open', `animate-out-${direction}`);
      // Reset the open panel variable
      openPanel = null;
      // Restore body scrolling
      document.body.style.overflow = '';
    }, {
      once: true
    }); // The event listener will only run once
    // Restore focus to the element that was focused before the panel opened
    if (lastFocusedElement) lastFocusedElement.focus();
  };

  // --- Event Listeners ---
  // Attach click listeners to all buttons that trigger a panel
  document.querySelectorAll('[data-target-panel]').forEach(button => {
    button.addEventListener('click', e => {
      // Get the target panel's ID from the data attribute
      const panelId = e.currentTarget.dataset.targetPanel;
      // Find the panel element by its ID
      const panel = document.getElementById(panelId);
      // Open the found panel
      open(panel);
    });
  });
  // Attach click listeners to all close buttons within panels
  document.querySelectorAll('.close-btn').forEach(button => {
    button.addEventListener('click', close);
  });
  // Close the panel when the overlay is clicked
  overlay.addEventListener('click', close);
  // Close the panel when the Escape key is pressed
  document.addEventListener('keydown', e => {
    if (e.key === 'Escape' && openPanel) {
      close();
    }
  });
});

FAQs

Q: Can I use this without TailwindCSS?
A: Yes. The core logic only adds and removes classes like panel, is-open, and animate-in-right. The TailwindCSS classes in the HTML are just for styling the examples. You can replace them with your own CSS.

Q: How does it handle accessibility?
A: The script includes two important accessibility features. First, it traps focus: when a panel opens, it moves focus to the close button inside. When it closes, it returns focus to the element that originally opened it (lastFocusedElement). Second, it allows users to close the active panel by pressing the Escape key.

Q: Can I change the animation speed or easing?
A: Yes, you can adjust the duration and timing function directly in the CSS animation properties. For example, to make the right panel slide in faster, you would change .panel.animate-in-right { animation: slideInFromRight 0.2s forwards ease-out; }.

Q: Can I have multiple panels open simultaneously?
A: The current implementation allows only one panel open at a time through the openPanel variable check. To support multiple panels, remove the if (openPanel) return; check and modify the focus management logic.

Q: How do I handle panel content that exceeds viewport height?
A: Add overflow-y: auto to the .panel-content element for scrollable content within the panel. The library handles body scroll prevention automatically when panels are open.

The post Multi-Direction Offcanvas Drawers with Smooth Sliding Animations appeared first on CSS Script.


Discover more from RSS Feeds Cloud

Subscribe to get the latest posts sent to your email.

Discover more from RSS Feeds Cloud

Subscribe now to keep reading and get access to the full archive.

Continue reading