Liquid Glass UI Migration: Adapting Custom Controls to the iOS 27 Material System
⚠️ Speculative Architecture & Preview: This article discusses future system iterations (e.g., iOS 27, Xcode 27) as conceptual planning and architectural design patterns. Technical details represent previews and proposals rather than finalized APIs.
Liquid Glass UI Migration: Adapting Custom Controls to the iOS 27 Material System
Adopting the iOS 27 design language is usually framed as a visual refresh, which leads teams to treat it as a color and corner-radius exercise. That framing produces the characteristic failure of a Liquid Glass UI migration: custom controls that look approximately right in a screenshot but feel wrong in motion, read poorly against dynamic content, and quietly cost far more GPU time than the system controls beside them. The root cause is that the glass material is not a background — it is a compositing contract between a surface and the content behind it.
This article covers what that contract requires of custom controls, how to migrate opaque surfaces incrementally, and how to measure the cost before shipping.
Key Takeaways
- The material samples content behind it. It is not an opaque or semi-transparent color, and it cannot be faked with a fixed alpha fill.
- Opaque custom layers break the contract. A control that paints an opaque background defeats sampling and loses the depth cues that define the system look.
- Nesting materials compounds cost. Two stacked material surfaces require two sampling passes; nested glass is the primary GPU regression.
- Contrast is dynamic. Legibility must be verified against the content behind the surface, not against a fixed background.
- Migrate custom controls last. System controls adopt the material automatically; custom ones require deliberate work.
The Contract: Sampling, Not Painting
A traditional custom control paints itself: a background color, a border, a shadow. A glass surface instead asks the system to sample the content behind it and produce a material from that sample. The distinction has a concrete consequence — a glass control has no independent appearance. Its appearance is a function of what is behind it.
+--------------------------------------------------+
| Content layer (photos, text, scrolling list) |
+--------------------------------------------------+
|
v sampled by
+--------------------------------------------------+
| Glass surface: material + content-aware tint |
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| Control content (label, icon, affordance) |
+--------------------------------------------------+
Figure 1: A glass surface does not paint an opaque layer; it samples the content beneath it and composites control content on top.
This is why a screenshot comparison is insufficient. Two controls can be pixel-identical in one frame and diverge completely as the content behind them scrolls.
Migrating an Opaque Custom Control
Consider a common legacy pattern: a floating action bar implemented as a view with a solid background, a shadow, and a hairline border.
// Legacy: an opaque surface that cannot sample content.
struct LegacyActionBar: View {
var body: some View {
HStack { /* controls */ }
.padding()
.background(Color(uiColor: .secondarySystemBackground)) // opaque
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(radius: 8) // forced depth
.overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.gray.opacity(0.3)))
}
}
The migration removes the opaque fill, the hand-rolled border, and the forced shadow, and replaces them with a single material surface:
// Migrated: let the system own the surface and its depth.
struct GlassActionBar: View {
var body: some View {
HStack { /* controls */ }
.padding()
.glassSurface(in: RoundedRectangle(cornerRadius: 16))
}
}
extension View {
/// Applies the system material and defers border/depth to it.
func glassSurface<S: Shape>(in shape: S) -> some View {
self
.background(.ultraThinMaterial, in: shape)
// No manual border or shadow: the material provides edge
// definition and depth through sampling, not decoration.
}
}
The important change is subtractive. The migration is not “add glass”; it is “remove the layers that prevent sampling.” Borders and shadows added on top of a material are the most common reason a migrated control still looks slightly off — they re-impose a fixed appearance the material is designed to compute.
Legibility Against Dynamic Content
The second contract requirement is contrast. An opaque surface guarantees a known background; a glass surface does not. Control content must remain legible over an arbitrary sample, which means contrast cannot be assumed — it must be enforced.
// The system can boost contrast for text over a material.
// Prefer this to over-darkening the material itself.
Text("Continue")
.font(.headline)
.foregroundStyle(.primary)
.glassSurface(in: Capsule())
.environment(\.colorSchemeContrast, .increased)
Two practical rules:
- Never hard-code a foreground color for glass content. Use semantic foreground styles and let the system resolve them against the sampled surface.
- Prefer content-aware tinting over opacity. If a control must read as “branded,” tint the material and verify it against the lightest and darkest content that can appear behind it. A fixed overlay opacity fails one of those extremes by construction.
For the broader accessibility surface — VoiceOver labeling, Dynamic Type on custom fonts, accessibilityRepresentation — see SwiftUI Accessibility Engineering.
Measuring the Cost
Material sampling is not free, and the cost is dominated by how many surfaces sample and how large they are — not by the visual complexity of the control.
| Pattern | Sampling passes | Relative cost |
|---|---|---|
| One material surface over content | 1 | Baseline |
| Material nested inside material | 2+ | High — avoid |
| Full-screen material over a scrolling list | 1, large area | Medium–high |
| Small material capsule over static content | 1, small area | Low |
Nested materials are the primary regression. They arise naturally when a glass container holds glass controls, and they are easy to introduce accidentally:
// Avoid: two material layers stacked, both sampling.
VStack { /* ... */ }
.glassSurface(in: RoundedRectangle(cornerRadius: 24))
.overlay {
Button("Action") { }
.glassSurface(in: Capsule()) // second sampling pass
}
For nested composition, give the inner control a flat, high-contrast treatment and let only the container sample. Profile with the Metal or Core Animation instrument before and after — the shape of the cost curve is counterintuitive enough that measurement, not intuition, should decide.
The Verdict: Subtract Before You Add
The most common migration mistake is aesthetic and additive: teams layer a material onto an existing control and adjust until it resembles the system surface. The system surface is not a texture to imitate; it is the result of the system owning the compositing. Migrate by removing the control’s self-painted appearance and letting the material do the work.
When to adopt system materials directly
- Any control that is not genuinely custom. System controls already honor the material and the accessibility settings.
- Floating bars, sheets, and sidebars — the surfaces the material was designed for.
When custom treatment is justified
- Branded surfaces where the material must be tinted, verified against extreme content.
- Controls with content the material cannot sample correctly, such as video or live camera previews behind a bar.
The Hidden Cost
- Appearance is no longer deterministic. Tests that asserted a control’s rendered color will fail, because the color now depends on content. Migrate those assertions to contrast-ratio checks rather than exact colors.
- Performance regressions are compositional, not local. A single new glass surface can be cheap; the same surface nested inside another can double sampling cost. Reviews must consider the composition, not the individual control.
- Custom blur chains become debt. Where a custom blur was used to approximate glass, it now competes with the real material. Remove it rather than tuning it.
The verdict: treat the material as infrastructure, not decoration. Remove the opaque layers, defer border and depth to the system, enforce contrast dynamically, and measure sampling cost as a property of the composition. A migrated control that follows the contract is both cheaper and more correct than a hand-tuned imitation.
Internal Links
- The Mechanics of Motion: Driving Liquid Glass UI with Phase-Based Animation — The motion system that pairs with the material surface.
- SwiftUI Accessibility Engineering: VoiceOver, Dynamic Type, and Assistive Technology — Verify legibility and accessibility for custom surfaces.
References & Further Reading
- Apple Design — Materials for the intended use of system materials.
- Apple Developer Documentation — SwiftUI materials for material and background APIs.
- Apple Design — Accessibility for contrast and legibility requirements.