
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
- Flutter
- No external dependencies
Requires iOS 17
Requires Flutter 3.16


