shadimage
solaceui
componentchat-messages-1source: ok · hit

Chat Messages 1

Solace UI chat messages component variation 1.

npx shadcn add "https://www.solaceui.com/r/chat-messages-1.json"
free

Where it goes

chat

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationmotion
iconslucide-react

Files5 · source cached 10/15/2026, 9:52:31 AM

src/registry/components/chat-messages/one/index.tsx
"use client";

import { useState } from "react";
import Chat, { type User } from "@/registry/components/chat-messages/chat";
import type { Message } from "@/registry/components/chat-messages/chat-message";
import IphoneFrame from "@/registry/components/chat-messages/iphone-frame";

export default function ChatMessagesDemo() {
    // Define users with their avatars
    const users: User[] = [
        {
            name: "User1",
            avatar:
                "https://assets.solaceui.com/solaceui-member-five.png", // Replace with actual avatar URL
        },
        {
            name: "User2",
            avatar:
                "https://assets.solaceui.com/solaceui-member-four.png", // Replace with actual avatar URL
        },
    ];

    const [messages] = useState<Message[]>([
        {
            id: "m1",
            name: "User1",
            message: "Hey, why'd you call it Solace UI ?",
        },
        {
            id: "m2",
            name: "User2",
            message: "Because it is meant to be chill and stress free.",
        },
        {
            id: "m3",
            name: "User1",
            message: "So is it like a digital chill zone?",
        },
        {
            id: "m4",
            name: "User2",
            message: "Exactly, simple and calm design.",
        },
    ]);

    return (
        <main className="relative flex flex-col items-center justify-center p-4 ">
            {/* Clipping Wrapper */}
            <div className="mx-auto overflow-hidden w-[300px] h-[360px] sm:w-[350px] sm:h-[400px]">
                <IphoneFrame>
                    <Chat
                        messages={messages}
                        currentUser="User2"
                        users={users} // Pass the users array with avatars
                    />
                </IphoneFrame>


            </div>
            <div
                className="absolute bottom-0 left-0 right-0 z-50 h-24 sm:h-32 bg-gradient-to-t from-background to-transparent pointer-events-none"
                aria-hidden="true"
            ></div>
        </main>
    );
}
src/registry/components/chat-messages/chat.tsx
"use client";

import { type FC, useRef, useEffect, useState, useLayoutEffect } from "react";
import { motion, AnimatePresence } from "motion/react";
import ChatMessage, { type Message } from "@/registry/components/chat-messages/chat-message";

export interface User {
    name: string;
    avatar: string;
}

interface ChatProps {
    messages: Message[];
    currentUser: string;
    users: User[];
}

const Chat: FC<ChatProps> = ({ messages, currentUser, users }) => {
    const containerRef = useRef<HTMLDivElement>(null);
    const [visibleMessages, setVisibleMessages] = useState<Message[]>([]);

    useEffect(() => {
        if (messages.length === 0) return;

        setVisibleMessages([]);
        let cancelled = false;

        (async () => {
            for (const msg of messages) {
                if (cancelled) return; // guard before delay
                await new Promise((r) => setTimeout(r, 700));
                if (cancelled) return; // guard after delay
                setVisibleMessages((prev) => [...prev, msg]);
            }
        })();

        return () => {
            cancelled = true;
        };
    }, [messages]);

    useLayoutEffect(() => {
        const el = containerRef.current;
        if (el) el.scrollTop = el.scrollHeight;
    }, [visibleMessages]);

    const getUserAvatar = (name: string) =>
        users.find((u) => u.name === name)?.avatar ??
        "/placeholder.svg?height=40&width=40";

    return (
        <div
            ref={containerRef}
            className="w-full h-full overflow-y-auto p-2 bg-white dark:bg-[#212121] flex flex-col py-[2rem]"
        >
            <AnimatePresence initial={false} mode="popLayout">
                {visibleMessages.map((message) => (
                    <motion.div
                        key={message.id}
                        layout
                        initial={{ opacity: 0, y: 20 }}
                        animate={{ opacity: 1, y: 0 }}
                        exit={{ opacity: 0, y: 20 }}
                        transition={{ duration: 0.4 }}
                        className={`flex mb-4 ${message.name === currentUser ? "justify-end" : "justify-start"
                            } items-end`}
                    >
                        <ChatMessage
                            message={message}
                            isCurrentUser={message.name === currentUser}
                            avatar={getUserAvatar(message.name)}
                        />
                    </motion.div>
                ))}
            </AnimatePresence>

            {visibleMessages.length > 0 &&
                visibleMessages[visibleMessages.length - 1].name === currentUser && (
                    <div className="text-xs text-right mt-1 mr-2 dark:text-white text-black">
                        Delivered
                    </div>
                )}
        </div>
    );
};

export default Chat;
src/registry/components/chat-messages/chat-message.tsx
"use client";

import { motion } from "motion/react";
import type { FC } from "react";
import Image from "next/image";

export interface Message {
    id: string;
    name: string;
    message: string;
    timestamp?: string;
}

interface ChatMessageProps {
    message: Message;
    isCurrentUser: boolean;
    avatar: string;
}

const ChatMessage: FC<ChatMessageProps> = ({
    message,
    isCurrentUser,
    avatar,
}) => {
    return (
        <motion.div
            layout
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: 20 }}
            transition={{ duration: 0.4 }}
            className={`flex mb-4 ${isCurrentUser ? "justify-end" : "justify-start"
                } items-end`}
        >
            {!isCurrentUser && (
                <div className="flex-shrink-0 mr-2">
                    <div className="w-8 h-8 rounded-full overflow-hidden">
                        <Image
                            src={avatar || "/placeholder.svg"}
                            alt={`${message.name}'s avatar`}
                            width={32}
                            height={32}
                            className="object-cover"
                          unoptimized
                        />
                    </div>
                </div>
            )}

            <div
                className={`max-w-[70%] px-2 py-1 rounded-2xl ${isCurrentUser
                        ? "bg-blue-500 text-white rounded-br-none"
                        : "bg-gray-200 text-black rounded-bl-none"
                    }`}
            >
                <p className="text-sm sm:text-base">{message.message}</p>
                {message.timestamp && (
                    <p
                        className={`text-xs mt-1 ${isCurrentUser ? "text-blue-100" : "text-gray-500"
                            }`}
                    >
                        {message.timestamp}
                    </p>
                )}
            </div>

            {isCurrentUser && <div className="w-8 "></div>}
        </motion.div>
    );
};

export default ChatMessage;
src/registry/components/chat-messages/blur-fade.tsx
// Component From Motion-Primitives

"use client";

import {
    AnimatePresence,
    motion,
    useInView,
    UseInViewOptions,
    Variants,
    MotionProps,
} from "motion/react";
import { useRef } from "react";

type MarginType = UseInViewOptions["margin"];

interface BlurFadeProps extends MotionProps {
    children: React.ReactNode;
    className?: string;
    variant?: {
        hidden: { y: number };
        visible: { y: number };
    };
    duration?: number;
    delay?: number;
    offset?: number;
    direction?: "up" | "down" | "left" | "right";
    inView?: boolean;
    inViewMargin?: MarginType;
    blur?: string;
}

export function BlurFade({
    children,
    className,
    variant,
    duration = 0.4,
    delay = 0,
    offset = 6,
    direction = "down",
    inView = false,
    inViewMargin = "-50px",
    blur = "6px",
    ...props
}: BlurFadeProps) {
    const ref = useRef(null);
    const inViewResult = useInView(ref, { once: true, margin: inViewMargin });
    const isInView = !inView || inViewResult;
    const defaultVariants: Variants = {
        hidden: {
            [direction === "left" || direction === "right" ? "x" : "y"]:
                direction === "right" || direction === "down" ? -offset : offset,
            opacity: 0,
            filter: `blur(${blur})`,
        },
        visible: {
            [direction === "left" || direction === "right" ? "x" : "y"]: 0,
            opacity: 1,
            filter: `blur(0px)`,
        },
    };
    const combinedVariants = variant || defaultVariants;
    return (
        <AnimatePresence>
            <motion.div
                ref={ref}
                initial="hidden"
                animate={isInView ? "visible" : "hidden"}
                exit="hidden"
                variants={combinedVariants}
                transition={{
                    delay: 0.04 + delay,
                    duration,
                    ease: "easeOut",
                }}
                className={className}
                {...props}
            >
                {children}
            </motion.div>
        </AnimatePresence>
    );
}
src/registry/components/chat-messages/iphone-frame.tsx
import type { FC, ReactNode } from "react";
import {
    WifiIcon as LuWifi,
    BatteryMediumIcon as LuBatteryMedium,
    SignalIcon as LuSignal,
} from "lucide-react";

interface IphoneFrameProps {
    children: ReactNode;
    className?: string;
}

const IphoneFrame: FC<IphoneFrameProps> = ({ children, className = "" }) => {
    // Get current time in format HH:MM
    const getCurrentTime = () => {
        const now = new Date();
        const hours = now.getHours().toString().padStart(2, "0");
        const minutes = now.getMinutes().toString().padStart(2, "0");
        return `${hours}:${minutes}`;
    };

    return (
        // The main phone body
        <div
            className={`relative mx-auto border-[10px] border-gray-900 bg-[#212121] dark:border-gray-200 dark:bg-gray-100 rounded-[44px] shadow-xl overflow-hidden w-[300px] h-[600px] sm:w-[350px] sm:h-[700px] ${className}`}
        >
            {/* Status Bar */}
            <div className="absolute top-0 left-0 right-0 h-8 z-20 px-6 flex justify-between items-center text-black dark:text-white dark:bg-[#212121] bg-white">
                {/* Left side - Time */}
                <div className="text-xs font-semibold flex items-center">
                    <LuSignal className="w-3.5 h-3.5 mr-1" />
                    {getCurrentTime()}
                </div>

                {/* Notch (center) */}
                <div
                    className="absolute top-0 left-1/2 -translate-x-1/2 w-[45%] h-[24px] bg-black dark:bg-gray-100 rounded-b-xl z-30 flex justify-center items-center space-x-2 p-1"
                >
                    {/* Notch Details (Speaker/Camera) */}
                    <div
                        className="w-10 h-2 rounded-full bg-white dark:bg-black"></div>
                    <div
                        className="w-2 h-2 rounded-full bg-white dark:bg-black"></div>
                </div>

                {/* Right side - WiFi and Battery */}
                <div className="text-xs font-semibold flex items-center">
                    <LuWifi className="w-4 h-4 mr-1" />
                    <LuBatteryMedium className="w-5 h-5" />
                </div>
            </div>

            {/* The screen area */}
            <div
                className="w-full h-full (bg-white dark:bg-black) rounded-[34px] overflow-hidden pt-8"
            >
                {children}
            </div>

            {/* Side Buttons */}
            {/* Volume Up */}
            <div
                className="absolute -left-[12px] top-[100px] w-[4px] h-[50px] rounded-l-md bg-gray-800 dark:bg-gray-300"></div>
            {/* Volume Down */}
            <div
                className="absolute -left-[12px] top-[160px] w-[4px] h-[50px] rounded-l-md bg-gray-800 dark:bg-gray-300"></div>
            {/* Power/Side Button */}
            <div
                className="absolute -right-[12px] top-[130px] w-[4px] h-[80px] rounded-r-md bg-gray-800 dark:bg-gray-300"></div>
        </div>
    );
};

export default IphoneFrame;

More from solaceui

Browse all components in this library →