Skip to content

ChatBubble

One message in a conversation. The avatar, the sender's name, the time, the delivery mark, the media above the text and the link card below it are each drawn only when they are given something.

tsx
import { Avatar, ChatBubble } from 'neba';

<ChatBubble avatar={<Avatar name="Jane Doe" size="sm" />} name="Jane" time="09:41">
  Are we still on for the review at 3?
</ChatBubble>

<ChatBubble side="end" variant="solid" status="read">
  Yes — I pushed the branch just now.
</ChatBubble>;

Props

PropTypeDefaultDescription
side'start' | 'end''start'Whose message this is. Decides which way the row runs and which corner of the sheet is cut short
nameReactNodeWho sent it, above the bubble
timeReactNodeWhen it was sent, beside the name
avatarReactNodeThe sender’s picture: an Avatar. Left out, the bubble takes the whole row
status'sending' | 'sent' | 'delivered' | 'read' | 'failed'How far the message has got, drawn as a mark under the bubble. Left out, nothing is drawn
statusLabelstringOverrides the word the mark is read out as
typingbooleanfalseDraws the three dots instead of the message. children is left alone, so the same bubble can go back to it
mediaReactNodeA picture, a video, a map: drawn edge to edge above the text, cropped by the bubble’s corners
previewChatBubbleLinkPreviewA link in the message, unfurled into a card: url · title · description · image · site · newTab
actionsReactNodeThe message’s own actions: usually a Menu trigger. Sits beside the bubble and stays out of the way until the row is hovered or focused
localestring'en'Which language the marks are read out in: a BCP 47 tag. Unsupported tags fall back to English
variantshared'solid' | 'outline' | 'text''outline'Weight of the bubble’s surface. solid is the usual way to tell your own messages apart; side deliberately does not decide it
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Height and type scale
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. Arbitrary colour values are not accepted
densityshared'default' | 'compact''default'Padding inside the bubble, and nothing else
elevationshared0 | 1 | 2 | 30Drop shadow depth. 0 means no shadow at all
childrenReactNodeThe message

Every other <div> attribute passes through to the row, except title: a bubble has no headline, and the browser's tooltip on a whole message is rarely what anyone wants.

The shared axes (variant size color density elevation) are defined in prop conventions.

Examples

side

start is the default and end is the other party. It runs the row the other way (avatar, bubble and actions all flip), and cuts the corner nearest the speaker short, which is what says where the message came from without hanging a tail off the sheet.

variant is what tells your own messages from everyone else's, and it is deliberately not tied to side: filling the trailing column is a convention, not a law.

avatar, name and time

avatar takes an Avatar at whatever size the thread uses. name and time sit above the bubble as one line; leave both out on a follow-up message and the run reads as one turn rather than three.

status

Five steps: sending, sent, delivered, read and failed. Only the last two carry a colour: a thread where every message is marked in colour is a thread where the colour has stopped meaning anything.

The mark is the whole of what is drawn. The word behind it is read out but never shown; statusLabel replaces it.

typing

typing draws three dots in place of the message. children is left alone, so the same bubble goes back to the message the moment it arrives.

The dots light in sequence and never move: colour is the axis every indeterminate indicator in the library uses, and something bouncing in the corner of a thread being read is exactly what the design language has no time for.

media

media is drawn edge to edge above the text, so the bubble's own corners crop it. An <img> or <video> inside it is stretched to the width of the bubble; anything else is laid out as it comes.

preview

preview unfurls a link into a card under the text: url, title, description, image, site, and newTab for the ones that should leave the app. The card's surface is mixed out of the bubble's own text colour, so it works on a filled bubble and a bare one alike.

actions

actions sits beside the bubble: a Menu trigger, most of the time. It stays out of the way until the row is hovered or something in it takes focus, and is simply always there on a pointer that cannot hover.

locale

The delivery marks and the typing dots are read out as words, and locale is which language those words are in: a BCP 47 tag such as ko, pt-BR or zh-Hant. Tags with no translation fall back to English.

tsx
<ChatBubble side="end" status="read" locale="ko">
  방금 브랜치 올렸어요
</ChatBubble>

Accessibility

  • The status mark is a glyph with the word behind it in text only a screen reader reaches. Set locale, or write the word out in statusLabel.
  • A ChatBubble is one message, not a thread. Wrap the conversation in the markup the page needs: a list, or a container with role="log" for one that keeps updating.
  • media carries no alt of its own: pass a real <img alt="…">, or an empty one where the text beside it already says what the picture is.

Released under the MIT License