Message Bubble

A state-driven speech bubble with incoming and outgoing variants and an optional reaction badge

A state atom whose appearance (hidden, visible) is a pure function of the state prop. The bubble enters from 12 px below at 0.94 scale, fades and scales up to rest. Incoming bubbles align to the left on theme.muted; outgoing bubbles align to the right on theme.primary. An optional reaction emoji badge pops in via the reactionStyle channel.

Installation

$ pnpm dlx shadcn@latest add @remocn/message-bubble

Installing message-bubble automatically installs the shared remocn-ui core (lib/remocn-ui/) via registryDependencies (["remocn-ui"]). You do not need to install it separately. Both message-bubble (the renderer) and use-message-bubble-transition (the hook) are copied into your project.

States

MessageBubbleState is:

type MessageBubbleState =
  | "hidden"
  | "visible"

hidden rests at opacity 0, translateY 12 px, and scale 0.94. visible brings the bubble to full opacity, translateY 0, and scale 1.

Smooth transitions

For animated enter, use useMessageBubbleTransition from the copied use-message-bubble-transition.ts file. It reads the frame, interpolates between state presets, and returns a resolved MessageBubbleStyle — pass it to the style prop:

import { MessageBubble } from "@/components/remocn/message-bubble";
import { useMessageBubbleTransition } from "@/components/remocn/use-message-bubble-transition";

export const Scene = () => {
  const style = useMessageBubbleTransition([
    { at: 20, state: "visible", duration: 14 },
    { at: 80, state: "hidden",  duration: 14 },
  ]);

  return (
    <div style={{ position: "relative", width: "100%", height: "100%" }}>
      <MessageBubble style={style} variant="incoming">Hey — ready for the demo?</MessageBubble>
    </div>
  );
};

The duration field overrides the file's DEFAULT_DURATION (= 14) for that specific transition. To globally tune timing and easing, edit use-message-bubble-transition.ts directly in your project — that file is yours (shadcn "own your code" philosophy).

style takes precedence over state when both are provided.

Variants

The variant prop sets the color and alignment of the bubble.

type MessageBubbleVariant =
  | "incoming"
  | "outgoing"

incoming uses theme.muted as the background and theme.foreground as text, aligned to the left. outgoing uses theme.primary as the background and theme.primaryForeground as text, aligned to the right.

<MessageBubble variant="incoming">Hey — ready for the demo?</MessageBubble>
<MessageBubble variant="outgoing">Yep, pushing it live now</MessageBubble>

Reactions

Pass a reaction emoji string to the reaction prop to render an emoji badge anchored to the bottom corner of the bubble. Drive the badge's animated entrance via the reactionStyle channel — a separate style object that controls the badge opacity and scale independently from the bubble body.

<MessageBubble
  variant="incoming"
  state="visible"
  reaction="🔥"
  reactionStyle={{ opacity: 1, scale: 1 }}
>
  Yep, pushing it live now
</MessageBubble>

When reaction is omitted no badge is rendered.

Props

PropTypeDefaultDescription
state
"hidden" | "visible""hidden"Current visual state (snap path). State changes snap — no automatic cross-fade.
style
MessageBubbleStyleResolved animated visual (smooth path). Pass an interpolated MessageBubbleStyle from useMessageBubbleTransition. Takes precedence over state when provided.
variant
"incoming" | "outgoing""incoming"Sets the bubble color and alignment. incoming uses theme.muted and left-aligns; outgoing uses theme.primary and right-aligns.
children
React.ReactNodeThe message text or content rendered inside the bubble.
reaction
stringOptional emoji string rendered as a badge anchored to the bottom corner of the bubble.
reactionStyle
MessageBubbleStyleAnimated style for the reaction badge. Controls opacity and scale independently from the bubble body.
maxWidth
number460Maximum width of the bubble in pixels.
theme
Partial<RemocnTheme>Per-component theme override. Merges with the active RemocnUIProvider theme and the mode defaults. Must be concrete oklch/hex/rgb values — not CSS custom properties.
className
stringOptional className applied to the bubble wrapper element.