AniUI

Chat Bubble

Message bubble for chat interfaces with sent/received variants.

Installation#

npx @aniui/cli add chat-bubble

Hey, how are you?

2:30 PM

I'm doing great, thanks!

2:31 PM
Web preview — components render natively on iOS & Android
import { ChatBubble } from "@/components/ui/chat-bubble";

export function MyScreen() {
  return (
    <View className="gap-3 p-4">
      <ChatBubble variant="received" timestamp="2:30 PM">
        Hey, how are you?
      </ChatBubble>
      <ChatBubble variant="sent" timestamp="2:31 PM" status="read">
        I'm doing great, thanks!
      </ChatBubble>
      <ChatBubble variant="sent" timestamp="2:31 PM" status="delivered">
        How about you?
      </ChatBubble>
    </View>
  );
}

Usage#

app/index.tsx
import { ChatBubble } from "@/components/ui/chat-bubble";

export function MyScreen() {
  return (
    <View className="gap-3 p-4">
      <ChatBubble variant="received" timestamp="2:30 PM">
        Hey, how are you?
      </ChatBubble>
      <ChatBubble variant="sent" timestamp="2:31 PM" status="read">
        I'm doing great, thanks!
      </ChatBubble>
      <ChatBubble variant="sent" timestamp="2:31 PM" status="delivered">
        How about you?
      </ChatBubble>
    </View>
  );
}

Props#

PropTypeDefault
variant
"sent" | "received"
"received"
children
React.ReactNode
-
timestamp
string
-
status
"sent" | "delivered" | "read"
-
className
string
-

Also accepts all View props.

Accessibility#

  • Message bubble with sent/received styling for visual distinction.
  • Message content is readable by screen readers with sender context.

Source#

components/ui/chat-bubble.tsx
import React from "react";
import { View, Text } from "react-native";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const bubbleVariants = cva("max-w-[80%] rounded-2xl px-4 py-2.5", {
  variants: {
    variant: {
      sent: "bg-primary self-end rounded-br-sm",
      received: "bg-secondary self-start rounded-bl-sm",
    },
  },
  defaultVariants: { variant: "received" },
});

const textVariants = cva("text-base", {
  variants: {
    variant: {
      sent: "text-primary-foreground",
      received: "text-secondary-foreground",
    },
  },
  defaultVariants: { variant: "received" },
});

export interface ChatBubbleProps
  extends React.ComponentPropsWithoutRef<typeof View>,
    VariantProps<typeof bubbleVariants> {
  className?: string;
  children: React.ReactNode;
  timestamp?: string;
  status?: "sent" | "delivered" | "read";
}

const statusIcons: Record<string, string> = {
  sent: "✓",
  delivered: "✓✓",
  read: "✓✓",
};

export function ChatBubble({
  variant,
  className,
  children,
  timestamp,
  status,
  ...props
}: ChatBubbleProps) {
  const isSent = variant === "sent";

  return (
    <View className={cn(bubbleVariants({ variant }), className)} {...props}>
      <Text className={textVariants({ variant })}>{children}</Text>
      {(timestamp || status) && (
        <View className={cn("flex-row items-center gap-1 mt-1", isSent ? "self-end" : "self-start")}>
          {timestamp && (
            <Text className={cn("text-[10px]", isSent ? "text-primary-foreground/60" : "text-muted-foreground")}>
              {timestamp}
            </Text>
          )}
          {status && isSent && (
            <Text className={cn("text-[10px]", status === "read" ? "text-blue-300" : "text-primary-foreground/60")}>
              {statusIcons[status]}
            </Text>
          )}
        </View>
      )}
    </View>
  );
}