Velocity UI
Loading…
Menu

Crystal Sheet

A highly interactive bottom sheet with a glassmorphic backdrop and spring-loaded drag gestures. Perfect for mobile-first settings menus.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add crystal-sheet

Source Code

crystal-sheet.tsx
'use client'

import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { X, Settings, User, Bell, LogOut, ChevronRight, Moon, Sun, Shield, CreditCard, Check } from 'lucide-react'

export default function CrystalSheet() {
  const [isOpen, setIsOpen] = useState(false)
  const [darkMode, setDarkMode] = useState(false)
  const [notifications, setNotifications] = useState(true)
  
  // Animation Variants
  const sheetVariants = {
    hidden: { y: "100%", opacity: 0 },
    visible: { 
      y: 0, 
      opacity: 1,
      transition: { type: "spring", damping: 25, stiffness: 200 } 
    },
    exit: { y: "100%", opacity: 0 }
  }

  const listVariants = {
    visible: { transition: { staggerChildren: 0.05 } }
  }

  const itemVariants = {
    hidden: { opacity: 0, x: -10 },
    visible: { opacity: 1, x: 0 }
  }

  return (
    <div className={`relative w-full h-[700px] flex items-center justify-center transition-colors duration-500 ${darkMode ? 'bg-black' : 'bg-neutral-100'}`}>
      
      {/* Trigger Button */}
      <motion.button 
        whileHover={{ scale: 1.05 }}
        whileTap={{ scale: 0.95 }}
        onClick={() => setIsOpen(true)}
        className={`px-6 py-3 rounded-2xl font-medium shadow-xl flex items-center gap-2 border ${
          darkMode ? 'bg-neutral-900 border-white/10 text-white' : 'bg-white border-black/5 text-black'
        }`}
      >
        <Settings size={18} className={isOpen ? 'rotate-90' : 'rotate-0'} style={{ transition: '0.5s' }} />
        Open Settings
      </motion.button>

      <AnimatePresence>
        {isOpen && (
          <>
            {/* Backdrop */}
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              onClick={() => setIsOpen(false)}
              className="absolute inset-0 bg-black/40 backdrop-blur-md z-40"
            />
            
            {/* Compact Sheet */}
            <motion.div
              variants={sheetVariants}
              initial="hidden"
              animate="visible"
              exit="exit"
              className={`absolute bottom-6 w-[92%] max-w-sm overflow-hidden rounded-[32px] border shadow-2xl z-50 flex flex-col ${
                darkMode ? 'bg-neutral-900/95 border-white/10' : 'bg-white/95 border-black/5'
              }`}
            >
              {/* Glossy Overlay */}
              <div className="absolute inset-0 pointer-events-none bg-gradient-to-b from-white/5 to-transparent" />

              <div className="p-6 relative">
                {/* Header */}
                <div className="flex items-center justify-between mb-6">
                  <h2 className={`text-xl font-bold tracking-tight ${darkMode ? 'text-white' : 'text-neutral-900'}`}>Settings</h2>
                  <button onClick={() => setIsOpen(false)} className="p-2 opacity-50 hover:opacity-100 transition-opacity">
                    <X size={20} className={darkMode ? 'text-white' : 'text-black'} />
                  </button>
                </div>

                {/* Content */}
                <motion.div variants={listVariants} className="space-y-2">
                  
                  {/* Appearance Toggle */}
                  <motion.div variants={itemVariants} className={`flex items-center justify-between p-3 rounded-2xl border ${darkMode ? 'bg-white/5 border-white/5' : 'bg-black/5 border-black/5'}`}>
                    <div className="flex items-center gap-3">
                      <div className={`p-2 rounded-xl ${darkMode ? 'bg-purple-500/20 text-purple-400' : 'bg-purple-500/10 text-purple-600'}`}>
                        {darkMode ? <Moon size={18} /> : <Sun size={18} />}
                      </div>
                      <span className={`text-sm font-semibold ${darkMode ? 'text-white' : 'text-neutral-900'}`}>Appearance</span>
                    </div>
                    <button 
                      onClick={() => setDarkMode(!darkMode)}
                      className={`w-11 h-6 rounded-full p-1 transition-colors duration-300 ${darkMode ? 'bg-purple-600' : 'bg-neutral-300'}`}
                    >
                      <motion.div animate={{ x: darkMode ? 20 : 0 }} className="w-4 h-4 bg-white rounded-full shadow-md" />
                    </button>
                  </motion.div>

                  {/* Notification Toggle (Added Functionality) */}
                  <motion.div variants={itemVariants} className={`flex items-center justify-between p-3 rounded-2xl border ${darkMode ? 'bg-white/5 border-white/10' : 'bg-black/5 border-black/5'}`}>
                    <div className="flex items-center gap-3">
                      <div className={`p-2 rounded-xl ${darkMode ? 'bg-blue-500/20 text-blue-400' : 'bg-blue-500/10 text-blue-600'}`}>
                        <Bell size={18} />
                      </div>
                      <span className={`text-sm font-semibold ${darkMode ? 'text-white' : 'text-neutral-900'}`}>Notifications</span>
                    </div>
                    <button 
                      onClick={() => setNotifications(!notifications)}
                      className={`flex items-center justify-center w-6 h-6 rounded-lg border transition-all ${
                        notifications 
                        ? (darkMode ? 'bg-blue-500 border-blue-400 text-white' : 'bg-blue-600 border-blue-700 text-white')
                        : (darkMode ? 'bg-transparent border-white/20' : 'bg-transparent border-black/20')
                      }`}
                    >
                      {notifications && <Check size={14} strokeWidth={3} />}
                    </button>
                  </motion.div>

                  {/* Standard Menu Items */}
                  {[
                    { icon: User, label: 'Profile' },
                    { icon: Shield, label: 'Security' },
                    { icon: CreditCard, label: 'Billing' },
                  ].map((item, i) => (
                    <motion.div 
                      key={i} 
                      variants={itemVariants}
                      whileHover={{ scale: 1.02, x: 4 }}
                      className={`group flex items-center justify-between p-3 rounded-2xl cursor-pointer transition-all ${
                        darkMode ? 'hover:bg-white/5' : 'hover:bg-black/5'
                      }`}
                    >
                      <div className="flex items-center gap-3 text-neutral-500 group-hover:text-inherit transition-colors">
                        <item.icon size={18} />
                        <span className={`text-sm font-medium ${darkMode ? 'text-neutral-400 group-hover:text-white' : 'text-neutral-600 group-hover:text-black'}`}>{item.label}</span>
                      </div>
                      <ChevronRight size={14} className="opacity-0 group-hover:opacity-100 transition-opacity" />
                    </motion.div>
                  ))}
                </motion.div>

                {/* Footer Action */}
                <div className="mt-6 pt-4 border-t border-white/10">
                  <motion.button 
                    whileTap={{ scale: 0.97 }}
                    className={`w-full py-3 rounded-2xl font-bold text-sm transition-all flex items-center justify-center gap-2 ${
                      darkMode ? 'bg-red-500/10 text-red-400 hover:bg-red-500 hover:text-white' : 'bg-red-50 text-red-600 hover:bg-red-600 hover:text-white'
                    }`}
                  >
                    <LogOut size={16} />
                    Log Out
                  </motion.button>
                </div>
              </div>
            </motion.div>
          </>
        )}
      </AnimatePresence>
    </div>
  )
}

Dependencies

  • framer-motion: latest
  • lucide-react: latest

Props

Component property reference.

NameTypeDefaultDescription
triggerReactNodenullElement that opens the sheet.
contentReactNodenullContent displayed inside the sheet.
Context Worth Keeping In Orbit

Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.

I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.

I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.