---
title: "ShelfWidgetProviding, DroppyKit"
description: "Adopt to contribute one or more widgets to the notch shelf."
canonical: "https://getdroppy.app/docs/droppykit/api/shelf-widget-providing"
last-updated: "2026-09-09"
---

# ShelfWidgetProviding

Adopt to contribute one or more widgets to the notch shelf.

`@MainActor protocol ShelfWidgetProviding : Droplet`

Every member is main-actor isolated, inherited from [`Droplet`](https://getdroppy.app/docs/droppykit/api/droplet).

## When the host reads the descriptors

The host reads `widgetDescriptors` (and, for a [`ShelfWidgetDynamicLayoutProviding`](https://getdroppy.app/docs/droppykit/api/shelf-widget-dynamic-layout-providing) droplet, [`currentLayoutTraits(for:)`](https://getdroppy.app/docs/droppykit/api/shelf-widget-dynamic-layout-providing#current-layout-traits-for)):

- when the droplet starts, stops, is updated or is removed;
- after the droplet broadcasts a state change, which is any of these: its `objectWillChange` fires (when the droplet is an `ObservableObject`, as every `droppykit new` scaffold is); one of its keys in [`DropletPreferencesService`](https://getdroppy.app/docs/droppykit/api/droplet-preferences-service) changes; an `@Observable` property read during the previous read changes (when the droplet is not an `ObservableObject`); or [`statePublisher`](https://getdroppy.app/docs/droppykit/api/droplet-install-state-service#state-publisher) fires;
- when the droplet calls [`invalidateLayout(for:)`](https://getdroppy.app/docs/droppykit/api/droplet-shelf-service#invalidate-layout-for), or its [`shelfWidgetLayoutInvalidationPublisher`](https://getdroppy.app/docs/droppykit/api/shelf-widget-dynamic-layout-providing#shelf-widget-layout-invalidation-publisher) emits.

Broadcasts are coalesced: a burst costs one read, and the host makes at most about four a second however often the droplet broadcasts. A read that changed nothing changes nothing on screen. One that did is applied live, without a relaunch: a new widget id appears in the shelf's widget picker, Settings and search; an id the droplet stops declaring leaves the shelf, while the user's placement of it is kept, so it returns to the same slot when the droplet offers it again; a changed title or symbol shows wherever the widget is listed; and a changed width or height re-lays the open shelf out on its own spring.

So a widget that should appear only once the droplet is configured can return no descriptor until then and declare it the moment it is: write the setting through `host.preferences`, or publish it on the droplet's own `objectWillChange`. State held by another object reaches the host only through one of those broadcasts: forward that object's `objectWillChange` to the droplet's own, or store the value in preferences. The widget's view is yours to keep current; a broadcast re-reads descriptors and does not rebuild a mounted view whose descriptor is unchanged.

Droppy 16.0.1 and earlier read the descriptors only when the droplet started, stopped, was updated or was removed, and when it called [`invalidateLayout(for:)`](https://getdroppy.app/docs/droppykit/api/droplet-shelf-service#invalidate-layout-for).

- Note: Maturity: Stable. Covered by the 1.0 compatibility promise. Host support: mounted. Droppy lays a droplet's widget out with the same algorithm as its own, from the traits in your descriptor. See [Host support](https://getdroppy.app/docs/droppykit/host-support).

## Properties

### widgetDescriptors

`@MainActor var widgetDescriptors: [ShelfWidgetDescriptor] { get }`

The widgets this droplet provides right now. Read on the occasions listed above, never on a timer; keep it stable, cheap and free of side effects, and return the same descriptor for the same state, since the host compares each read with the last one to decide what to update.

## Methods

### makeWidgetSettingsPopover(_:)

`@MainActor func makeWidgetSettingsPopover(_ id: ShelfWidgetID) -> AnyView?`

Optional inline settings popover shown from the widget's gear affordance. Return `nil` when the widget has no inline settings. Defaults to `nil`.

### makeWidgetView(_:context:)

`@MainActor func makeWidgetView(_ id: ShelfWidgetID, context: ShelfWidgetContext) -> AnyView`

Builds the widget's live view. Views render on dark shelf surfaces; use light foreground colors and the DroppyKit design tokens.

Hosts MUST call this with `.solo`, `isPaired == false`, and the settled solo slot width for a one-widget row. For every grouped row of two to four widgets, hosts MUST set `isPaired` and `isCompact` to `true`, pass each widget's settled slot width, and assign `.leading`, `.middle`, or `.trailing` according to its real position. Providers MUST render both the solo and grouped contexts from these facts; they must not infer row count from a product-specific width table.
