Glass Tag Cloud
Tags of tinted, interactive Liquid Glass that melt together when they get close.

- Glass
- iOS 26.0+
- intermediate
- Origin
- Original work
- Found by
- tags · tag cloud · chips · flow layout · liquid glass · glass · merge · filter · etiquetas · nube de etiquetas · filtros · cristal líquido · cristal · fusionar · diseño fluido
Description
Section titled “Description”Tags of tinted, interactive Liquid Glass that melt together when they get close.
mergeDistance is the gap below which neighbouring tags blend. With Reduce Motion on, selecting a tag changes it without animating.
@available(iOS 26, *)struct GlassTagCloudDemo: View { @State private var selection: Set<String> = ["SwiftUI"] @State private var isTight = false @Environment(\.accessibilityReduceMotion) private var reduceMotion
private let tags = [ GlassTagCloudTag(title: "SwiftUI", tint: .blue), GlassTagCloudTag(title: "Animation", tint: .pink), GlassTagCloudTag(title: "Layout", tint: .orange), GlassTagCloudTag(title: "Glass", tint: .teal), GlassTagCloudTag(title: "Gestures", tint: .purple), GlassTagCloudTag(title: "Symbols", tint: .green), GlassTagCloudTag(title: "Charts", tint: .red), GlassTagCloudTag(title: "Navigation", tint: .indigo), GlassTagCloudTag(title: "Metal", tint: .mint), GlassTagCloudTag(title: "Widgets", tint: .yellow), ]
var body: some View { ZStack { LinearGradient(colors: [.indigo, .black], startPoint: .topLeading, endPoint: .bottomTrailing) .ignoresSafeArea()
// Scrolls only when Dynamic Type makes the content taller than the screen. ViewThatFits(in: .vertical) { content
ScrollView { content } } } }
private var content: some View { VStack(alignment: .leading, spacing: 32) { Text("Pick your interests") .font(.largeTitle.bold()) .foregroundStyle(.white)
GlassTagCloud(tags: tags, selection: $selection, spacing: isTight ? 3 : 18) .frame(maxWidth: .infinity, alignment: .leading)
Toggle("Tight spacing", isOn: $isTight.animation(reduceMotion ? nil : .smooth(duration: 0.7))) .font(.headline) .foregroundStyle(.white) } .padding(24) }}
@available(iOS 26, *)API reference
Section titled “API reference”| Parameter | Type | Default |
|---|---|---|
tags | [GlassTagCloudTag] | Required |
selection | Binding<Set<String>> | Required |
spacing | CGFloat | 16 |
mergeDistance | CGFloat | 10 |
Copy this code into your project, or download the file.
// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
@available(iOS 26, *)struct GlassTagCloudTag: Identifiable { let title: String let tint: Color
var id: String { title }}
/// Tags of tinted, interactive Liquid Glass that melt together when they get close.////// `mergeDistance` is the gap below which neighbouring tags blend. With Reduce Motion on,/// selecting a tag changes it without animating.@available(iOS 26, *)struct GlassTagCloud: View { let tags: [GlassTagCloudTag] @Binding var selection: Set<String> var spacing: CGFloat = 16 var mergeDistance: CGFloat = 10
@ScaledMetric(relativeTo: .subheadline) private var horizontalPadding = 16.0 @ScaledMetric(relativeTo: .subheadline) private var verticalPadding = 10.0 @Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View { GlassEffectContainer(spacing: mergeDistance) { GlassTagCloudLayout(spacing: spacing) { ForEach(tags) { tag in tagButton(tag) } } } }
private func tagButton(_ tag: GlassTagCloudTag) -> some View { let isSelected = selection.contains(tag.title)
return Button { withAnimation(reduceMotion ? nil : .snappy) { if isSelected { selection.remove(tag.title) } else { selection.insert(tag.title) } } } label: { Text(tag.title) .font(.subheadline.weight(.semibold)) .foregroundStyle(.white) .padding(.horizontal, horizontalPadding) .padding(.vertical, verticalPadding) // A little taller for a touch than it looks, up to the size of a touch target. .padding(.vertical, 3) .contentShape(.capsule) .padding(.vertical, -3) } .buttonStyle(.plain) .accessibilityAddTraits(isSelected ? .isSelected : []) .glassEffect(.regular.tint(tag.tint.opacity(isSelected ? 0.85 : 0.3)).interactive(), in: .capsule) }}
@available(iOS 26, *)private struct GlassTagCloudLayout: Layout { var spacing: CGFloat
var animatableData: CGFloat { get { spacing } set { spacing = newValue } }
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let sizes = subviews.map { $0.sizeThatFits(ProposedViewSize(width: proposal.width, height: nil)) } return GlassTagCloudFlow.arrange(sizes, width: proposal.width ?? .infinity, spacing: spacing).size }
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let tagProposal = ProposedViewSize(width: bounds.width, height: nil) let sizes = subviews.map { $0.sizeThatFits(tagProposal) } let origins = GlassTagCloudFlow.arrange(sizes, width: bounds.width, spacing: spacing).origins for (subview, origin) in zip(subviews, origins) { subview.place(at: CGPoint(x: bounds.minX + origin.x, y: bounds.minY + origin.y), proposal: tagProposal) } }}
@available(iOS 26, *)nonisolated enum GlassTagCloudFlow { /// Places items left to right, starting a new row when the next item would not fit in `width`. static func arrange(_ sizes: [CGSize], width: CGFloat, spacing: CGFloat) -> (origins: [CGPoint], size: CGSize) { var origins: [CGPoint] = [] var cursor = CGPoint.zero var rowHeight: CGFloat = 0 var usedWidth: CGFloat = 0
for size in sizes { if cursor.x > 0, cursor.x + size.width > width { cursor = CGPoint(x: 0, y: cursor.y + rowHeight + spacing) rowHeight = 0 } origins.append(cursor) usedWidth = max(usedWidth, cursor.x + size.width) rowHeight = max(rowHeight, size.height) cursor.x += size.width + spacing }
return (origins, CGSize(width: usedWidth, height: cursor.y + rowHeight)) }}
// MARK: - Demo
@available(iOS 26, *)struct GlassTagCloudDemo: View { @State private var selection: Set<String> = ["SwiftUI"] @State private var isTight = false @Environment(\.accessibilityReduceMotion) private var reduceMotion
private let tags = [ GlassTagCloudTag(title: "SwiftUI", tint: .blue), GlassTagCloudTag(title: "Animation", tint: .pink), GlassTagCloudTag(title: "Layout", tint: .orange), GlassTagCloudTag(title: "Glass", tint: .teal), GlassTagCloudTag(title: "Gestures", tint: .purple), GlassTagCloudTag(title: "Symbols", tint: .green), GlassTagCloudTag(title: "Charts", tint: .red), GlassTagCloudTag(title: "Navigation", tint: .indigo), GlassTagCloudTag(title: "Metal", tint: .mint), GlassTagCloudTag(title: "Widgets", tint: .yellow), ]
var body: some View { ZStack { LinearGradient(colors: [.indigo, .black], startPoint: .topLeading, endPoint: .bottomTrailing) .ignoresSafeArea()
// Scrolls only when Dynamic Type makes the content taller than the screen. ViewThatFits(in: .vertical) { content
ScrollView { content } } } }
private var content: some View { VStack(alignment: .leading, spacing: 32) { Text("Pick your interests") .font(.largeTitle.bold()) .foregroundStyle(.white)
GlassTagCloud(tags: tags, selection: $selection, spacing: isTight ? 3 : 18) .frame(maxWidth: .infinity, alignment: .leading)
Toggle("Tight spacing", isOn: $isTight.animation(reduceMotion ? nil : .smooth(duration: 0.7))) .font(.headline) .foregroundStyle(.white) } .padding(24) }}
@available(iOS 26, *)#Preview { GlassTagCloudDemo()}