# Rim glow and edge fade

> Light a glowing rim around the panel to signal state, and soften scrolling content where it meets the panel's edges

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.

## Rim glow

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.

### Lighting it

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

```swift
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](/guides/companion-surfaces/) - 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.

### Styling it

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

```swift
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.

### Accessibility

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. |

### Notch and floating

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.

## Scroll edge fade

### Turning it on for the panel

Set the fade once on the configuration:

```swift
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](/guides/file-shelf/) - and yours do too once you mark
them with `nookScrollEdgeFade(axes:)`:

```swift
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.

### One scroll view on its own

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

```swift
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).

### How it is drawn

- **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.

## Chrome shadow

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:

```swift
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.

## Feedback cues

`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`:

```swift
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](/guides/theming/#sounds).

## Ambient wash

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:

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

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

## See also

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