solaceui
componentchat-messages-2source: ok · hit
Chat Messages 2
Solace UI chat messages component variation 2.
npx shadcn add "https://www.solaceui.com/r/chat-messages-2.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 · mediumanimationmotion
iconslucide-react
Files5 · source cached 10/14/2026, 9:09:23 PM
src/registry/components/chat-messages/two/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 ChatMessagesReverse() {
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: "n1",
name: "User1",
message: "Hey, what is the best component library?",
},
{
id: "n2",
name: "User2",
message: "I think Solace UI is awesome it is simple and easy to use.",
},
{
id: "n3",
name: "User1",
message: "So, it is reliable and has a clean design?",
},
{
id: "n4",
name: "User2",
message: "Yes, exactly. It makes development a breeze.",
},
]);
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-[420px]">
<IphoneFrame>
<Chat
messages={messages}
currentUser="User1"
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-linear-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;