---
title: "Live activities, DroppyKit"
description: "The compact row beside the notch, and the detail behind it."
canonical: "https://getdroppy.app/docs/droppykit/live-activities"
last-updated: "2026-09-09"
---

# Live activities

The compact row beside the notch, and the detail behind it.

A live activity is Droppy's version of the Dynamic Island: a compact status that stays visible while something is happening, in the wings either side of the notch — or inside a floating island on a display without one.

Only one droplet holds the compact seat at a time. You publish a state; the host decides who wins.

### Publishing state

[`liveActivityState`](https://getdroppy.app/docs/droppykit/api/live-activity-providing#live-activity-state) is a publisher. Emit a [`LiveActivityState`](https://getdroppy.app/docs/droppykit/api/live-activity-state) while you have something to say, and `nil` to stand down.

```
extension WorldClockDroplet: LiveActivityProviding {
    public var liveActivityState: AnyPublisher<LiveActivityState?, Never> {
        activitySubject.eraseToAnyPublisher()
    }

    private func publishActivity() {
        activitySubject.send(
            LiveActivityState(
                priority: 200,
                accessibilityTitle: "Amsterdam \(time)",
                isInteractive: false
            )
        )
    }
}
```

Emitting every second is fine — the host diffs before it redraws. What is not fine is holding the seat when you have nothing to show: publish `nil` the moment your task ends.

`priority` decides who wins the seat. Keep it honest: a timer that is counting down outranks a status that merely exists, and a droplet that pins itself high is one users turn off.

### The compact presentation

Two builders, one per wing:

```
public func makeCompactLeading() -> AnyView {
    AnyView(
        Image(systemName: "globe")
            .font(.system(size: DroppyLiveActivityMetrics.iconSize, weight: .medium))
            .foregroundStyle(AdaptiveColors.notchSurfacePrimaryText)
            .padding(.trailing, DroppySpacing.sm)
    )
}

public func makeCompactTrailing() -> AnyView {
    AnyView(
        Text(time)
            .font(.system(size: DroppyLiveActivityMetrics.labelFontSize, weight: .medium, design: .rounded))
            .monospacedDigit()
            .foregroundStyle(AdaptiveColors.notchSurfacePrimaryText)
            .padding(.leading, DroppySpacing.sm)
    )
}
```

The space is small and fixed: 70pt per wing at rest, 120pt when the row is interactive. Use `monospacedDigit()` on anything that ticks, or the row jitters as digits change width.

### Hover and the shelf

A droplet's live activity is a compact row and nothing else, exactly like Droppy's own media, AI coding and File Tray activities. Hovering it or clicking it does what hovering or clicking any of Droppy's activities does: it opens the shelf, where your shelf widget is. The row hides while the shelf is open and comes back when it closes.

[`makeExpanded(context:)`](https://getdroppy.app/docs/droppykit/api/live-activity-providing#make-expanded-context) is not mounted by Droppy. It stays a requirement so droplets already built keep loading, but no card grows out of the row. Controls that must stay reachable, Stop and Start above all, belong in a shelf widget.

### How content appears

Every motion around your row is Droppy's own, run by the same code that runs for Droppy's activities: the row unfolds out of the notch edge when it takes the seat, the shelf opens and closes on its own springs, and the row returns the way Droppy's own activities return after the shelf closes. You hand the host views; the views take no entrance transition of their own, or the entrance plays twice.

What you do animate is what changes inside the row while it is up, such as a glyph that flips in a wing. Use [`DroppyTransition`](https://getdroppy.app/docs/droppykit/api/droppy-transition), which carries Droppy's own curves:

```
Image(systemName: isCleaning ? "stop.fill" : "keyboard")
    .id(isCleaning)
    .transition(DroppyTransition.compactContent)
```

`DroppyTransition` is 1.8.0 API, so a droplet that uses it declares `minAPI` 1.8.0.

### Seats

[`seat`](https://getdroppy.app/docs/droppykit/api/droplet-live-activity-service#seat) reports what you currently hold:

- [`compact`](https://getdroppy.app/docs/droppykit/api/droplet-live-activity-seat#compact) — the primary seat.
- [`secondary`](https://getdroppy.app/docs/droppykit/api/droplet-live-activity-seat#secondary) — your activity rides the round companion pill beside one of Droppy's own, drawn by `makeCompanionCompact(context:)`. Only an activity whose state sets `joinsPersistentActivitySet` is seated there, after Droppy's own companions. Draw for the context's `slotSize`, the square Droppy's own companions draw their mark in: a glyph, a ring or a few characters, never your wing's layout. Anything larger is scaled down to it and clipped. A flick or a tap on the pill promotes your activity to `compact`, as with Droppy's own companions, and the activity it displaced takes the pill.
- [`none(_:)`](https://getdroppy.app/docs/droppykit/api/droplet-live-activity-seat#none) — with a reason. Droplets see `idle` when they publish nil, `outranked` when another droplet wins, and `surfaceSuppressed` while Droppy's own surfaces own the notch (media, timer, recording, meeting, HUD up, shelf open). `pairFull`, `displayIneligible`, and `userDisabled` are defined for future use and are not awarded to droplets today.

[`liveActivitySeatDidChange(_:)`](https://getdroppy.app/docs/droppykit/api/live-activity-providing#live-activity-seat-did-change) tells you when it moves. Use it to stop work nobody can see. Losing the seat is normal, not an error. A droplet holds compact only when none of Droppy's own activities want the notch; when media plays it moves to the companion pill (or stands down, if it does not join the persistent set), and when the shelf opens it stands down.

To stand down before your publisher catches up:

```
host.liveActivity.yield(reason: .idle)
```

### Interactive rows

Set `isInteractive` on the state when your row has controls. The host widens the wings, and [`DroppyLiveActivityControlStyle`](https://getdroppy.app/docs/droppykit/api/droppy-live-activity-control-style) gives you the 24pt circular control Droppy's own media row uses. Do not use a glass button style in this row; that chrome is for shelf widgets and settings, not island chrome.

```
Button { skip() } label: { Image(systemName: "forward.fill") }
    .buttonStyle(DroppyLiveActivityControlStyle(prominence: .accent))
```

Publish one stable state and keep it stable: fixed `priority` and fixed `isInteractive`, only `accessibilityTitle` ticks. Flapping either flag resizes the shell and drops hover. Publish `nil` only when the task truly ends, never to represent an idle toggle. Start and Stop belong in your shelf widget, which exists whether or not the row is published.

### The companion pair

Droppy's own activities can pair as island plus detached pill. External droplets do not hold the pill seat today, so design the compact row to be complete on its own.

When that changes, the second renders through [`makeCompanionCompact(context:)`](https://getdroppy.app/docs/droppykit/api/live-activity-providing#make-companion-compact-context) with the slot size in `context.slotSize`. Return a narrower composition; the default reuses your trailing wing, which is usually too wide.
