---
title: "Build a droplet: SwiftUI that draws on Droppy's own surfaces"
description: "A droplet is a Swift package that builds a dynamic library and links DroppyKit. Scaffold it, build the bundle, and iterate in the harness."
canonical: "https://getdroppy.app/blog/build-your-own-droppy-droplet"
last-updated: "2026-09-19"
---

# Build a droplet: SwiftUI that draws on Droppy's own surfaces

[All posts](https://getdroppy.app/blog/)

Droppy guides

Jordy Spruit September 19, 2026 8 min read

You can build your own tools for Droppy. A timezone clock, a small status display or a focused workflow can live on its shelf and notch, written in SwiftUI against the public DroppyKit SDK. The useful starting point is one small thing you want within reach while you work.

**Version note, September 19, 2026:** this guide follows DroppyKit 1.8.1. Its current scaffold declares a Droppy 15.3.0 minimum, which every public Droppy since 16.0.0 meets, the current stable included. That requirement is enforced. Start with the harness; use a Playground or Droppy build that meets your droplet's declared minimum before installing it. The [host support guide](https://getdroppy.app/docs/droppykit/host-support) describes the 15.3 surface target, not a promise that every older download supports it.

**Direct answer:** a droplet is a Swift package that builds a dynamic library and links DroppyKit. Clone the SDK repository once, put its `Scripts` folder on your PATH, run `droppykit new` to scaffold, `droppykit run` to see your views in the harness, and `droppykit build` to get a `.droplet` bundle in `.build/`. Droppy loads that bundle into its own process, so a droplet is not sandboxed code: it runs in-process, on the main actor, with Droppy's permissions. You need macOS 14 or later, Swift 6.2 with Xcode 26, and Droppy 15.3.0 or later.

![Cobalt engraved illustration of a droplet assembled in three pieces above a workbench with drafting tools](https://getdroppy.app/assets/images/blog/editorial-20260919/build-droplet-hero.webp?v=1)

A droplet is a normal Swift package. What makes it a droplet is where Droppy loads it and what it may draw.

## What a droplet can put on screen

A droplet draws through surfaces that ship inside Droppy: a shelf widget on the expanded shelf, a live activity in the compact row beside the notch, a mini HUD for a short confirmation, and, beyond those, a lock screen row, a menu bar extra, a shelf takeover and a settings pane in Droppy's own Settings window. Not every surface is in every release. The SDK's [Host support](https://getdroppy.app/docs/droppykit/host-support) page names a Droppy version and what it mounts, and in 15.3 all of these are mounted. A surface Droppy cannot mount refuses and says so rather than appearing to succeed.

## Setting up the toolchain

`droppykit` is not in the package your droplet depends on, because SwiftPM checks the SDK out for the compiler, not for you. It lives in the SDK repository's `Scripts` folder: clone that repository once and add the folder to your PATH with an `export` line in your shell profile.

From a directory where you keep projects, the [public DroppyKit repository](https://gitlab.com/droppyformac1/droppykit) gives you this starting sequence. The new droplet is a separate folder beside the SDK checkout.

```
git clone https://gitlab.com/droppyformac1/droppykit.git
export PATH="$PWD/droppykit/Scripts:$PATH"
droppykit new worldclock --name "World Clock"
cd worldclock
droppykit run
```

## Scaffolding with droppykit new

`droppykit new worldclock --name "World Clock"` writes a package that builds, a droplet that runs, a manifest that validates, a starter Icon Composer document and a placeholder avatar, so `droppykit validate` passes before any of it has been replaced. The Swift type name comes from the id: `world-clock` becomes `WorldClock`. It also writes `AGENTS.md`, `CLAUDE.md` and the MCP wiring for Claude Code, Codex and Cursor, because most droplets are written with an agent at the keyboard.

Droplet ids have rules, because an id becomes a directory name, a preferences namespace and part of a path: 3 to 40 characters of lowercase letters, digits and hyphens, starting with a letter and ending with a letter or digit. Dots are forbidden, since preferences are namespaced to `droplet.<your-id>.<key>`, so a dot would let one droplet's keys overlap another's namespace.

## The entry point, and why the bundle is a library

Every droplet has two types. The **principal** is what Droppy's loader instantiates through the bundle's `NSPrincipalClass`: it is `@objc` and does nothing but make the droplet, because it runs before the host is ready. The **droplet** lives for the session, conforms to `Droplet`, and holds the host.

`activate(host:)` is called once, after Droppy's managers are up, and runs under a watchdog: keep it fast and store the host, because there is no global accessor. Throwing marks the droplet failed for the session. `deactivate()` is called on disable or removal, and everything started in activation has to stop there, because Swift cannot unload code and a timer left running keeps running until Droppy relaunches. `prepareForRemoval()` returns `true` by default and can veto a removal that would lose work in flight.

The product is a dynamic library on purpose. Droppy already carries DroppyKit as a resilient framework, and your bundle must link that framework in the form the host expects. A bare `swift build` is not the supported packaging path: linking a second SDK copy or using non-resilient imports can cause type-identity or missing-symbol failures. Use `droppykit build`, which applies the library-evolution and linkage settings, then `droppykit validate` before sharing the bundle.

## The manifest is what everything else reads

`droplet.json` tells the build script, the validator, the Store and getdroppy.app what this droplet is: its id, name and version, the `kit` block with its ABI and minimum SDK API, `minAppVersion`, the declared `capabilities` and `surfaces`, its icon, creator and source repository. Two fields matter most. `surfaces` and your Swift conformances must agree in both directions, and the harness reports the difference, because a manifest and code that disagree is the most common reason a droplet validates locally and does nothing once installed. And `capabilities` are shown to the user before they install, so ask for what you use and nothing more: a long list is the most common reason a submission is sent back.

| Where you work | Command | What you get |
| --- | --- | --- |
| First package | `droppykit new` | Package, manifest, icon document, harness target |
| Edit and run loop | `droppykit run` | Every surface, in Droppy's Settings panel chrome |
| Pictures for review | `droppykit run -- --shots ./shots` | One PNG per harness page, no window |
| Machine-readable verdict | `droppykit run -- --report ./report.json` | Surfaces, problems, activity and log as JSON |
| Shipping bundle | `droppykit build` | `<Product>.droplet` in `.build/` |
| Submission checks | `droppykit validate` | The same checks a submission runs |

## Iterating in the harness

`droppykit run` opens your droplet in the harness, which uses Droppy's Settings panel source in a larger window so a solo and a paired widget fit side by side. Its host services simulate interactions: preferences live in memory and `workspace.open` records a URL instead of opening it. This makes the editing loop convenient, but it is not a sandbox around arbitrary code your droplet runs directly.

Two of its pages catch more than the rest. Overview compares conformances against the manifest and names a surface you declared but did not implement, or implemented without declaring, and its Hardware notch switch re-renders everything as a floating island, where content that assumed wings either side of a cut-out falls over. `--shots` renders each page to a PNG without a window, and `--report` writes the verdicts as JSON, where `problems` lists everything the pages would flag. A droplet is not finished while `problems` has anything in it.

## The same droplet on the real surfaces

Droppy Playground is a free, separate app drawing the notch, shelf, island and HUDs with Droppy's code. It includes the media player and omits the full app's file shelf, clipboard and Cloud features. Drop a built bundle or the package onto its Store page; a package is searched for a built bundle under `.build/`. It runs the same admission checks apart from Droppy's approval policy, and checks compatibility against the Droppy host version it was built from.

Release Droppy loads a local `.droplet` too, from the **Local droplets** section of its Store page. A droplet runs with Droppy's permissions, so before running a build it did not sign, Droppy asks, and the answer lasts until Droppy quits: nothing is recorded on disk, so nothing another app writes can approve a build for you. A bundle changed after approval is refused until you approve it again, so keep anything your droplet writes out of its own bundle. Rebuilding a droplet that has run installs the new bundle, but the earlier build keeps running until you restart Droppy.

The SDK ships the guides this workflow comes from: [Create your first droplet](https://getdroppy.app/docs/droppykit/first-droplet), [Droplet setup](https://getdroppy.app/docs/droppykit/droplet-setup), [Harness](https://getdroppy.app/docs/droppykit/harness), [Playground](https://getdroppy.app/docs/droppykit/playground) and [Submitting](https://getdroppy.app/docs/droppykit/submitting). [The companion guide to surface design](https://getdroppy.app/blog/design-droppy-droplet-notch-surfaces) takes the other half of the job.

## Sources and version scope

**Checked against the current product:** the commands, scaffold output and entry point types come from the DroppyKit SDK as it stands for Droppy 15.3.0, in its `Scripts` folder, with the requirements in [Add SPM](https://getdroppy.app/docs/droppykit/add-spm). Harness pages, `--shots` and `--report` are in [Harness](https://getdroppy.app/docs/droppykit/harness), and the Playground's install path, with release Droppy's per-launch approval for a local build, in [Playground](https://getdroppy.app/docs/droppykit/playground) and [Host support](https://getdroppy.app/docs/droppykit/host-support).
