Surface materials
A surface style decides what the chrome paints behind compact and expanded nook content: a flat opaque panel, a frosted vibrancy material, or Apple’s Liquid Glass. It is a user-facing preference - the framework ships a Settings control for it and persists the choice - so most hosts never set it in code; they read the resolved value back to pick a matching palette.
Where the style is picked
Section titled “Where the style is picked”The style lives on NookAppearancePreferences.surfaceStyle:
public enum NookSurfaceStyle: String, Codable, Sendable, CaseIterable { case solid // opaque panel, matches the menu-bar notch (the default) case translucent // frosted vibrancy material, shows the wallpaper case liquidGlass // Apple's Liquid Glass material (macOS 26+)}surfaceStyle defaults to .solid. The framework owns the Settings panel that
writes it - the Appearance screen renders a segmented “Surface” picker (Solid /
Translucent / Liquid Glass) and persists the selection through
AppState.replaceAppearancePreferences(_:). Your code reads from it; see how a
host reads the style below.
To ship a non-default out of the box, seed it on
NookConfiguration.preferenceDefaults (a seed the user can always override in
Settings, never persisted on its own):
configuration.preferenceDefaults = NookPreferenceDefaults( appearance: NookAppearancePreferences(surfaceStyle: .liquidGlass))The three styles
Section titled “The three styles”The mapping from a style to a concrete NookBackdrop lives in
NookBackdropMapping.notchBackdrop, keyed by the style, the effective color
scheme, and whether Reduce Transparency is on.
.solidpaints a flat opaque fill - true black on dark chrome, true white on light - so the expanded panel reads as one continuous surface with the physical notch. NoNSVisualEffectViewis involved. This is the default and the most legible over any wallpaper..translucentpaints a frostedNSVisualEffectViewsidebar material sampling the wallpaper behind the window, with a legibility darken pass composited on top. The darken scales withbackdropStrength(the Settings “Translucency strength” slider,0.15to1; the mapping clamps any other value into that range) so the user can let more wallpaper through..liquidGlasspaints Apple’s Liquid Glass material on macOS 26 (Tahoe) and later, and a layered approximation on earlier systems. The default mapping tints the glass toward the resolved theme - black for dark chrome, white for light - because the real material otherwise flips its own light/dark treatment to match the wallpaper and can end up fighting the chrome’s text. It adds a matching top-to-bottom scrim that tapers toward the bottom, so the surface reads glassier as it nears the wallpaper. Both scale withbackdropStrength(the Settings “Glass strength” slider).
Liquid Glass in depth
Section titled “Liquid Glass in depth”Liquid Glass renders through NookBackdrop.liquidGlass(LiquidGlass). The
LiquidGlass spec carries four knobs that drive both render paths:
public struct LiquidGlass: Equatable, Sendable { public var tint: Color? // nil = neutral, clear glass (the default) public var tintStrength: CGFloat // 0...1, default 0.18; ignored when tint is nil public var highlightStrength: CGFloat // 0...1 specular rim + sheen, default 0.6 public var shading: Shading? // legibility gradient the caller fully owns}shading is a Gradient plus a direction (startPoint/endPoint, defaulting
top-to-bottom), so the legibility pass is entirely the caller’s - the surface
renders exactly what the spec carries and never substitutes its own. The default
framework mapping supplies a sensible top-to-bottom darken; a host returning its
own .liquidGlass from a backdrop resolver can replace it wholesale.
Notch fade
Section titled “Notch fade”Many notch apps shade their glass black where the panel meets the hardware notch, clearing toward the bottom so the wallpaper shows through lower down. Ask for that look with one line instead of a backdrop resolver:
configuration.chromeBehavior.glassShading = .notchFadeNookGlassShading has two cases:
.even(the default) - glass tinted toward the theme with a light darken that eases toward the bottom, as described above..notchFade- clear glass shaded from black at the top edge to nothing at the bottom. The top always matches the notch; the Settings “Glass strength” slider scales how dark the fade is below it. Light chrome fades from white instead, so dark text stays legible.
The shading changes Liquid Glass only. Solid, Translucent, and Reduce Transparency look the same either way.
Collapsed, the fade goes solid
Section titled “Collapsed, the fade goes solid”The fade needs a panel to run down. Collapsed, a notch-fused chrome is the hardware notch’s own height - 32 points on a 1728-wide built-in display - and roughly three quarters of its width sits behind the camera, so a gradient sized for the expanded panel shades nothing but the two small wings either side of it.
So .notchFade paints the collapsed chrome the flat notch color instead - solid
black, or white for light chrome - which is the look the fade was after in the first
place: chrome indistinguishable from the hardware it sits in. The fade starts when
there is a panel tall enough to show it. The chrome re-resolves its backdrop on every
expand and collapse, so the two swap as it moves; nothing is re-resolved on a hide,
so no repaint lands under a panel that is fading out.
A fade sized for the tall chrome would look wrong squeezed into a small companion
pill - black at its top, clear at its bottom. So under .notchFade, companions that
inherit the expanded chrome’s backdrop get the same clear glass with a light, even
tint instead, scaled by the same strength. Collapsed there is no fade to squeeze, so
they go back to inheriting the chrome and the pill beside it matches it. To decide
that yourself, set chromeBehavior.companionBackdrop, a resolver like backdrop
that returns what companions inherit (nil for the chrome’s own). Companion content
reads the same value as \.nookChromeBackdrop. At the engine level this is
Nook.companionBackdrop.
The gradient itself is public too, for a host resolver or a raw Nook:
.liquidGlass(.init(shading: .notchFade(.black, strength: preferences.backdropStrength)))Real material vs the pre-Tahoe approximation
Section titled “Real material vs the pre-Tahoe approximation”The surface dispatches on availability. On macOS 26+ it draws the real system material; on macOS 15-25 it draws a layered approximation that reads as glass:
- Real material (macOS 26+). A
Color.clearpainted with.glassEffect(_:in:)using aGlassmaterial, clipped to the sameNookShapethe chrome already uses, with the host’sshadingoverlaid on top. A non-niltintbecomesGlass.regular.tint(tint.opacity(tintStrength)). macOS supplies its own edge highlights here, sohighlightStrengthonly adds a faint extra rim. - Approximation (macOS 15-25). A glassy
NSVisualEffectView(.hudWindow) material, an optional tint overlay, the sameshading, then the specular treatment that actually sells the glass read: a top-down sheen and a bright rim traced alongNookShape.highlightStrengthscales that sheen and rim.
Runtime gate and compile gate
Section titled “Runtime gate and compile gate”The real path is gated twice, and it matters for what you can build and where it runs:
- Runtime gate -
@available(macOS 26.0, *): the real.glassEffectmaterial only renders on macOS 26 and later. On an older OS the surface falls back to the approximation at runtime, so it cannot crash on systems without the material. - Compile gate -
#if compiler(>=6.2):Glassand.glassEffectexist only in the macOS 26 SDK (Xcode 26+, Swift 6.2). An@availablecheck still needs those symbols present in the SDK being compiled against, so an older Xcode cannot build the real path at all. The compile gate routes an earlier toolchain to the approximation unconditionally, so the package builds on Xcode before 26 instead of failing with “cannot find ‘Glass’ in scope”.
Putting both together: the package always builds, on any supported Xcode. The
real material needs the macOS 26 SDK (Xcode 26+) to build and macOS 26 at
runtime to render. Anywhere else, .liquidGlass still works - it just draws the
approximation.
Reduce Transparency
Section titled “Reduce Transparency”When the user enables Reduce Transparency, the mapping collapses both
translucent styles toward solid. .translucent and .liquidGlass both fall
back to a flat opaque fill (black on dark, white on light) - neither the frosted
material nor the glass renders when the user has opted out of translucency. The
guard is a single check in NookBackdropMapping.notchBackdrop:
if preferences.surfaceStyle == .solid || reduceTransparency { return .solid(isDark ? .black : .white)}So a host palette never has to special-case Reduce Transparency for the surface
material itself - the surface is already solid. (You may still want to nudge
subtleFill for contrast on a true-solid panel; see the Theming
guide.)
Read the style to pick a palette
Section titled “Read the style to pick a palette”A host that brands its chrome usually reads the chosen surface style and returns
a matching NookResolvedTheme. Branch on
appState.appearancePreferences.surfaceStyle:
configuration.theme = { appState in switch appState.appearancePreferences.surfaceStyle { case .solid: return SolidPalette.resolve(appState) case .translucent: return FrostPalette.resolve(appState) case .liquidGlass: return GlassPalette.resolve(appState) }}The switch is exhaustive over all three cases, so adding a palette branch for
.liquidGlass keeps it compiling. A glass-tuned palette typically leans on
lighter fills and brighter labels, since the glass keeps its own contrast and
needs less darken under chrome content than a frosted material does.
To paint brand-tinted glass instead of reading it back, supply a LiquidGlass
spec from a NookChromeBehavior backdrop resolver - that closure, not the
Settings style, is where the deeper customization lives:
configuration.chromeBehavior = NookChromeBehavior( backdrop: { context in .liquidGlass(.init(tint: .blue, tintStrength: 0.22)) })The resolver is re-run on every expand and collapse and is told which chrome it is answering for, so a host can shape the collapsed pill and the expanded panel separately rather than shading one gradient to suit both:
configuration.chromeBehavior.backdrop = { context in context.isExpanded ? .liquidGlass(.init(shading: .notchFade())) : .solid(.black)}See Backdrop resolver for the
whole NookBackdropContext.
Backdrops beyond the three styles
Section titled “Backdrops beyond the three styles”The Settings styles map to three NookBackdrop cases, but the engine paints more.
Return any of these from a backdrop resolver, or set Nook.backdrop directly on a
NookSurface-only host:
// Linear, radial (radii in points), elliptical (radii as fractions of the frame),// or angular. Elliptical keeps its look as the chrome grows from pill to panel..gradient(.linear(Gradient(colors: [.indigo, .black]))).gradient(.elliptical(Gradient(colors: [.teal, .black]), center: .top, endRadiusFraction: 1)).gradient(.angular(Gradient(colors: [.pink, .orange, .pink])))
// A SwiftUI MeshGradient (macOS 15). A mesh whose points or colors do not match its// width * height paints only its background..meshGradient(MeshGradient(width: 2, height: 2, points: [[0, 0], [1, 0], [0, 1], [1, 1]], colors: [.purple, .indigo, .black, .black]))
// Anything else: an image, a Shader, an animated view. The id stands in for the view// in equality, so give a different look a different id..custom(.init(id: "aurora") { context in context.reduceTransparency ? AnyView(Color.black) : AnyView(AuroraView())})The custom view fills the surface’s frame and is clipped to the outline;
context.shape is that outline, for a view that wants to trace its own edge.
The existing cases also expose the internals they used to fix:
| Knob | What it does | Default |
|---|---|---|
Vibrancy.darkenColor |
the color of the legibility overlay, so a light chrome can lighten instead of darken | black |
LiquidGlass.variant |
.regular or .clear glass on macOS 26 and later |
.regular |
LiquidGlass.fallbackMaterial |
the material the pre-Tahoe approximation starts from | .hudWindow |
LiquidGlass.highlightColor |
the approximation’s sheen and rim color | white |
LiquidGlass.rimWidth |
the approximation’s rim width; 0 keeps the sheen only | 1 |
Apple pairs .clear glass with a dimming layer behind content; the glass’s
shading is where that goes. Glass.interactive() is not exposed: the chrome is a
surface, not a control.
Reduce Transparency works per case. The surface paints vibrancy, solid, and Liquid
Glass exactly as given (the framework mapping swaps them for a solid fill, as
above). Gradients and meshes are painted with every color fully opaque while
Reduce Transparency is on, hues kept, so a fade to .clear becomes a fade to
black. A custom backdrop is the host’s view, so it decides: read
context.reduceTransparency and paint something opaque.
Theme backdrops
Section titled “Theme backdrops”A theme can describe the backdrop for each surface style the person can pick, as data a theme file can hold:
"backdrops": { "solid": { "kind": "linearGradient", "stops": ["#101014", "#000000"], "start": "top", "end": "bottom" }, "translucent": { "kind": "vibrancy", "material": "hudWindow", "darken": { "dark": 0.5, "light": 0.1 }, "darkenColor": "white" }, "liquidGlass": { "kind": "liquidGlass", "tint": "accent", "tintStrength": 0.3, "variant": "clear", "shading": { "stops": [{ "black": 0.3 }, { "black": 0.05 }] } }, "glassShading": "notchFade"}var theme = NookTheme()theme.backdrops.solid = .radialGradient(.init(gradient: NookGradientSpec(colors: ["#1A1030", "#000000"]), center: .top))theme.backdrops.translucent = .vibrancy(.init(material: .hudWindow, darken: 0.4))theme.backdrops.glassShading = .notchFadeThe kinds are framework (the mapping above), solid, vibrancy (with
darkenColor), liquidGlass (with variant, fallbackMaterial, highlightColor,
and rimWidth), linearGradient, radialGradient, ellipticalGradient,
angularGradient, mesh, and custom. A style the theme leaves out uses the
framework’s mapping. Colors resolve for the chrome’s appearance, so "accent" and
{"dark": ..., "light": ...} work here too.
- The person’s backdrop strength scales a vibrancy darken and a glass tint and
shading unless the description sets
"scalesWithStrength": false. - Under Reduce Transparency a vibrancy or glass description gives way to the framework’s solid fill. Gradients and meshes are painted opaque by the surface.
- A theme’s backdrop is painted in both the collapsed and expanded states; use a
chromeBehavior.backdropresolver for one that differs between them. A host resolver always wins over the theme. glassShadingshades the framework’s own Liquid Glass while the host’schromeBehavior.glassShadingis left at.even.- A kind this build does not know is read with its
fallback, so a theme written by a newer build still opens.
A theme file cannot hold a view, so a custom backdrop is named, and the host supplies the view:
"liquidGlass": { "kind": "custom", "id": "com.example.aurora", "fallback": { "kind": "framework" } }configuration.themeBackdrops["com.example.aurora"] = { context in .custom(.init(id: "com.example.aurora") { _ in AuroraView() })}With nothing registered under the id, the fallback is painted.
See also
Section titled “See also”- Theming -
NookTheme, its palette, and the backdrops it describes. Sources/NookSurface/NookBackdrop.swift- theNookBackdropandLiquidGlasstypes, the source of truth for the knobs.Sources/NookKit/App/NookBackdropMapping.swift- how a style, color scheme, Reduce Transparency, and the chrome’s state map to a backdrop.