Fliin
On this page

Blocks / messaging

Message bubble

Shows one message of a chat thread: sent at the end, received at the start, grouped by author, with the quoted reply, the time, and sending, sent, read or failed with retry.

16 of 16 variants pass
Message bubble, received state
Platform
Brightness
Text scale
Direction

Import

Message bubble is part of Fliin Pro. With a license key, sign in once with fliin login, then add it in a Flutter app where fliin init has run:

fliin login
fliin add message-bubble

Also installs: core, button, icons, link-button, menu

Examples

Received

A message from someone else: outgoing: false puts it on the start side, and showFooter adds its time. Its actions open on a long press.

Message bubble, received state
import 'package:your_app/ui/fliin/components/message_bubble/fl_message_bubble.dart';
import 'package:flutter/widgets.dart';

class MessageBubbleReceivedExample extends StatelessWidget {
  const MessageBubbleReceivedExample({super.key});

  @override
  Widget build(BuildContext context) {
    return FlMessageBubble(
      text: 'Shall we go back this weekend?',
      outgoing: false,
      author: 'Leah Bernard',
      time: '10:25',
      showFooter: true,
      actions: [FlMessageAction(label: 'Copy', onPressed: () {})],
      actionsLabel: 'Actions',
    );
  }
}

Group author

Two messages in a row from the same person, as in a group: the first shows the author, the last the time, and position joins their corners.

Message bubble, group-author state
import 'package:your_app/ui/fliin/components/message_bubble/fl_message_bubble.dart';
import 'package:flutter/widgets.dart';

class MessageBubbleGroupAuthorExample extends StatelessWidget {
  const MessageBubbleGroupAuthorExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        FlMessageBubble(
          text: 'Shall we go back this weekend?',
          outgoing: false,
          author: 'Leah Bernard',
          time: '10:25',
          showAuthor: true,
          position: FlMessageGroupPosition.first,
          actions: [FlMessageAction(label: 'Copy', onPressed: () {})],
          actionsLabel: 'Actions',
        ),
        FlMessageBubble(
          text: 'I can pick you up around 7.',
          outgoing: false,
          author: 'Leah Bernard',
          time: '10:25',
          showFooter: true,
          position: FlMessageGroupPosition.last,
          actions: [FlMessageAction(label: 'Copy', onPressed: () {})],
          actionsLabel: 'Actions',
        ),
      ],
    );
  }
}

Reply

The user's answer to an earlier message: quote shows what it replies to, and status that it was sent.

Message bubble, reply state
import 'package:your_app/ui/fliin/components/message_bubble/fl_message_bubble.dart';
import 'package:flutter/widgets.dart';

class MessageBubbleReplyExample extends StatelessWidget {
  const MessageBubbleReplyExample({super.key});

  @override
  Widget build(BuildContext context) {
    return FlMessageBubble(
      text: 'Perfect for Friday!',
      outgoing: true,
      author: 'You',
      time: '10:27',
      status: FlMessageStatus.sent,
      statusLabel: 'Sent',
      showFooter: true,
      quote: const FlMessageQuote(author: 'Leah Bernard', text: 'I can pick you up around 7.'),
      actions: [FlMessageAction(label: 'Copy', onPressed: () {})],
      actionsLabel: 'Actions',
    );
  }
}

Read

FlMessageStatus.read once the other person has seen the message.

Message bubble, read state
import 'package:your_app/ui/fliin/components/message_bubble/fl_message_bubble.dart';
import 'package:flutter/widgets.dart';

class MessageBubbleReadExample extends StatelessWidget {
  const MessageBubbleReadExample({super.key});

  @override
  Widget build(BuildContext context) {
    return FlMessageBubble(
      text: 'Perfect for Friday!',
      outgoing: true,
      author: 'You',
      time: '10:27',
      status: FlMessageStatus.read,
      statusLabel: 'Read',
      showFooter: true,
      actions: [FlMessageAction(label: 'Copy', onPressed: () {})],
      actionsLabel: 'Actions',
    );
  }
}

Sending

A message on its way: FlMessageStatus.sending, with no actions yet.

Message bubble, sending state
import 'package:your_app/ui/fliin/components/message_bubble/fl_message_bubble.dart';
import 'package:flutter/widgets.dart';

class MessageBubbleSendingExample extends StatelessWidget {
  const MessageBubbleSendingExample({super.key});

  @override
  Widget build(BuildContext context) {
    return const FlMessageBubble(
      text: 'I’ll bring the picnic.',
      outgoing: true,
      author: 'You',
      time: '10:28',
      status: FlMessageStatus.sending,
      statusLabel: 'Sending…',
      showFooter: true,
    );
  }
}

Failed

A message that could not be sent: FlMessageStatus.failed and onRetry let the user send it again.

Message bubble, failed state
import 'package:your_app/ui/fliin/components/message_bubble/fl_message_bubble.dart';
import 'package:flutter/widgets.dart';

class MessageBubbleFailedExample extends StatelessWidget {
  const MessageBubbleFailedExample({super.key});

  @override
  Widget build(BuildContext context) {
    return FlMessageBubble(
      text: 'Do you have the key?',
      outgoing: true,
      author: 'You',
      time: '10:29',
      status: FlMessageStatus.failed,
      statusLabel: 'Not sent',
      onRetry: () {},
      retryLabel: 'Retry',
      actions: [FlMessageAction(label: 'Copy', onPressed: () {})],
      actionsLabel: 'Actions',
    );
  }
}

Reactions

Reactions under a bubble, each a pill the user taps to add or remove their own (selected), and editedLabel after the time of a message changed since it was sent.

Message bubble, reactions state
import 'package:your_app/ui/fliin/components/message_bubble/fl_message_bubble.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:flutter/widgets.dart';

class MessageBubbleReactionsExample extends StatefulWidget {
  const MessageBubbleReactionsExample({super.key});

  @override
  State<MessageBubbleReactionsExample> createState() => _MessageBubbleReactionsExampleState();
}

class _MessageBubbleReactionsExampleState extends State<MessageBubbleReactionsExample> {
  var _hearted = true;

  @override
  Widget build(BuildContext context) {
    final hearts = _hearted ? 3 : 2;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        FlMessageBubble(
          text: 'The tickets are booked, we leave at 8:12 from platform 4.',
          outgoing: false,
          author: 'Leah Bernard',
          time: '10:25',
          editedLabel: 'Edited',
          showFooter: true,
          reactions: [
            FlMessageReaction(
              icon: FlIcon(FlIcons.heartFill),
              text: '$hearts',
              semanticLabel: _hearted ? 'Heart, $hearts, including you' : 'Heart, $hearts',
              selected: _hearted,
              onPressed: () => setState(() => _hearted = !_hearted),
            ),
            FlMessageReaction(icon: FlIcon(FlIcons.starFill), text: '1', semanticLabel: 'Star, 1', onPressed: () {}),
          ],
        ),
        FlMessageBubble(
          text: 'Perfect, see you there!',
          outgoing: true,
          author: 'You',
          time: '10:27',
          status: FlMessageStatus.read,
          statusLabel: 'Read',
          showFooter: true,
          reactions: [FlMessageReaction(icon: FlIcon(FlIcons.check), text: '1', semanticLabel: 'Check, 1')],
        ),
      ],
    );
  }
}

Usage

The code the reference images render:

final example = Builder(
  builder: (context) {
    final theme = FlScope.of(context);
    final actions = [FlMessageAction(label: 'Copy', onPressed: () {})];
    final actionsLabel = 'Actions';
    return ColoredBox(
      color: theme.colors.surface,
      child: SingleChildScrollView(
        padding: EdgeInsetsDirectional.all(theme.space.md),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            FlMessageBubble(
              text: 'Shall we go back this weekend?',
              outgoing: false,
              author: 'Leah Bernard',
              time: '10:25',
              showAuthor: true,
              position: FlMessageGroupPosition.first,
              actions: actions,
              actionsLabel: actionsLabel,
            ),
            FlMessageBubble(
              text: 'I can pick you up around 7.',
              outgoing: false,
              author: 'Leah Bernard',
              time: '10:25',
              showFooter: true,
              position: FlMessageGroupPosition.last,
              actions: actions,
              actionsLabel: actionsLabel,
            ),
            FlMessageBubble(
              text: 'Friday works!',
              outgoing: true,
              author: 'You',
              time: '10:27',
              status: FlMessageStatus.read,
              statusLabel: 'Read',
              showFooter: true,
              quote: FlMessageQuote(
                author: 'Leah Bernard',
                text: 'I can pick you up around 7.',
              ),
              actions: actions,
              actionsLabel: actionsLabel,
            ),
            FlMessageBubble(
              text: 'I’ll bring the picnic.',
              outgoing: true,
              author: 'You',
              time: '10:28',
              status: FlMessageStatus.sending,
              statusLabel: 'Sending…',
              showFooter: true,
            ),
            FlMessageBubble(
              text: 'Do you have the key to the shed?',
              outgoing: true,
              author: 'You',
              time: '10:29',
              status: FlMessageStatus.failed,
              statusLabel: 'Not sent',
              onRetry: () {},
              retryLabel: 'Try again',
              actions: actions,
              actionsLabel: actionsLabel,
            ),
          ],
        ),
      ),
    );
  },
);

API

FlMessageStatus: enum {sending, sent, read, failed}
FlMessageStatus.sending: static final FlMessageStatus
FlMessageStatus.sent: static final FlMessageStatus
FlMessageStatus.read: static final FlMessageStatus
FlMessageStatus.failed: static final FlMessageStatus
FlMessageGroupPosition: enum {single, first, middle, last}
FlMessageGroupPosition.single: static final FlMessageGroupPosition
FlMessageGroupPosition.first: static final FlMessageGroupPosition
FlMessageGroupPosition.middle: static final FlMessageGroupPosition
FlMessageGroupPosition.last: static final FlMessageGroupPosition
FlMessageQuote: final class
FlMessageQuote.new: const new({required String author, required String text})
FlMessageQuote.author: get String
FlMessageQuote.text: get String
FlMessageAction: final class
FlMessageAction.new: const new({required String label, required VoidCallback onPressed, bool destructive})
FlMessageAction.label: get String
FlMessageAction.onPressed: get VoidCallback
FlMessageAction.destructive: get bool
FlMessageReaction: final class
FlMessageReaction.new: const new({required String text, required String semanticLabel, Widget? icon, bool selected, VoidCallback? onPressed})
FlMessageReaction.text: get String
FlMessageReaction.icon: get Widget?
FlMessageReaction.semanticLabel: get String
FlMessageReaction.selected: get bool
FlMessageReaction.onPressed: get VoidCallback?
FlMessageBubble: class extends StatefulWidget
FlMessageBubble.new: const new({super.key, required String text, required bool outgoing, required String author, required String time, FlMessageStatus? status, String? statusLabel, bool showAuthor, bool showFooter, FlMessageQuote? quote, FlMessageGroupPosition position, VoidCallback? onRetry, String? retryLabel, List<FlMessageAction> actions, String? actionsLabel, List<FlMessageReaction> reactions, String? editedLabel})
FlMessageBubble.text: get String
FlMessageBubble.outgoing: get bool
FlMessageBubble.author: get String
FlMessageBubble.time: get String
FlMessageBubble.status: get FlMessageStatus?
FlMessageBubble.statusLabel: get String?
FlMessageBubble.showAuthor: get bool
FlMessageBubble.showFooter: get bool
FlMessageBubble.quote: get FlMessageQuote?
FlMessageBubble.position: get FlMessageGroupPosition
FlMessageBubble.onRetry: get VoidCallback?
FlMessageBubble.retryLabel: get String?
FlMessageBubble.actions: get List<FlMessageAction>
FlMessageBubble.actionsLabel: get String?
FlMessageBubble.reactions: get List<FlMessageReaction>
FlMessageBubble.editedLabel: get String?
FlMessageBubble.createState: State<FlMessageBubble>()

Reference renders

The 16 images published with version 0.2.5, rendered by the shared harness on macOS with Flutter 3.47.7. fliin verify message-bubble compares your copy to them.

iOS · Light
Message bubble, iOS, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Message bubble, iOS, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Message bubble, iOS, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Message bubble, iOS, Light, text 2.0×, Right to left
Text 2.0× · Right to left
iOS · Dark
Message bubble, iOS, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Message bubble, iOS, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Message bubble, iOS, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Message bubble, iOS, Dark, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Light
Message bubble, Android, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Message bubble, Android, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Message bubble, Android, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Message bubble, Android, Light, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Dark
Message bubble, Android, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Message bubble, Android, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Message bubble, Android, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Message bubble, Android, Dark, text 2.0×, Right to left
Text 2.0× · Right to left

Use it to

  • show a sent or received chat message in a bubble
  • show whether a message went out, was read or failed
  • offer a retry on a message whose delivery failed
  • show the quoted message a reply answers above it
  • let people copy or delete a message on a long press
Not for
  • a brief message that disappears on its own: use a toast (toast)
  • a status message in a page: use an alert (alert)
States
  • Received
  • Group author
  • Reply
  • Read
  • Sending
  • Failed

Arrows to move, Enter to open, Escape to close.