iMessage UIv0.1.0

Components

HeroBubble

The low-level tailed bubble with the SwiftUI-accurate tail curl. Two modes: fixed-width (server-rendered at an exact baked width — zero layout shift; the homepage hero uses this) and multiline (wraps and self-measures with ResizeObserver).

the low-level bubble: SwiftUI-accurate tail, wraps at maxWidthPx
sent flips the tail and the fill
"use client";

import { HeroBubble, IMessageScreen } from "imessage-ui";

export default function HeroBubbleDemo() {
  return (
    <IMessageScreen className="flex w-full max-w-sm flex-col gap-2 rounded-2xl p-4">
      <HeroBubble
        multiline
        text="the low-level bubble: SwiftUI-accurate tail, wraps at maxWidthPx"
        tail
        maxWidthPx={260}
      />
      <HeroBubble multiline variant="sent" text="sent flips the tail and the fill" tail />
    </IMessageScreen>
  );
}

Usage

import { HeroBubble, buildBubblePath } from "imessage-ui";

<HeroBubble multiline tail text="wraps at maxWidthPx" />
<HeroBubble text="fixed width" contentWidth={96} tail />

Examples

Fixed width

Hi, I'm Wesley 👋
welcome to my portfolio
"use client";

import { HeroBubble, IMessageScreen } from "imessage-ui";

/**
 * Fixed-width mode (the default): you pass the measured glyph width and the
 * bubble renders at that exact size on the server — no layout shift, ever.
 * The homepage hero bakes these widths at build time.
 */
export default function FixedWidthDemo() {
  return (
    <IMessageScreen className="flex w-full max-w-sm flex-col gap-2 rounded-2xl p-4">
      <HeroBubble text="Hi, I'm Wesley 👋" contentWidth={124} tail />
      <HeroBubble text="welcome to my portfolio" contentWidth={168} variant="sent" tail />
    </IMessageScreen>
  );
}

contentWidth is required in this mode — the bubble renders at its final size on the server, so the entrance animation runs on stable geometry from the first frame.

API reference

HeroBubble

PropTypeDefaultDescription
text*stringBubble text.
contentWidthnumberBaked glyph width in px. Required when multiline is false; ignored otherwise.
multilinebooleanfalseWrap text and self-measure with ResizeObserver.
maxWidthPxnumber320Width cap in multiline mode.
tailbooleanfalseRender the iOS tail curl.
variant"received" | "sent""received"Gray left-tail or blue right-tail.
position"first" | "middle" | "last" | "solo""solo"Reserved for position-based corner tucking.
animationDelayMsnumberEntrance-animation delay on the wrapper.
isNewlySentbooleanfalsePlay the one-shot send spring (sent bubbles only).
classNamestringExtra classes on the wrapper.
radiusOverride / padVOverride / textOffsetnumber / number / {x, y}Pixel-calibration escape hatches used by the debug overlay.

buildBubblePath

buildBubblePath(w: number, h: number, r?: number): string

The SVG path builder behind every tailed bubble — a direct port of the SwiftUI BubbleShape. Exported so you can clip, mask or draw the exact iMessage silhouette yourself (the glass bubbles clip their backdrop-filter with it).