-50% CODE

BLOOM50

|

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 &quot;Add Random Notification&quot; 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. 1. Copy the component code
  2. 2. Paste it into your React/Next.js project
  3. 3. Make sure you have Tailwind CSS configured
  4. 4. Customize colors, spacing, and content as needed

Dependencies

react^18.0.0
tailwindcss^3.0.0