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:
npx vui-registry-cli-v1 add crystal-sheetSource Code
'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: latestlucide-react: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| trigger | ReactNode | null | Element that opens the sheet. |
| content | ReactNode | null | Content displayed inside the sheet. |
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.

