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).
"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
"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
| Prop | Type | Default | Description |
|---|---|---|---|
| text* | string | — | Bubble text. |
| contentWidth | number | — | Baked glyph width in px. Required when multiline is false; ignored otherwise. |
| multiline | boolean | false | Wrap text and self-measure with ResizeObserver. |
| maxWidthPx | number | 320 | Width cap in multiline mode. |
| tail | boolean | false | Render 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. |
| animationDelayMs | number | — | Entrance-animation delay on the wrapper. |
| isNewlySent | boolean | false | Play the one-shot send spring (sent bubbles only). |
| className | string | — | Extra classes on the wrapper. |
| radiusOverride / padVOverride / textOffset | number / number / {x, y} | — | Pixel-calibration escape hatches used by the debug overlay. |
buildBubblePath
buildBubblePath(w: number, h: number, r?: number): stringThe 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).