Chat

Message.

Layout for one chat message: avatar, header, content and footer, aligned to the start or end of the thread.

Hub terminal
Turtle T-03 finished the north tunnel.
Fuel is at 12 percent. Send it home?
Yes, park it at the charging dock.

Installation#

With the CLI

$npx lanterncn add message

This also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/message. See Installation if your project is not set up yet.

Manually

  1. Copy the source into your project

    components/ui/message.tsx
    import * as React from "react";
    
    import { cn } from "@/lib/utils";
    
    function MessageGroup({ className, ...props }: React.ComponentProps<"div">) {
      return <div data-slot="message-group" className={cn("flex min-w-0 flex-col gap-2", className)} {...props} />;
    }
    
    function Message({
      className,
      align = "start",
      ...props
    }: React.ComponentProps<"div"> & { align?: "start" | "end" }) {
      return (
        <div
          data-slot="message"
          data-align={align}
          className={cn(
            "group/message relative flex w-full min-w-0 gap-2.5 text-sm data-[align=end]:flex-row-reverse",
            className,
          )}
          {...props}
        />
      );
    }
    
    function MessageAvatar({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="message-avatar"
          className={cn(
            "flex w-fit min-w-8 shrink-0 items-center justify-center self-end overflow-hidden rounded-full bg-secondary group-has-data-[slot=message-footer]/message:-translate-y-6 [&_[data-slot=avatar]]:size-8",
            className,
          )}
          {...props}
        />
      );
    }
    
    function MessageContent({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="message-content"
          className={cn(
            "flex w-full min-w-0 flex-col gap-1.5 wrap-break-word group-data-[align=end]/message:*:data-slot:self-end",
            className,
          )}
          {...props}
        />
      );
    }
    
    function MessageHeader({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="message-header"
          className={cn(
            "flex max-w-full min-w-0 items-center gap-2 px-1 font-mono text-[10px] tracking-[0.15em] text-muted-foreground uppercase group-has-data-[variant=ghost]/message:px-0",
            className,
          )}
          {...props}
        />
      );
    }
    
    function MessageFooter({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="message-footer"
          className={cn(
            "flex max-w-full min-w-0 items-center gap-2 px-1 font-mono text-[10px] tracking-[0.1em] text-muted-foreground group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end [&_svg:not([class*='size-'])]:size-3",
            className,
          )}
          {...props}
        />
      );
    }
    
    export { MessageGroup, Message, MessageAvatar, MessageContent, MessageFooter, MessageHeader };
  2. Update the import paths to match your project

    The source imports cn from @/lib/utils.

Usage#

import {
  MessageGroup,
  Message,
  MessageAvatar,
  MessageContent,
  MessageFooter,
  MessageHeader,
} from "@/components/ui/message";
<Message align="end">
  <MessageContent>
    <Bubble>
      <BubbleContent>Park it at the charging dock.</BubbleContent>
    </Bubble>
    <MessageFooter>09:42</MessageFooter>
  </MessageContent>
</Message>

Message only handles layout. Put Bubble, Attachment or any other content inside MessageContent; with align="end" its children line up on the right.