Building a Custom Slide-Out Drawer with Accessibility and Smooth Animations

Introduction

Slide-out menus, also known as drawers, are a staple of modern user interfaces. They provide an elegant way to present additional content or actions without overwhelming the main screen. In this article, we’ll explore the creation of a custom slide-out drawer component in React, focusing on accessibility, animations, and ease of use.

This custom drawer offers features like focus trapping, scroll locking, keyboard accessibility, and smooth spring-based animations using popular libraries like Framer Motion and react-focus-lock.

Features

1. Smooth Animations with Framer Motion

The drawer uses Framer Motion for its open and close animations. The spring-based transitions provide a polished and fluid experience for users:

  • Initial Position: The drawer slides in from the right (x: "100%").
  • Open Position: Animates smoothly to x: 0.
  • Close Animation: Slides back to x: "100%" when dismissed.

2. Focus Management

Accessibility is a key consideration. Using react-focus-lock, the drawer traps focus within its content when open, ensuring that:

  • Keyboard navigation stays confined to the drawer.
  • Focus returns to the previously focused element when the drawer is closed.

3. Scroll Locking

To prevent background scrolling when the drawer is open, the component integrates react-remove-scroll. This ensures:

  • The content behind the drawer remains fixed.
  • The user experience feels more intuitive and controlled.

4. Escape Key Support

Keyboard users can close the drawer by pressing the Escape key, thanks to a custom useEscapeKey hook. This feature enhances usability and adheres to accessibility best practices.

5. Overlay Click-to-Close

An overlay behind the drawer provides an easy way for users to dismiss it. Clicking anywhere on the overlay triggers the onOpenChange callback, closing the drawer.

Code Overview

Here’s a high-level look at the Drawer component:

import React from "react";
import styles from "./Drawer.module.css";
import FocusLock from "react-focus-lock";
import { RemoveScroll } from "react-remove-scroll";
import { useEscapeKey } from "../../hooks/useEscapeKey";
import { AnimatePresence, motion } from "framer-motion";

function Drawer({
  children,
  onOpenChange,
  isOpen,
}: {
  children: React.ReactNode;
  isOpen: boolean;
  onOpenChange: (isOpen: boolean) => void;
}) {
  useEscapeKey(() => {
    onOpenChange(false);
  });

  return (
    <>
      {isOpen && (
        <div
          className={styles.overlay}
          onClick={() => {
            onOpenChange(false);
          }}
        ></div>
      )}
      <RemoveScroll enabled={isOpen} forwardProps>
        <AnimatePresence>
          {isOpen ? (
            <div className={styles.drawerContainer}>
              <FocusLock returnFocus>
                <motion.div
                  className={styles.drawer}
                  initial={{ x: "100%" }}
                  animate={{ x: 0 }}
                  transition={{
                    duration: 0.2,
                    type: "spring",
                    stiffness: 200,
                    damping: 25,
                  }}
                  exit={{ x: "100%" }}
                >
                  <div>{children}</div>
                  <button
                    onClick={() => {
                      onOpenChange(false);
                    }}
                  >
                    Close
                  </button>
                </motion.div>
              </FocusLock>
            </div>
          ) : null}
        </AnimatePresence>
      </RemoveScroll>
    </>
  );
}

export default Drawer;

How It Works

1. Opening and Closing

The drawer’s visibility is controlled by the isOpen prop, with onOpenChange toggling the state. Clicking the overlay or the close button sets isOpen to false.

2. Focus Management

Using react-focus-lock, all focusable elements outside the drawer are locked, ensuring that the user cannot accidentally navigate away.

3. Animation Logic

Framer Motion powers the animations:

  • When the drawer opens, it animates from x: "100%" to x: 0.
  • On close, it slides back to x: "100%".

4. Scroll Locking

react-remove-scroll ensures that the background content does not scroll when the drawer is open, enhancing the user experience.

5. Escape Key Support

The useEscapeKey hook listens for the Escape key and triggers the close logic.

Example Usage

Here’s an example of how to use the Drawer component in your application:

import React, { useState } from 'react'
import Drawer from './components/Drawer'

function App() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open Drawer</button>
      <Drawer isOpen={isOpen} onOpenChange={setIsOpen}>
        <div>
          <h2>Slide-Out Menu</h2>
          <p>Here’s some content inside the drawer!</p>
        </div>
      </Drawer>
    </div>
  )
}

export default App

Accessibility Features

1. Focus Trapping

Users navigating with a keyboard will have their focus locked within the drawer when it’s open. This ensures a smooth and accessible experience.

2. Dismiss with Escape Key

Pressing the Escape key provides a quick way to dismiss the drawer, enhancing usability for power users.

3. ARIA Roles and Labels

You can extend the component to include ARIA attributes like role="dialog" and aria-labelledby for better screen reader support.

Challenges and Solutions

Challenge: Managing Focus and Scroll Locking

Solution: By integrating react-focus-lock and react-remove-scroll, the component ensures a seamless experience for both keyboard and mouse users.

Challenge: Smooth Animations

Solution: Framer Motion’s spring transition provides natural and fluid animations.

Demo and Repository

Want to see the slide-out drawer in action? Check out the live demo or explore the code on GitHub.

Conclusion

This custom Slide-Out Drawer strikes a balance between simplicity and usability. With smooth animations, accessibility features, and easy integration, it’s an excellent addition to any modern web application. Whether you’re building a settings menu, a side navigation bar, or a custom modal, this component is both versatile and developer-friendly.

Ready to integrate it into your project? Check out the demo or dive into the code to get started! 🎉