Skip to content
GitHub

Rim glow and edge fade

Two opt-in effects for the panel itself. The rim glow lights the chrome’s edge in a color your content chooses - blue while work is running, say - so the user catches the state at the edge of vision. The scroll edge fade softens scrolling content where it meets the panel’s edges instead of cutting it off.

Both are off until you opt in, and both work in the notch, floating, and auto layouts. Examples/CompanionNook/main.swift uses both: its rim glows blue while the sleep timer runs, and its Up Next and Library lists fade at the edges.

The rim is two layers: a crisp line traced just inside the chrome’s edge, and a soft halo that spills past it and slowly breathes.

Publish a color from your content with nookRimGlow(_:); publish nil to turn it off:

struct SyncStatus: View {
@ObservedObject var sync: SyncModel
var body: some View {
SyncBadge(sync: sync)
.nookRimGlow(sync.isRunning ? .blue : nil)
}
}

This is the same seam as the ambient wash (nookAmbientColor(_:), backed by NookAmbientColorPreferenceKey): content publishes a color through a SwiftUI preference, NookRimGlowPreferenceKey, and the surface draws it without knowing why it was chosen. When several views publish, the last non-nil color wins.

One difference matters. The ambient wash is read from expanded content only; the rim is read from everything the chrome hosts - the compact slots, the expanded content, and every companion surface - because a state signal must survive the nook collapsing. Publish from both your compact and expanded content, or from a companion, whose content stays mounted in both states.

NookConfiguration.rimGlow sets the look; the defaults are shown:

configuration.rimGlow = NookRimGlowStyle(
lineWidth: 1.5, // the crisp line inside the edge
glowRadius: 8, // the halo's blur; 0 draws the line alone
intensity: 0.9, // 0...1 peak strength
pulses: true, // the halo breathes while lit
followsAmbientColor: false // see below
)

With followsAmbientColor: true, the rim takes the ambient wash color whenever no content publishes a rim color, so a host that tints its surface gets a matching edge. It is off by default, so a host already using the ambient wash never gains a rim it did not ask for.

The rim adapts to the user’s settings on its own:

Setting Rim
Reduce Motion The halo stays lit but stops breathing. Colors cross-fade without motion.
Increase Contrast No soft halo and no breathing; the line is drawn twice as heavy and fully opaque.
Reduce Transparency No translucent halo; the line stays.

In the notch layout the chrome’s top edge is fused with the menu bar and the hardware notch, so the rim fades out across that band and rises out of the menu bar instead of outlining it; the compact pill glows mostly along its bottom edge. The floating panel glows all the way around.

The rim is drawn by the surface for the chrome itself. Companion surfaces do not glow, but they can light the chrome’s rim.

Set the fade once on the configuration:

configuration.scrollEdgeFade = .standard // every edge, 20 pt deep

The framework’s own scroll views follow it - the built-in Settings screen and the NookComponents file shelf - and yours do too once you mark them with nookScrollEdgeFade(axes:):

ScrollView {
LazyVStack { rows }
}
.nookScrollEdgeFade(axes: .vertical)
ScrollView(.horizontal) {
HStack { chips }
}
.nookScrollEdgeFade(axes: .horizontal)

A scroll view only fades the edges along its own scroll axes, and does nothing while the panel’s fade is off - so the modifier is safe to leave in place.

To fade a single scroll view regardless of the panel setting, pass a fade directly:

ScrollView { rows }
.nookScrollEdgeFade(NookScrollEdgeFade(edges: .bottom, length: 28))

NookScrollEdgeFade has two fields: edges (default .all) and length, the depth of the fade in points (default 20).

  • macOS 26 and later, top and bottom edges - Apple’s soft scroll edge effect (scrollEdgeEffectStyle(.soft, for:)). The system draws it only where content scrolls under a bar, so the fade reserves a safeAreaBar of length on each faded edge.
  • macOS 26, leading and trailing edges, and every edge on macOS 15-25 - a gradient mask. The system effect does not exist on earlier systems and is not drawn for side edges.

Either way, length is reserved as a content margin on each faded edge, so content at rest is never faded - only content that scrolls into the margin is. Like the Liquid Glass backdrop, the system path is gated at compile time as well as at run time, so the package still builds with an Xcode older than 26.

This and the next two sections are engine-level settings on a Nook you drive yourself; NookConfiguration does not carry them yet.

The panel’s window shadow is off, because a window shadow is cast by the whole rectangular panel. Nook.chromeShadow casts one from the chrome’s outline instead, so it follows the notch shape through every spring between compact and expanded. It is off (nil) by default:

nook.chromeShadow = NookChromeShadow(color: .black.opacity(0.4), radius: 14, y: 6)

It is drawn only outside the outline, so it never darkens a translucent backdrop, and in the notch form it fades out across the menu-bar band the way the rim halo does, since the chrome is fused with the top of the screen there. The floating form casts it on every side.

Nook.playFeedback(_:tint:duration:repeats:) plays a peripheral cue along the chrome’s edge: .shimmer sweeps a bright band across it, and .pulse brightens the whole edge in place (what the shimmer becomes under Reduce Motion). To change more than the color, pass a NookFeedbackStyle:

nook.playFeedback(.shimmer, style: NookFeedbackStyle(
color: .green, // band edges, glow, and the pulse
coreColor: nil, // default: white at 75% in the band's center
bandGradient: nil, // or a Gradient laid across the band, clear at both ends
lineWidth: 6, // the visible line is half this, inside the edge
pulseLineWidth: 4,
glow: .standard, // or nil, or NookFeedbackStyle.Glow(color:width:radius:)
blendMode: .plusLighter // .normal on a light chrome, which .plusLighter cannot darken
))

NookFeedbackStyle.standard is the built-in look in the system accent color, and the tint: overload is that look in another color.

Through NookKit, coordinator.playFeedback(_:duration:repeats:) plays the cue in the theme’s feedback.tint and, when the theme has one, its sound.feedback; see Sounds.

Content that publishes nookAmbientColor(_:) gets a wash of that color behind the expanded chrome. Nook.ambientWash shapes it: a list of opacity stops along a linear gradient. The default, NookAmbientWash.standard, runs 34%, 16%, 6%, and 2% from top to bottom:

nook.ambientWash = NookAmbientWash(opacities: [0.5, 0], startPoint: .top, endPoint: .center)

NookAmbientColorBackground(color:wash:) paints the same wash in a host’s own view.

  • Companion surfaces - floating your own views beside the nook, and a convenient place to light the rim from.
  • Surface materials - the backdrop the rim is drawn on.
  • Sources/NookSurface/NookRimGlow.swift and Sources/NookSurface/NookScrollEdgeFade.swift - the types behind this guide.