Glass NotificationsSwipe-to-dismiss notification stack with glass cards and spring-animated layout transitions.
NotificationsGlassInteractive
Dark mode only
Refresh
Full screen
.png)
Notifications5
New Message
Alex sent you a photo
2m ago
New Like
Sarah liked your post
5m ago
Security
New login from MacBook Pro
12m ago
Update Available
Version 4.2 is ready to install
1h ago
Reminder
Team standup in 15 minutes
1h ago
'use client'
// npm install @phosphor-icons/react framer-motion
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Bell, ChatCircle, Heart, ShieldCheck, X, ArrowUp } from '@phosphor-icons/react'
interface Notification {
id: number
icon: typeof Bell
color: string
title: string
message: string
time: string
}
const INITIAL_NOTIFICATIONS: Notification[] = [
{
id: 1,
icon: ChatCircle,
color: '#3A86FF',
title: 'New Message',
message: 'Alex sent you a photo',
time: '2m ago',
},
{
id: 2,
icon: Heart,
color: '#FF7B54',
title: 'New Like',
message: 'Sarah liked your post',
time: '5m ago',
},
{
id: 3,
icon: ShieldCheck,
color: '#06D6A0',
title: 'Security',
message: 'New login from MacBook Pro',
time: '12m ago',
},
{
id: 4,
icon: ArrowUp,
color: '#B388FF',
title: 'Update Available',
message: 'Version 4.2 is ready to install',
time: '1h ago',
},
{
id: 5,
icon: Bell,
color: '#FFBE0B',
title: 'Reminder',
message: 'Team standup in 15 minutes',
time: '1h ago',
},
]
function NotificationCard({
notification,
onDismiss,
index,
}: {
notification: Notification
onDismiss: (id: number) => void
index: number
}) {
const Icon = notification.icon
return (
<motion.div
layout
initial={{ x: 60, scale: 0.9 }}
animate={{ x: 0, scale: 1, transition: { type: 'spring', stiffness: 280, damping: 24, delay: index * 0.05 } }}
exit={{ opacity: 0, x: -60, scale: 0.9, filter: 'blur(4px)', transition: { duration: 0.2, ease: 'easeIn' } }}
drag="x"
dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.3}
onDragEnd={(_, info) => {
if (Math.abs(info.offset.x) > 80) {
onDismiss(notification.id)
}
}}
className="group relative isolate w-full cursor-grab overflow-hidden rounded-2xl active:cursor-grabbing transition-colors duration-200"
style={{
background: 'rgba(255, 255, 255, 0.06)',
border: '1px solid rgba(255, 255, 255, 0.08)',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.06)',
}}
whileHover={{ backgroundColor: 'rgba(255, 255, 255, 0.1)' }}
>
{/* Blur layer — non-animating, isolated from drag frames */}
<div
className="pointer-events-none absolute inset-0 z-[-1] rounded-2xl"
style={{ backdropFilter: 'blur(20px) saturate(1.6)', WebkitBackdropFilter: 'blur(20px) saturate(1.6)' }}
/>
<div className="flex items-start gap-3.5 px-4 py-3.5 pr-12">
{/* Icon */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 400, damping: 18, delay: 0.1 + index * 0.05 }}
className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl"
style={{
background: `${notification.color}18`,
border: `1px solid ${notification.color}22`,
}}
>
<Icon size={18} weight="regular" style={{ color: notification.color }} />
</motion.div>
{/* Content */}
<div className="min-w-0 flex-1">
<h4 className="text-sm font-semibold text-white/85">{notification.title}</h4>
<p className="mt-0.5 text-[13px] text-white/40">{notification.message}</p>
</div>
</div>
{/* Dismiss + time — positioned top-right */}
<div className="absolute right-3 top-3 flex flex-col items-end gap-1.5">
<motion.button
whileHover={{ scale: 1.2, backgroundColor: 'rgba(255,255,255,0.15)' }}
whileTap={{ scale: 0.85 }}
onClick={() => onDismiss(notification.id)}
className="flex h-5 w-5 cursor-pointer items-center justify-center rounded-full"
style={{
background: 'rgba(255,255,255,0.06)',
}}
>
<X size={11} weight="regular" className="text-white/30" />
</motion.button>
<span className="text-[10px] text-white/25">{notification.time}</span>
</div>
{/* Bottom accent line */}
<div
className="absolute bottom-0 left-4 right-4 h-[1px]"
style={{
background: `linear-gradient(90deg, transparent, ${notification.color}22, transparent)`,
}}
/>
</motion.div>
)
}
export default function GlassNotification() {
const [notifications, setNotifications] = useState(INITIAL_NOTIFICATIONS)
const dismiss = (id: number) => {
setNotifications((prev) => prev.filter((n) => n.id !== id))
}
const reset = () => setNotifications(INITIAL_NOTIFICATIONS)
return (
<div className="relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-[#1A1A19]">
{/* Background image */}
<img
src="https://ik.imagekit.io/aitoolkit/bg%20images/Ethereal%20Orange%20Flower%201%20(1).png"
alt=""
className="pointer-events-none absolute inset-0 h-full w-full object-cover opacity-60"
/>
{/* Notification stack */}
<div
className="relative flex w-[360px] flex-col gap-2.5"
>
{/* Header */}
<div className="mb-1 flex items-center justify-between px-1">
<div className="flex items-center gap-2">
<Bell size={20} weight="regular" className="text-white/40" />
<span className="text-sm font-semibold text-white/60">
Notifications
</span>
{notifications.length > 0 && (
<motion.span
layout
className="flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[9px] font-semibold text-white"
style={{
background: 'rgba(255, 107, 245, 0.4)',
border: '1px solid rgba(255, 107, 245, 0.3)',
}}
>
{notifications.length}
</motion.span>
)}
</div>
{notifications.length < INITIAL_NOTIFICATIONS.length && (
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={reset}
className="cursor-pointer text-xs font-medium text-white/30 transition-colors hover:text-white/50"
>
Reset
</motion.button>
)}
</div>
{/* Cards */}
<AnimatePresence mode="popLayout">
{notifications.map((n, i) => (
<NotificationCard key={n.id} notification={n} onDismiss={dismiss} index={i} />
))}
</AnimatePresence>
{/* Empty state */}
<AnimatePresence>
{notifications.length === 0 && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="flex flex-col items-center gap-3 py-12"
>
<span className="text-sm text-white/60">All caught up</span>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
)
}
Add to your project
One command adds this component to your project.
1
Run the following command. New project? Run npx shadcn@latest init first to set up Tailwind and path aliases.
npx shadcn@latest add @aicanvas/glass-notification2
For dark mode, add the dark class to your <html> element:
<html class="dark">

