Bottom Modal

PreviousNext

Cute bottom-centered modal with smooth slide-up animation and glassmorphism design

Docs
uitripledcomponent

Preview

Loading preview…
components/modals/bottom-modal.tsx
"use client";

import { Button } from "@/components/ui/button";
import { AnimatePresence, motion } from "framer-motion";
import { Sparkles, X } from "lucide-react";
import { useState } from "react";

export function BottomModal() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    <div className="text-center space-y-8 w-full mx-auto">
      <motion.div
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.4, delay: 0.1 }}
      >
        <Button
          onClick={() => setIsModalOpen(true)}
          size="lg"
          className="gap-2 w-full rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 shadow-lg hover:shadow-xl transition-all duration-300"
          aria-label="Open bottom modal"
        >
          <Sparkles className="h-5 w-5" aria-hidden="true" />
          Open Bottom Modal
        </Button>
      </motion.div>

      <AnimatePresence>
        {isModalOpen && (
          <>
            {/* Backdrop */}
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              transition={{ duration: 0.2 }}
              className="fixed inset-0 bg-background/80 backdrop-blur-sm z-40 h-full"
              onClick={() => setIsModalOpen(false)}
              aria-hidden="true"
            />

            {/* Modal */}
            <motion.div
              initial={{ y: "100%", opacity: 0 }}
              animate={{ y: 0, opacity: 1 }}
              exit={{ y: "100%", opacity: 0 }}
              transition={{
                type: "spring",
                damping: 25,
                stiffness: 300,
              }}
              className="fixed bottom-0 right-0 left-0 mx-auto w-full md:max-w-md z-50"
              role="dialog"
              aria-modal="true"
              aria-labelledby="modal-title"
              aria-describedby="modal-description"
            >
              <div className="group relative overflow-hidden rounded-tl-2xl rounded-tr-2xl border border-border/40 bg-background/60 backdrop-blur shadow-lg">
                {/* Gradient overlay */}
                <div className="absolute inset-0 bg-gradient-to-br from-foreground/[0.04] via-transparent to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 -z-10" />

                {/* Header */}
                <div className="relative flex items-center justify-between p-4 border-b border-border/40">
                  <h3
                    id="modal-title"
                    className="text-lg font-semibold text-foreground"
                  >
                    Hello
                  </h3>
                  <button
                    onClick={() => setIsModalOpen(false)}
                    className="p-2 rounded-lg border border-border/40 bg-background/60 backdrop-blur hover:border-border/60 hover:bg-background/70 transition-all duration-200"
                    aria-label="Close modal"
                  >
                    <X className="h-5 w-5 text-foreground/70 hover:text-foreground" />
                  </button>
                </div>

                {/* Content */}
                <div className="relative p-6 space-y-4">
                  <motion.div
                    initial={{ opacity: 0, y: 10 }}
                    animate={{ opacity: 1, y: 0 }}
                    transition={{ delay: 0.1, duration: 0.3 }}
                    className="text-center space-y-2"
                  >
                    <h4 className="text-xl font-semibold text-foreground">
                      Welcome to the Modal!
                    </h4>
                    <p
                      id="modal-description"
                      className="text-sm text-foreground/70"
                    >
                      This is a cute bottom-centered modal with smooth
                      animations powered by Framer Motion.
                    </p>
                  </motion.div>

                  <motion.div
                    initial={{ opacity: 0, y: 10 }}
                    animate={{ opacity: 1, y: 0 }}
                    transition={{ delay: 0.2, duration: 0.3 }}
                    className="flex gap-3"
                  >
                    <Button
                      variant="outline"
                      className="flex-1 rounded-lg border-border/40 bg-background/40 hover:bg-background/60"
                      onClick={() => setIsModalOpen(false)}
                    >
                      Cancel
                    </Button>
                    <Button
                      className="flex-1 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90"
                      onClick={() => setIsModalOpen(false)}
                    >
                      Got it!
                    </Button>
                  </motion.div>
                </div>
              </div>
            </motion.div>
          </>
        )}
      </AnimatePresence>
    </div>
  );
}

Installation

npx shadcn@latest add @uitripled/bottom-modal

Usage

import { BottomModal } from "@/components/bottom-modal"
<BottomModal />