Drawer

Component

Slide-out glassmorphism panels for dense interfaces, accessible natively with swipe-to-close physics constraints.

Install via CLI

Run this command to automatically add the component and its dependencies to your project.

npx @abhaysinghr516/business-wish add drawer
New to the CLI? Run npx @abhaysinghr516/business-wish init first to initialize your project.

Implement highly effective Off-Canvas Drawers (or Bottom Sheets) that dramatically conserve on-page real estate. Drawers uniquely intercept gestures natively—relying on Framer Motion's drag physics to allow users to literally swipe them closed out of frame precisely.

Perfect for mobile navigation, advanced filter menus, or quick action settings.

Bottom Sheet Drawer

Bottom drawer featuring a drag handle, backdrop blur, and swipe-down-to-close physics constraint limits.

"use client";

import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { X } from "lucide-react";

const BottomDrawer: React.FC = () => {
  const [isOpen, setIsOpen] = useState(false);

  useEffect(() => {
    if (isOpen) {
      document.body.style.overflow = "hidden";
    } else {
      document.body.style.overflow = "unset";
    }
  }, [isOpen]);

  return (
    <>
      <button
        onClick={() => setIsOpen(true)}
        className="px-5 py-2.5 bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 font-medium rounded-full"
      >
        Open Bottom Sheet
      </button>

      <AnimatePresence>
        {isOpen && (
          <>
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              onClick={() => setIsOpen(false)}
              className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50 cursor-pointer"
            />

            <motion.div
              initial={{ y: "100%" }}
              animate={{ y: 0 }}
              exit={{ y: "100%" }}
              transition={{ type: "spring", damping: 25, stiffness: 200 }}
              drag="y"
              dragConstraints={{ top: 0 }}
              dragElastic={0.2}
              onDragEnd={(e, { offset, velocity }) => {
                if (offset.y > 100 || velocity.y > 500) setIsOpen(false);
              }}
              className="fixed bottom-0 left-0 right-0 max-h-[85vh] h-[400px] bg-white dark:bg-neutral-900 rounded-t-[2.5rem] p-6 z-50 shadow-2xl flex flex-col"
            >
              <div className="w-12 h-1.5 bg-neutral-200 dark:bg-neutral-700 rounded-full mx-auto mb-6 shrink-0" />
              
              <div className="flex justify-between items-center mb-4 shrink-0">
                <h2 className="text-xl font-bold dark:text-white">Settings</h2>
                <button onClick={() => setIsOpen(false)} className="p-2 bg-neutral-100 dark:bg-neutral-800 rounded-full">
                  <X size={18} className="dark:text-white" />
                </button>
              </div>

              <div className="flex-1 overflow-y-auto">
                <p className="text-neutral-500 mb-6">Swipe down on this drawer to close it natively!</p>
              </div>
            </motion.div>
          </>
        )}
      </AnimatePresence>
    </>
  );
};

export default BottomDrawer;

Side Menu Drawer

Sliding off-canvas menu from the left edge. Great for global navigation replacements.

"use client";

import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { X } from "lucide-react";

const LeftDrawer: React.FC = () => {
  const [isOpen, setIsOpen] = useState(false);

  useEffect(() => {
    if (isOpen) {
      document.body.style.overflow = "hidden";
    } else {
      document.body.style.overflow = "unset";
    }
  }, [isOpen]);

  return (
    <>
      <button
        onClick={() => setIsOpen(true)}
        className="px-5 py-2.5 border border-neutral-200 dark:border-neutral-700 dark:text-white font-medium rounded-full"
      >
        Open Side Drawer
      </button>

      <AnimatePresence>
        {isOpen && (
          <>
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              onClick={() => setIsOpen(false)}
              className="fixed inset-0 bg-black/60 z-50 cursor-pointer"
            />

            <motion.div
              initial={{ x: "-100%" }}
              animate={{ x: 0 }}
              exit={{ x: "-100%" }}
              transition={{ type: "spring", damping: 25, stiffness: 200 }}
              drag="x"
              dragConstraints={{ right: 0 }}
              dragElastic={0.05}
              onDragEnd={(e, { offset, velocity }) => {
                if (offset.x < -100 || velocity.x < -500) setIsOpen(false);
              }}
              className="fixed top-0 left-0 bottom-0 w-[300px] max-w-[80vw] bg-white dark:bg-neutral-900 z-50 shadow-2xl flex flex-col pt-12 p-6"
            >
              <div className="flex justify-between items-center mb-8 shrink-0">
                <h2 className="text-xl font-bold dark:text-white">Menu</h2>
                <button onClick={() => setIsOpen(false)}>
                  <X size={24} className="text-neutral-400" />
                </button>
              </div>
            </motion.div>
          </>
        )}
      </AnimatePresence>
    </>
  );
};

export default LeftDrawer;