Notification Card
Interactive notification system with auto-dismiss, actions, expandable content, and multiple types.
ReactTailwind CSSTypeScriptResponsiveCard
Live Preview
Interactive
Notification Card.tsxLive Code
"use client";
import { useState } from "react";
type NotificationType = "success" | "warning" | "error" | "info";
interface Notification {
id: string;
type: NotificationType;
title: string;
message: string;
timestamp: Date;
actions?: {
label: string;
action: () => void;
variant?: "primary" | "secondary";
}[];
autoDismiss?: boolean;
dismissTime?: number;
}
export default function NotificationCard() {
const [notifications, setNotifications] = useState<Notification[]>([]);
const [isExpanded, setIsExpanded] = useState<Record<string, boolean>>({});
const sampleNotifications: Omit<Notification, "id" | "timestamp">[] = [
{
type: "success",
title: "Payment Successful",
message:
"Your payment of $299.99 has been processed successfully. You will receive a confirmation email shortly.",
autoDismiss: true,
dismissTime: 5000,
actions: [
{
label: "View Receipt",
action: () => console.log("View receipt"),
variant: "primary",
},
{ label: "Dismiss", action: () => {}, variant: "secondary" },
],
},
{
type: "warning",
title: "Storage Almost Full",
message:
"Your account storage is 85% full. Consider upgrading your plan or removing old files.",
actions: [
{
label: "Upgrade Plan",
action: () => console.log("Upgrade"),
variant: "primary",
},
{
label: "Manage Files",
action: () => console.log("Manage"),
variant: "secondary",
},
],
},
{
type: "error",
title: "Connection Failed",
message:
"Unable to connect to the server. Please check your internet connection and try again.",
actions: [
{
label: "Retry",
action: () => console.log("Retry"),
variant: "primary",
},
],
},
{
type: "info",
title: "New Feature Available",
message:
"We have released a new dashboard feature that helps you track your analytics better. Check it out!",
actions: [
{
label: "Learn More",
action: () => console.log("Learn more"),
variant: "primary",
},
{ label: "Later", action: () => {}, variant: "secondary" },
],
},
];
const addNotification = () => {
const sample =
sampleNotifications[
Math.floor(Math.random() * sampleNotifications.length)
];
const newNotification: Notification = {
...sample,
id: Date.now().toString(),
timestamp: new Date(),
};
setNotifications((prev) => [newNotification, ...prev]);
if (newNotification.autoDismiss && newNotification.dismissTime) {
setTimeout(() => {
dismissNotification(newNotification.id);
}, newNotification.dismissTime);
}
};
const dismissNotification = (id: string) => {
setNotifications((prev) => prev.filter((notif) => notif.id !== id));
setIsExpanded((prev) => {
const newExpanded = { ...prev };
delete newExpanded[id];
return newExpanded;
});
};
const toggleExpanded = (id: string) => {
setIsExpanded((prev) => ({
...prev,
[id]: !prev[id],
}));
};
const clearAll = () => {
setNotifications([]);
setIsExpanded({});
};
const getNotificationStyles = (type: NotificationType) => {
const styles = {
success: {
border: "border-green-200",
bg: "bg-green-50",
icon: "text-green-600",
title: "text-green-900",
message: "text-green-800",
progress: "bg-green-600",
},
warning: {
border: "border-yellow-200",
bg: "bg-yellow-50",
icon: "text-yellow-600",
title: "text-yellow-900",
message: "text-yellow-800",
progress: "bg-yellow-600",
},
error: {
border: "border-red-200",
bg: "bg-red-50",
icon: "text-red-600",
title: "text-red-900",
message: "text-red-800",
progress: "bg-red-600",
},
info: {
border: "border-blue-200",
bg: "bg-blue-50",
icon: "text-blue-600",
title: "text-blue-900",
message: "text-blue-800",
progress: "bg-blue-600",
},
};
return styles[type];
};
const getNotificationIcon = (type: NotificationType) => {
const icons = {
success: (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
\/>
<\/svg>
),
warning: (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
\/>
<\/svg>
),
error: (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
\/>
<\/svg>
),
info: (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
\/>
<\/svg>
),
};
return icons[type];
};
const formatTimestamp = (timestamp: Date) => {
const now = new Date();
const diffInMinutes = Math.floor(
(now.getTime() - timestamp.getTime()) \/ 60000
);
if (diffInMinutes < 1) return "Just now";
if (diffInMinutes < 60) return `${diffInMinutes}m ago`;
const diffInHours = Math.floor(diffInMinutes \/ 60);
if (diffInHours < 24) return `${diffInHours}h ago`;
const diffInDays = Math.floor(diffInHours \/ 24);
return `${diffInDays}d ago`;
};
return (
<div className="w-full max-w-md mx-auto space-y-4">
<div className="flex flex-col sm:flex-row gap-2 p-4 bg-gray-50 rounded-lg">
<button
onClick={addNotification}
className="flex-1 bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition"
>
<span className="hidden sm:inline">Add Random Notification<\/span>
<span className="sm:hidden">Add Notification<\/span>
<\/button>
{notifications.length > 0 && (
<button
onClick={clearAll}
className="flex-1 sm:flex-none bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition"
>
Clear All ({notifications.length})
<\/button>
)}
<\/div>
<div className="space-y-3 max-h-[500px] overflow-y-auto">
{notifications.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<div className="w-12 h-12 mx-auto mb-3 bg-gray-100 rounded-full flex items-center justify-center">
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 17h5l-5 5v-5z"
\/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4"
\/>
<\/svg>
<\/div>
<p className="text-sm">No notifications<\/p>
<p className="text-xs text-gray-400 mt-1">
<span className="hidden sm:inline">
Click "Add Random Notification" to see examples
<\/span>
<span className="sm:hidden">
Add a notification to get started
<\/span>
<\/p>
<\/div>
) : (
notifications.map((notification) => {
const styles = getNotificationStyles(notification.type);
const expanded = isExpanded[notification.id];
const shouldTruncate = notification.message.length > 80;
return (
<div
key={notification.id}
className={`${styles.border} ${styles.bg} border rounded-lg shadow-sm overflow-hidden transition-all duration-300`}
>
{notification.autoDismiss && notification.dismissTime && (
<div className="h-1 bg-gray-200">
<div
className={`h-full ${styles.progress} transition-all ease-linear`}
style={{
animation: `shrink ${notification.dismissTime}ms linear forwards`,
}}
\/>
<\/div>
)}
<div className="p-4">
<div className="flex items-start gap-3">
<div className={`flex-shrink-0 ${styles.icon}`}>
{getNotificationIcon(notification.type)}
<\/div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<h4 className={`font-semibold text-sm ${styles.title}`}>
{notification.title}
<\/h4>
<div className="flex items-center gap-1 flex-shrink-0">
<span className="text-xs text-gray-500">
{formatTimestamp(notification.timestamp)}
<\/span>
<button
onClick={() => dismissNotification(notification.id)}
className="text-gray-400 hover:text-gray-600 p-1 rounded transition"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
\/>
<\/svg>
<\/button>
<\/div>
<\/div>
<div className={`mt-1 text-sm ${styles.message}`}>
{shouldTruncate && !expanded ? (
<div>
{notification.message.substring(0, 80)}...
<button
onClick={() => toggleExpanded(notification.id)}
className="ml-2 text-gray-600 hover:text-gray-800 font-medium underline"
>
Show more
<\/button>
<\/div>
) : (
<div>
{notification.message}
{shouldTruncate && expanded && (
<button
onClick={() => toggleExpanded(notification.id)}
className="ml-2 text-gray-600 hover:text-gray-800 font-medium underline"
>
Show less
<\/button>
)}
<\/div>
)}
<\/div>
{notification.actions &&
notification.actions.length > 0 && (
<div className="flex flex-wrap gap-2 mt-3">
{notification.actions.map((action, index) => (
<button
key={index}
onClick={() => {
action.action();
if (
action.label === "Dismiss" ||
action.label === "Later"
) {
dismissNotification(notification.id);
}
}}
className={`px-3 py-1 rounded text-sm font-medium transition ${
action.variant === "primary"
? `bg-${
notification.type === "warning"
? "yellow"
: notification.type === "error"
? "red"
: notification.type === "info"
? "blue"
: "green"
}-600 text-white hover:bg-${
notification.type === "warning"
? "yellow"
: notification.type === "error"
? "red"
: notification.type === "info"
? "blue"
: "green"
}-700`
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
<span className="hidden sm:inline">
{action.label}
<\/span>
<span className="sm:hidden">
{action.label === "View Receipt"
? "View"
: action.label === "Upgrade Plan"
? "Upgrade"
: action.label === "Manage Files"
? "Manage"
: action.label === "Learn More"
? "Learn"
: action.label}
<\/span>
<\/button>
))}
<\/div>
)}
<\/div>
<\/div>
<\/div>
<\/div>
);
})
)}
<\/div>
<div className="text-center text-xs text-gray-500 p-3 bg-gray-50 rounded-lg">
<p>
<span className="hidden sm:inline">
Interactive notification system with auto-dismiss, actions, and
responsive design
<\/span>
<span className="sm:hidden">
Interactive notifications with auto-dismiss
<\/span>
<\/p>
<\/div>
<style jsx>{`
@keyframes shrink {
from {
width: 100%;
}
to {
width: 0%;
}
}
`}<\/style>
<\/div>
);
}
How to use
- 1. Copy the component code
- 2. Paste it into your React/Next.js project
- 3. Make sure you have Tailwind CSS configured
- 4. Customize colors, spacing, and content as needed
Dependencies
react
^18.0.0tailwindcss
^3.0.0Back to Components
Need complete templates?
View Premium Templates