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.
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>;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
| Prop | Type | Default | Description |
|---|---|---|---|
| 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 |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. A message lies in the thread rather than floating over it |
| name | ReactNode | — | Who sent it, above the bubble |
| time | ReactNode | — | When it was sent, beside the name |
| avatar | ReactNode | — | The 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 |
| statusLabel | string | — | What the mark is read out as. Never drawn |
| typing | boolean | false | Draws 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 |
| typingLabel | string | 'Typing…' | What the dots are read out as. Never drawn |
| media | ReactNode | — | A picture, a video, a map — drawn edge to edge above the text, so the bubble’s corners crop it |
| preview | PlChatBubbleLinkPreview | — | A link in the message, unfurled into a card under the text |
| actions | ReactNode | — | The 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 |
| children | ReactNode | — | The message |
| Prop | Type | Default | Description |
|---|---|---|---|
| sideshared | PlChatBubbleSide | PlChatBubbleSide.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 | PlassVariant | PlassVariant.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 | PlassSize | PlassSize.md | Type scale and padding of the bubble |
| colorshared | PlassColor | PlassColor.primary | Semantic colour role |
| densityshared | PlassDensity | PlassDensity.standard | Padding inside the bubble, and nothing else |
| elevationshared | int | 0 | Drop shadow depth. A message lies in the thread rather than floating over it |
| name | Widget? | — | Who sent it, above the bubble |
| time | Widget? | — | When it was sent, beside the name |
| avatar | Widget? | — | The sender’s picture — a PlAvatar at the size the thread uses. Left out, the bubble takes the whole row |
| status | PlChatBubbleStatus? | — | 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 |
| statusLabel | String? | — | What the mark is read out as. Never drawn |
| typing | bool | false | Draws 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 |
| typingLabel | String | 'Typing…' | What the dots are read out as. Never drawn |
| media | Widget? | — | A picture, a video, a map — drawn edge to edge above the text, so the bubble’s corners crop it |
| preview | PlChatBubbleLinkPreview? | — | A link in the message, unfurled into a card under the text |
| actions | Widget? | — | The message's own actions. Sits beside the bubble and stays there: there is no screen here that certainly has a pointer |
| child | Widget? | — | 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
| Prop | Type | Default | Description |
|---|---|---|---|
| url * | string | — | Where the card goes |
| title | ReactNode | — | The page’s title |
| description | ReactNode | — | Its summary, clamped to two lines |
| image | string | — | The share image, drawn across the top of the card |
| site | ReactNode | — | Who published it — a domain, a site name |
| newTab | boolean | false | Opens the card in a new tab, with the rel that protects the opener |
| Prop | Type | Default | Description |
|---|---|---|---|
| onPressed | VoidCallback? | — | Called when the card is pressed. Not a url, because Flutter has no navigation of its own |
| title | Widget? | — | The page’s title |
| description | Widget? | — | Its summary, clamped to two lines |
| image | ImageProvider? | — | The share image, drawn across the top of the card — the shape every image in Flutter has |
| site | Widget? | — | 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.
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.
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.
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.
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.
size
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.
statusLabelis 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>, andnewTabbrings therelthat stops the new page reaching back throughwindow.opener. mediashould carry its ownalt. 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.
statusLabelis 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.
mediashould 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
| React | Flutter | Why |
|---|---|---|
preview.url and newTab | preview.onPressed | Flutter has no navigation of its own, so where a link goes is the app's. |
preview.image as a src | an ImageProvider | The shape every image in Flutter has: an asset, a file, a network URL, or something drawn. |
children | child | Flutter's name. |
| the handle fades in on hover | the handle is always there | There is no screen here that certainly has a pointer, so the reachable answer is the honest one. |
role="status" on the dots | a named live region | Flutter names the state on the node itself. |
className, style, native attributes | — | There is no class list and no style attribute to pass through. |