Skip to content

PlChatBubble

One message in a conversation. Everything around the bubble is optional, and side decides only which way the row runs and which corner of the sheet is cut short.

React
tsx
import { PlChatBubble } from 'plass-ui';

<PlChatBubble name="Ada Lovelace" time="09:12" avatar={<PlAvatar name="Ada Lovelace" />}>
  Have a look at the new fills.
</PlChatBubble>;

<PlChatBubble side="end" variant="solid" status="read">
  Already did.
</PlChatBubble>;
dart
import 'package:plass_ui/plass_ui.dart';

const PlChatBubble(
  name: Text('Ada Lovelace'),
  time: Text('09:12'),
  avatar: PlAvatar(name: 'Ada Lovelace'),
  child: Text('Have a look at the new fills.'),
);

const PlChatBubble(
  side: PlChatBubbleSide.end,
  variant: PlassVariant.solid,
  status: PlChatBubbleStatus.read,
  child: Text('Already did.'),
);

Props

PropTypeDefaultDescription
sideshared'start' | 'end''start'Whose message this is. Not them/me or left/right, because a thread runs the way the language does. It decides which way the row runs and which corner of the sheet is cut short
variantshared'solid' | 'glass' | 'ghost''glass'What the bubble’s surface is made of. A bubble is the thing being coloured, so solid floods it and the text switches to --p-on-solid. Deliberately not tied to side: which end is filled is a decision about the product, not about the component
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Type scale and padding of the bubble
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role
densityshared'default' | 'compact''default'Padding inside the bubble, and nothing else
elevationshared0 | 1 | 2 | 30Drop shadow depth. A message lies in the thread rather than floating over it
nameReactNodeWho sent it, above the bubble
timeReactNodeWhen it was sent, beside the name
avatarReactNodeThe sender’s picture — a PlAvatar at the size the thread uses. 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: a received message has no delivery state worth showing
statusLabelstringWhat the mark is read out as. Never drawn
typingbooleanfalseDraws the three dots instead of the message. What children holds is left alone, so the same bubble can go back to it when the message arrives
typingLabelstring'Typing…'What the dots are read out as. Never drawn
mediaReactNodeA picture, a video, a map — drawn edge to edge above the text, so the bubble’s corners crop it
previewPlChatBubbleLinkPreviewA link in the message, unfurled into a card under the text
actionsReactNodeThe message’s own actions. Sits beside the bubble and stays out of the way until the row is hovered or something in it takes focus
childrenReactNodeThe message
PropTypeDefaultDescription
sidesharedPlChatBubbleSidePlChatBubbleSide.startWhose message this is. Not them/me or left/right, because a thread runs the way the language does. It decides which way the row runs and which corner of the sheet is cut short
variantsharedPlassVariantPlassVariant.glassWhat the bubble’s surface is made of. A bubble is the thing being coloured, so solid floods it and the text switches to --p-on-solid. Deliberately not tied to side: which end is filled is a decision about the product, not about the component
sizesharedPlassSizePlassSize.mdType scale and padding of the bubble
colorsharedPlassColorPlassColor.primarySemantic colour role
densitysharedPlassDensityPlassDensity.standardPadding inside the bubble, and nothing else
elevationsharedint0Drop shadow depth. A message lies in the thread rather than floating over it
nameWidget?Who sent it, above the bubble
timeWidget?When it was sent, beside the name
avatarWidget?The sender’s picture — a PlAvatar at the size the thread uses. Left out, the bubble takes the whole row
statusPlChatBubbleStatus?How far the message has got, drawn as a mark under the bubble. Left out, nothing is drawn: a received message has no delivery state worth showing
statusLabelString?What the mark is read out as. Never drawn
typingboolfalseDraws the three dots instead of the message. What child holds is left alone, so the same bubble can go back to it when the message arrives
typingLabelString'Typing…'What the dots are read out as. Never drawn
mediaWidget?A picture, a video, a map — drawn edge to edge above the text, so the bubble’s corners crop it
previewPlChatBubbleLinkPreview?A link in the message, unfurled into a card under the text
actionsWidget?The message's own actions. Sits beside the bubble and stays there: there is no screen here that certainly has a pointer
childWidget?The message

Every native <div> attribute passes straight through, onto the row. color and title are excluded from the pass-through because both are Plass props here.

The row runs the other way for the reader's own message by reversing the direction, not the list, so a thread in Arabic is mirrored without being told twice, and the column inside is set back the right way up, or everything in it would be mirrored along with the row.

PlChatBubbleLinkPreview

PropTypeDefaultDescription
url * stringWhere the card goes
titleReactNodeThe page’s title
descriptionReactNodeIts summary, clamped to two lines
imagestringThe share image, drawn across the top of the card
siteReactNodeWho published it — a domain, a site name
newTabbooleanfalseOpens the card in a new tab, with the rel that protects the opener
PropTypeDefaultDescription
onPressedVoidCallback?Called when the card is pressed. Not a url, because Flutter has no navigation of its own
titleWidget?The page’s title
descriptionWidget?Its summary, clamped to two lines
imageImageProvider?The share image, drawn across the top of the card — the shape every image in Flutter has
siteWidget?Who published it — a domain, a site name

What the shared axes (side variant size color density elevation) mean across the library is in prop conventions.

Examples

side

start and end rather than them/me or left/right: a thread runs the way the language does, and the same two words already mean this everywhere else in the library.

The corner nearest the speaker is cut short. That is the library's one piece of chat vocabulary, and it does the job a drawn tail does elsewhere, without hanging a triangle off a sheet of glass that is supposed to have been cut with a straight edge. It is a flat 4px at every size, because the radius ladder only runs 8px to 16px and a four-pixel difference is not something anyone reads as meaning something.

React

variant

A bubble is the thing being coloured, unlike a PlCard, which holds other people's content and so keeps its sheet undyed, so solid floods it and the text switches to the family's own ink. That is what makes a column of your own messages read as yours at a glance rather than one line at a time.

It is deliberately not tied to side. Filling the right-hand column is a convention, not a law, and a thread that fills neither is a perfectly good thread.

React

status

Only two of the five carry a colour: the one that arrived and the one that did not. The three in between are the ordinary course of events, and a thread where every message is marked in colour is a thread where the colour has stopped meaning anything.

failed is not a fifth step on the ladder. It is the message that did not go, which is why it is the only one drawn in another family.

React

typing

Three dots that light in sequence, in colour only. The dots never move, so a bubble being typed into does not bounce in a thread somebody is reading.

What childrenchild holds is left alone, so the same bubble goes back to the message when it arrives.

media and preview

media is drawn edge to edge above the text, so the bubble's own corners crop it, which is why a bubble's padding lives on its sections rather than on the sheet.

The link card's surface is mixed out of `currentColor`the bubble's own ink rather than out of a token, because it is the one part of a bubble that has to work on both a filled surface and a bare one: on solid the text is white and the card is a white wash, on glass the text is the page's ink and the card is a grey one. A fixed token would be invisible against one of the two.

preview takes an onPressed rather than a url, and an ImageProvider rather than a src. Flutter has no navigation of its own, so where a link goes is the app's, the same trade a PlTextLink makes.

React

actions

The handle stays out of the way of the message until the row is reached for. It would otherwise sit in the middle of a conversation being read, and a pointer that cannot hover has nothing to reveal it, so on touch it is simply always there.

The handle sits beside the message and stays there. The React build fades it in on hover and reveals it unconditionally where hover does not exist; here there is no such thing as a screen that certainly has a pointer, so the honest answer is the one that is always reachable.

React

size

React

Accessibility

  • The delivery mark is the whole of what is drawn, and the word behind it is in a visually hidden box, for the readers a double tick says nothing to. statusLabel is what changes that word.
  • The typing dots are a role="status", so a message being written is announced once rather than on every frame.
  • The link card is a real <a>, and newTab brings the rel that stops the new page reaching back through window.opener.
  • media should carry its own alt. The component does not know what the picture is of.
  • The bubble adds no role of its own. A thread is a list, and the list belongs to the page, which is what lets a virtualised one still be one.
  • The delivery mark is the whole of what is drawn, and the word behind it is the mark's name, for the readers a double tick says nothing to. statusLabel is what changes that word.
  • The typing dots are a live region with a name, so a message being written is announced once rather than on every frame. They light in colour only and never move, so a bubble being typed into does not bounce in a thread somebody is reading.
  • The link card is announced as a link and answers a press from the keyboard, with the focus ring drawn round it.
  • media should carry its own name. The component does not know what the picture is of.
  • The bubble adds no role of its own. A thread is a list, and the list belongs to the page, which is what lets a lazily built one still be one.

Differences from the React build

ReactFlutterWhy
preview.url and newTabpreview.onPressedFlutter has no navigation of its own, so where a link goes is the app's.
preview.image as a srcan ImageProviderThe shape every image in Flutter has: an asset, a file, a network URL, or something drawn.
childrenchildFlutter's name.
the handle fades in on hoverthe handle is always thereThere is no screen here that certainly has a pointer, so the reachable answer is the honest one.
role="status" on the dotsa named live regionFlutter names the state on the node itself.
className, style, native attributesThere is no class list and no style attribute to pass through.

Released under the MIT License