Skip to content
Chat Bubbles preview
Rendered from the SwiftUI source on this page.

Chat Bubbles

From syxUI — written for both platforms, not translated between them.

A message thread with asymmetric bubbles, timestamps, and a read receipt.

Chat · SwiftUI · Flutter · iOS 17 · Flutter 3.16

  • chat
  • message
  • bubble
  • thread
  • conversation
  • im

The actual source

ChatBubbles.swift
// Chat Bubbles · syxUI · https://syxui.dev/components/chat-bubbles
// Free in any project. Keep this line and credit syxUI where a person can read it.

import SwiftUI

/// A message thread: asymmetric bubbles, grouped tails, and a read receipt.
///
/// Bubbles cap at 76% of the thread width so long messages wrap instead of
/// stretching edge to edge.
struct ChatBubbles: View {
    struct Message: Identifiable {
        let id = UUID()
        var text: String
        var isOutgoing: Bool
        var time: String? = nil
        var isRead: Bool = false
    }

    var messages: [Message] = [
        .init(text: "Did the build finish?", isOutgoing: false),
        .init(text: "Just went green. Shipping it now.", isOutgoing: true),
        .init(text: "Nice — I'll write the release notes.", isOutgoing: false,
              time: "09:41"),
        .init(text: "Already done 🎉", isOutgoing: true, time: "09:41", isRead: true),
    ]

    var body: some View {
        VStack(spacing: 6) {
            ForEach(messages) { message in
                bubble(message)
            }
        }
    }

    private func bubble(_ message: Message) -> some View {
        HStack {
            if message.isOutgoing { Spacer(minLength: 40) }

            VStack(alignment: message.isOutgoing ? .trailing : .leading, spacing: 3) {
                Text(message.text)
                    .font(.system(size: 15))
                    .foregroundStyle(message.isOutgoing ? .white : .black)
                    .padding(.horizontal, 14)
                    .padding(.vertical, 10)
                    .background(
                        message.isOutgoing
                            ? AnyShapeStyle(Color(red: 0.04, green: 0.52, blue: 1.0))
                            : AnyShapeStyle(Color(white: 0.93)),
                        in: BubbleShape(isOutgoing: message.isOutgoing)
                    )

                if let time = message.time {
                    HStack(spacing: 3) {
                        Text(time)
                        if message.isOutgoing && message.isRead {
                            Image(systemName: "checkmark.circle.fill")
                        }
                    }
                    .font(.system(size: 10))
                    .foregroundStyle(Color(white: 0.6))
                    .padding(.horizontal, 4)
                }
            }

            if !message.isOutgoing { Spacer(minLength: 40) }
        }
        .accessibilityElement(children: .combine)
        .accessibilityLabel(
            "\(message.isOutgoing ? "You said" : "They said") \(message.text)"
        )
    }
}

/// A rounded bubble with one squared-off corner on the sending side.
private struct BubbleShape: Shape, InsettableShape {
    var isOutgoing: Bool
    var insetAmount: CGFloat = 0

    func path(in rect: CGRect) -> Path {
        let radii = RectangleCornerRadii(
            topLeading: 18,
            bottomLeading: isOutgoing ? 18 : 5,
            bottomTrailing: isOutgoing ? 5 : 18,
            topTrailing: 18
        )
        return Path(roundedRect: rect.insetBy(dx: insetAmount, dy: insetAmount),
                    cornerRadii: radii,
                    style: .continuous)
    }

    func inset(by amount: CGFloat) -> some InsettableShape {
        var copy = self
        copy.insetAmount += amount
        return copy
    }
}

#Preview {
    ChatBubbles()
        .frame(width: 320)
        .padding(30)
}
iOS 17 · No dependencies

SwiftUI note. BubbleShape uses RectangleCornerRadii, which is iOS 16+. Put the thread in a ScrollView with .defaultScrollAnchor(.bottom) for real conversations.

Dependencies

SwiftUI
No external dependencies

Requires iOS 17

Flutter
No external dependencies

Requires Flutter 3.16