---
title: "Expanded surfaces, DroppyKit"
description: "A takeover on the notch, for something that needs the whole shelf."
canonical: "https://getdroppy.app/docs/droppykit/expanded-surfaces"
last-updated: "2026-09-09"
---

# Expanded surfaces

A takeover on the notch, for something that needs the whole shelf.

An expanded surface replaces the shelf's normal contents with yours: a picker, a form, a full view of something a widget can only summarise. It is the biggest thing a droplet can put on screen, and the one that most needs restraint — while it is up, the user cannot see anything else Droppy would have shown.

Requires the `expanded-surface` capability.

### Declaring one

```
extension WorldClockDroplet: ExpandedSurfaceProviding {
    public var expandedSurfaces: [ExpandedSurfaceDescriptor] {
        [
            ExpandedSurfaceDescriptor(
                id: "clocks-detail",
                title: "World Clock",
                systemImage: "globe",
                suppresses: [.shelfWidgets, .autoCollapse]
            )
        ]
    }
}
```

### What it suppresses

[`ExpandedSurfaceHostSuppression`](https://getdroppy.app/docs/droppykit/api/expanded-surface-host-suppression) is the host chrome your surface turns off while it is presented. Ask for the least you need:

| Option | Turns off |
| --- | --- |
| `shelfWidgets` | The normal widget row |
| `favoritesBar` | The favourites strip |
| `floatingNavLane` | The floating navigation lane |
| `notificationBanners` | Banners over the shelf |
| `autoCollapse` | The shelf closing on its own |
| `outsideClickDismissal` | Clicking away closing it |
| `swipeGestures` | Swipe-to-close |

`takeover` is the full set. Reach for it only when a stray click really would lose the user's work: suppressing `outsideClickDismissal` means the obvious way out stops working, and a surface that traps someone is worse than one that closes early.

### Sizing

```
public func expandedSurfaceSize(
    _ id: ExpandedSurfaceID,
    fitting proposal: ExpandedSurfaceSizeProposal
) -> CGSize? {
    CGSize(width: proposal.standardSize.width, height: CGFloat(clocks.count) * 44 + 64)
}
```

Return `nil` to take `proposal.standardSize`. The proposal also carries `maximumSize`, the surface kind, and the display, so a surface can be shorter on a small screen.

The shelf asks on its layout passes, and again while your surface is up each time the droplet broadcasts a state change: its `objectWillChange` fires (when the droplet is an `ObservableObject`), one of its keys in [`DropletPreferencesService`](https://getdroppy.app/docs/droppykit/api/droplet-preferences-service) changes, an `@Observable` property read during the previous call changes (when it is not), or [`statePublisher`](https://getdroppy.app/docs/droppykit/api/droplet-install-state-service#state-publisher) fires. That ask repeats the shelf's last proposal, and the shelf re-lays itself out only when your answer changed, so a surface that grows (the clock list above gaining a row) resizes the shelf without you doing anything else. A burst of broadcasts costs one call, at most about four a second.

Droppy hears the droplet object, not the provider. When the provider is a separate object, a session the droplet owns, forward its `objectWillChange` to the droplet's own, or store the value in preferences. (`@Observable` state is heard wherever it lives, but only for a droplet that is not itself an `ObservableObject`.) Droppy 16.0.1 and earlier asked only on layout passes the shelf made for its own reasons, so a surface that grew stayed at its old size until something else moved the shelf.

### Changing a surface while it is up

[`expandedSurfaces`](https://getdroppy.app/docs/droppykit/api/expanded-surface-providing#expanded-surfaces) is read when you present, to find the descriptor for the id you asked for. A presentation keeps that descriptor until it is dismissed, its suppressions above all: dropping `autoCollapse` halfway through would close the shelf under the user, and adding `outsideClickDismissal` would take away the way out they were using. A changed descriptor applies on the next presentation.

A broadcast never dismisses a presented surface. If [`expandedSurfaceProvider`](https://getdroppy.app/docs/droppykit/api/expanded-surface-hosting#expanded-surface-provider) starts returning another object, the shelf draws from that one; if it returns `nil`, the shelf draws an empty surface until the user closes it. When a session ends, call [`dismissExpandedSurface(_:)`](https://getdroppy.app/docs/droppykit/api/droplet-notch-surface-service#dismiss-expanded-surface) before you take its provider away.

Every size here is the **content area**: what your view draws in, and what [`availableSize`](https://getdroppy.app/docs/droppykit/api/expanded-surface-context#available-size) hands you back, clamped. The host adds the shelf's own chrome around it (the side insets, the cutout clearance, the slot gutter), so a 400-point-tall answer is a 400-point-tall surface, not a 400-point-tall shell with your bottom row behind the shell's edge. The standard size is a solo widget card in the shelf's minimum shell (352 × 93 on a notch, 370 × 93 as an island); the ceiling is two thirds of the display less the chrome.

### Presenting and dismissing

```
let presentation = host.notchSurface.presentExpandedSurface(
    ExpandedSurfacePresentationRequest(surfaceID: "clocks-detail", opensShelf: true)
)
```

[`ExpandedSurfacePresentation`](https://getdroppy.app/docs/droppykit/api/expanded-surface-presentation) identifies **the presentation, not the surface**, and the difference matters. Tearing a takeover down runs after the close spring settles, and by then the user may have summoned the same surface again — a teardown keyed on the surface id would kill the fresh one. Capture the presentation you started with and compare before acting on a delayed callback.

[`expandedSurfaceDidDismiss(_:presentation:reason:)`](https://getdroppy.app/docs/droppykit/api/expanded-surface-providing#expanded-surface-did-dismiss-presentation-reason) is called once per presentation, for every reason including your own dismissal. [`ExpandedSurfaceDismissalReason`](https://getdroppy.app/docs/droppykit/api/expanded-surface-dismissal-reason) tells you which: the user collapsed the shelf, another surface pre-empted yours, the display changed, the shelf was turned off, or host policy ended it.
