imessage-ui
v0.1.0
Published
Pixel-accurate iMessage UI kit for React — tailed bubbles, tapback reactions, typing indicators, avatars, chat chrome, and the iOS send/pop animations.
Maintainers
Readme
imessage-ui
A pixel-accurate iMessage UI kit for React — tailed bubbles with the real SwiftUI tail curl, iOS tapback reactions, typing indicators, avatars, chat chrome, and the send/pop spring animations. It's the shared design system behind wesleykamau.com's group-chat simulator, the homepage guestbook, and the intake bot, so all three stay identical down to the pixel.
One source of truth, three surfaces. Edit a bubble here and the group chat, the guestbook, and the intake bot all move together.
Install
It's a workspace package. Inside this repo it's already linked via npm
workspaces ("imessage-ui": "*"). Another app in a sibling repo
consumes it with a file: dependency:
// the other app's package.json
"dependencies": {
"imessage-ui": "file:../Website/packages/imessage-ui"
}Because the package ships TypeScript source (no build step), every consumer must transpile it. In Next.js:
// next.config.ts
const nextConfig = {
transpilePackages: ["imessage-ui"],
};Then load the global primitive layer once (root layout) and you're set:
// app/layout.tsx
import "imessage-ui/css";Quick start
import { IMessageScreen, ChatBubble, TypingIndicator } from "imessage-ui";
export function Thread() {
return (
<IMessageScreen>
<ChatBubble variant="received" text="hey it's wesley" position="first" />
<ChatBubble variant="received" text="well, the ai version i built" position="last"
avatar={{ initial: "W", gradient: ["#0a84ff", "#0066d6"] }} />
<ChatBubble variant="sent" text="no wayyy" reactions={[{ kind: "heart" }]} />
<TypingIndicator avatar={{ initial: "W", gradient: ["#0a84ff", "#0066d6"] }} />
</IMessageScreen>
);
}Theming
<IMessageScreen> (or the .screen class from imessage-ui/styles)
establishes the full --imsg-* token palette. Light by default; a .dark
ancestor flips every token to dark. Override any token inline to retheme:
<IMessageScreen style={{ "--imsg-blue": "#34c759" } as React.CSSProperties}>…</IMessageScreen>Key tokens (full list in imessage.module.css .screen):
| Token | Role |
| --- | --- |
| --imsg-blue | Sent bubble fill |
| --imsg-bubble | Received bubble fill |
| --imsg-text | Bubble text |
| --imsg-screen-bg | Chat backdrop |
| --imsg-thumb-color | Like/dislike tapback glyph |
| --imsg-wall-offset | Tail wall inset (default 5px) |
Components
| Component | Purpose | Key props |
| --- | --- | --- |
| IMessageScreen | Token scope + backdrop wrapper | className, style |
| ChatBubble | Sent/received message bubble | text, variant, position, tail, avatar, senderName, receipt, reactions, isNewlySent, hideAvatar |
| HeroBubble | Low-level tailed bubble | text, multiline | contentWidth, variant, tail, maxWidthPx, isNewlySent |
| ImageBubble | Image attachment bubble | attachment, variant, position, avatar, senderName |
| ReactionBubble | A laid tapback | kind, side, count |
| ReactionGlyph | Bare tapback icon (for pickers) | kind, size |
| TypingIndicator | Animated "…" bubble | avatar, senderName |
| AvatarCircle | Gradient-initial or photo avatar | avatar, size, showInitial |
| ChatHeader | iOS group header w/ avatar cluster | bots ({ id, avatar }[]), title, onReset |
| InputBar | Auto-growing input pill + send | value, onChange, onSend, awaiting, submitOnEnter |
Helpers: buildBubblePath(w, h, r), isEmojiOnly(text), cn(...).
Types: ReactionKind, Reaction, AvatarInfo, ImageAttachment, Attachment,
ChatBubbleVariant, ChatBubblePosition, HeroBubbleVariant, BubblePosition,
ChatHeaderBot.
Message grouping
position ("solo" | "first" | "middle" | "last") controls tails, avatars, and
sender-name placement the way iOS collapses a run of messages from one sender:
the tail and avatar render only on the last/solo bubble; the sender name only
on the first/solo bubble.
Reactions both ways
ChatBubble's reactions is a plain Reaction[]. The kit is render-only —
your app owns who reacted and persistence; pass the resulting array in. For a
tapback picker, render ReactionGlyph for each ReactionKind and lay the chosen
one down as a Reaction (see the guestbook and intake for working pickers).
Subpath exports
| Import | What |
| --- | --- |
| imessage-ui | Components, helpers, and types |
| imessage-ui/styles | The shared CSS-module class map — for app-level layout compositions that compose styles.receivedRow, styles.screen, etc. directly. Resolves to the one physical imessage.module.css, so hashes match the package internals. |
| imessage-ui/css | The global primitive layer (bubble classes + keyframes). Import once per app. |
Accessibility & motion
- Icon-only controls carry
aria-labels; decorative SVG isaria-hidden. - The entrance/
imsg-send-popanimations honorprefers-reduced-motion. - The opacity-only
.imsg-popis deliberate — atransformwould promote a bubble to its own GPU layer and break the frosted-header backdrop blur. Read the comments inimessage.global.cssbefore adding transforms.
Live gallery
Every component, in every state (sent/received, grouped, both-side reactions,
typing, image bubble, header, input bar, light + dark) renders at
/debug/imessage in the Website app —
the living showcase and visual-regression surface.
Versioning & sync
This package is the canonical source. Consumers pin it via workspace (*) or a
file: link, so a change here propagates on the next install/build — no copy
step. Bump the version + CHANGELOG.md on any breaking prop/class change.
