Liquid glass containers
AI coding agent skills for building iOS 26 Liquid Glass UIs with SwiftUI + UIKit. Install: npx skills add SohrabZ/liquid-glass-skills
npx -y skills add SohrabZ/liquid-glass-skills --skill liquid-glass-containersAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
3 things to look at
- 16 days oldThe repository was created 16 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Combine, group, and morph multiple Liquid Glass elements in iOS 26 SwiftUI. Covers GlassEffectContainer (required whenever you have more than one glass element — glass cannot sample other glass), morphing transitions with glassEffectID + a shared @Namespace, glassEffectUnion for merging distant elements, and glassEffectTransition (.identity / .matchedGeometry / .materialize). Use this skill when the user has two or more glass views, wants glass buttons/icons that "blend", "merge", or "morph" into each other, is building an expandable/collapsing cluster of glass controls, asks "why does my glass look wrong with multiple elements", "how do I animate between glass states", mentions "GlassEffectContainer", "glassEffectID", "glassEffectUnion", "matchedGeometry" for glass, or needs fluid transitions between glass shapes.
SKILL.md
6.9 KB, ~1.3k tokens by cl100k_base, as published. Nobody here has run it
Liquid Glass Containers, Morphing & Unions
The core constraint: glass cannot sample other glass. Any time you have more than one glass
element near each other, wrap them in a GlassEffectContainer. It provides a shared sampling
region, improves rendering performance, and is what enables morphing between elements. (Prereqs:
see liquid-glass-foundations for .glassEffect() basics.)
GlassEffectContainer
GlassEffectContainer {
HStack(spacing: 20) {
Image(systemName: "pencil")
.frame(width: 44, height: 44)
.glassEffect(.regular.interactive())
Image(systemName: "eraser")
.frame(width: 44, height: 44)
.glassEffect(.regular.interactive())
}
}
The spacing: parameter is the morphing threshold — glass elements within this distance
visually blend and morph together during transitions:
GlassEffectContainer(spacing: 40.0) {
ForEach(icons) { icon in
IconView(icon).glassEffect()
}
}
struct GlassEffectContainer<Content: View>: View {
init(spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
init(@ViewBuilder content: () -> Content)
}
Morphing transitions with glassEffectID
Requirements for morphing:
- Elements live in the same
GlassEffectContainer. - Each view has a
glassEffectIDsharing one@Namespace. - Views are conditionally shown/hidden to trigger the morph.
- The state change is wrapped in
withAnimation.
struct MorphingExample: View {
@State private var isExpanded = false
@Namespace private var namespace
var body: some View {
GlassEffectContainer(spacing: 30) {
Button(isExpanded ? "Collapse" : "Expand") {
withAnimation(.bouncy) { isExpanded.toggle() }
}
.glassEffect()
.glassEffectID("toggle", in: namespace)
if isExpanded {
Button("Action 1") { }
.glassEffect()
.glassEffectID("action1", in: namespace)
Button("Action 2") { }
.glassEffect()
.glassEffectID("action2", in: namespace)
}
}
}
}
func glassEffectID<ID: Hashable>(_ id: ID, in namespace: Namespace.ID) -> some View
Expandable action menu — a toggle that's always visible, surrounded by buttons that morph in
and out. Each conditional button gets its own glassEffectID; the toggle keeps a stable id:
struct ActionButtonsView: View {
@State private var showActions = false
@Namespace private var namespace
var body: some View {
ZStack {
Image("background").resizable().ignoresSafeArea()
GlassEffectContainer(spacing: 30) {
VStack(spacing: 30) {
if showActions {
actionButton("rotate.right").glassEffectID("rotate", in: namespace)
}
HStack(spacing: 30) {
if showActions {
actionButton("circle.lefthalf.filled").glassEffectID("contrast", in: namespace)
}
actionButton(showActions ? "xmark" : "slider.horizontal.3") {
withAnimation(.bouncy) { showActions.toggle() }
}
.glassEffectID("toggle", in: namespace)
if showActions {
actionButton("flip.horizontal").glassEffectID("flip", in: namespace)
}
}
if showActions {
actionButton("crop").glassEffectID("crop", in: namespace)
}
}
}
}
}
@ViewBuilder
func actionButton(_ systemImage: String, action: (() -> Void)? = nil) -> some View {
Button { action?() } label: {
Image(systemName: systemImage).frame(width: 44, height: 44)
}
.buttonStyle(.glass)
.buttonBorderShape(.circle)
}
}
glassEffectUnion — merge distant elements
When elements are too far apart to merge via spacing alone, force them into one visual glass
shape. All conditions must hold: same id, same glass effect type, similar shapes.
func glassEffectUnion<ID: Hashable>(id: ID, namespace: Namespace.ID) -> some View
struct UnionExample: View {
@Namespace var controls
var body: some View {
GlassEffectContainer {
VStack(spacing: 0) {
Button("Edit") { }
.buttonStyle(.glass)
.glassEffectUnion(id: "tools", namespace: controls)
Spacer().frame(height: 100) // large gap, still unified
Button("Delete") { }
.buttonStyle(.glass)
.glassEffectUnion(id: "tools", namespace: controls)
}
}
}
}
Use different ids to form subgroups — here the first 3 icons blend, the 4th floats separately:
GlassEffectContainer {
ForEach(0..<4) { index in
Image(systemName: icons[index])
.frame(width: 70, height: 70)
.glassEffect()
.glassEffectUnion(id: index < 3 ? "group1" : "group2", namespace: glassNamespace)
}
}
glassEffectTransition
Controls how a glass element appears/disappears during a morph.
func glassEffectTransition(_ transition: GlassEffectTransition, isEnabled: Bool = true) -> some View
enum GlassEffectTransition {
case identity // no changes
case matchedGeometry // matched-geometry transition (default)
case materialize // material appearance transition
}
Button("Action") { }
.glassEffect()
.glassEffectID("button", in: namespace)
.glassEffectTransition(.materialize)
Related skills
- Applying glass to standard components (buttons/toolbars/tabs) → liquid-glass-controls
- Larger compositions like floating action clusters → liquid-glass-advanced
- Performance rationale for always using a container → liquid-glass-best-practices
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.