---
title: "DropletShelfService, DroppyKit"
description: "Opens and closes Droppy&#x27;s shelf, and reveals this droplet&#x27;s widgets in it."
canonical: "https://getdroppy.app/docs/droppykit/api/droplet-shelf-service"
last-updated: "2026-09-09"
---

# DropletShelfService

Opens and closes Droppy's shelf, and reveals this droplet's widgets in it.

`@MainActor protocol DropletShelfService : AnyObject`

Requires the `shelf-read` capability to observe and `shelf-write` to act.

## Properties

### didChange

`@MainActor var didChange: AnyPublisher<Void, Never> { get }`

Fires when the shelf opens, closes, or swaps widgets.

### holdsOpen

`@MainActor var holdsOpen: Bool { get }`

Whether this droplet currently asks the shelf to stay open.

### holdsOpen

`@MainActor var holdsOpen: Bool { get }`

A host that does not implement holds never holds.

### isExpanded

`@MainActor var isExpanded: Bool { get }`

Whether the shelf is currently expanded.

### soloWidgetID

`@MainActor var soloWidgetID: ShelfWidgetID? { get }`

The widget currently shown alone on the shelf, if any. `nil` while the shelf is closed or showing a paired row.

## Methods

### close()

`@MainActor func close()`

Closes the shelf if this droplet opened it.

### invalidateLayout(for:)

`@MainActor func invalidateLayout(for widgetID: ShelfWidgetID)`

Asks the host to re-measure a widget whose content changed size. Call this after your model changes the size your view reports, not on every keystroke.

The host re-reads this droplet's [`widgetDescriptors`](https://getdroppy.app/docs/droppykit/api/shelf-widget-providing#widget-descriptors) and re-lays the open shelf out when what they declare changed; a call that changed nothing moves nothing. It names a widget the droplet already declares: one it does not is refused. A droplet whose state broadcasts reach the host (see [`ShelfWidgetProviding`](https://getdroppy.app/docs/droppykit/api/shelf-widget-providing)) is re-read on those too, so this call is for a size change the host cannot otherwise hear about.

### open(revealing:)

`@discardableResult @MainActor func open(revealing widgetID: ShelfWidgetID?) -> Bool`

Opens the shelf, optionally showing one of this droplet's widgets alone. Returns `false` when the host refused.

### requestKeyboardFocus()

`@discardableResult @MainActor func requestKeyboardFocus() -> Bool`

Asks the host to make the shelf panel key so a text field in one of this droplet's widgets receives keystrokes.

Accepted only while one of this droplet's widgets is on the expanded shelf and that widget declared [`keyboardFocusable`](https://getdroppy.app/docs/droppykit/api/shelf-widget-focus-policy#keyboard-focusable). Returns `false` when the shelf is closed, none of the droplet's widgets is showing, the widget is not keyboard-focusable, the capability is missing, or the panel could not become key.

Call it when the field appears, from the view's `onAppear` or the action that navigates to it. The host then keeps the panel eligible for the keyboard for as long as that widget stays on the shelf, even while the droplet's own page is open in Settings; there is nothing to release. A widget that opens alone is promoted by the host on its own, so a single field the user types into the instant the widget opens needs only the focus policy.

Requires `shelf-write`, and DroppyKit 1.10.0: declare `minAPI` 1.10.0 when a droplet calls this method.

### requestKeyboardFocus()

`@discardableResult @MainActor func requestKeyboardFocus() -> Bool`

A host that predates explicit shelf focus refuses the request.

### setHoldsOpen(_:)

`@discardableResult @MainActor func setHoldsOpen(_ holdsOpen: Bool) -> Bool`

Keeps the expanded shelf from closing on its own while one of this droplet's widgets is on it.

For a widget the user watches rather than touches: a teleprompter reading out, a timer counting down, a live transcript. The shelf normally closes a moment after the pointer leaves it and on any click outside it; while this is on, neither closes it, and it stays on screen over a full-screen app. The user can still close it: Droppy draws its floating close button below the shelf for as long as the hold counts, a swipe up and the shelf shortcut still work, and so does `close()`.

The hold only counts while one of your widgets is actually showing on the expanded shelf, so a hold you forget to release cannot pin the user's other widgets open. Release it the moment the work that needed it ends; the host also releases it when the droplet deactivates.

Requires `shelf-write`, and DroppyKit 1.5.0: declare `minAPI` 1.5.0, because a droplet that calls it cannot load in an older host. Returns `false` when the host refused, which is also what a host that does not implement holds answers.

### setHoldsOpen(_:)

`@discardableResult @MainActor func setHoldsOpen(_ holdsOpen: Bool) -> Bool`

A host that does not implement holds refuses every one.
