Design guidelines
How to make a droplet look like part of Droppy rather than a guest in it.
Droppy's surfaces are dark, dense, and small. A droplet that ignores that reads as a foreign window bolted onto the notch, and that is the most common reason a submission is sent back for design rather than for code.
Everything below is available as a token or a component. Use them: a hardcoded number is a number that stops matching the next time Droppy's styling moves, whereas a token follows it.
Surfaces are dark
Notch and shelf surfaces are dark chrome, always. Foreground colours come from AdaptiveColors:
Text(title).foregroundStyle(AdaptiveColors.notchSurfacePrimaryText)
Text(detail).foregroundStyle(AdaptiveColors.notchSurfaceSecondaryText)
Text(hint).foregroundStyle(AdaptiveColors.notchSurfaceTertiaryText)Do not paint your own background on a widget. The shelf provides it, and a second fill on top reads as a card floating inside a card.
Spacing and radius
DroppySpacing is a 4pt grid; DroppyRadius is a tiered scale. Pair every radius with style: .continuous — a circular corner beside Droppy's continuous ones is visible.
RoundedRectangle(cornerRadius: DroppyRadius.medium, style: .continuous)
.padding(DroppySpacing.mdl)No outlines, no gradients, sentence case
Droppy's design language is flat solid surfaces separated by fill contrast:
- No borders or outlines on cards or buttons. Separate surfaces by fill.
- No gradients, with one exception the SDK already owns — the Settings sidebar's icon tiles.
- Sentence case everywhere. Never ALL-CAPS labels, including small ones.
- No decorative dots inside chips or tags. Plain tinted text pills.
Live activity chrome, not cards
A live activity is mini-HUD chrome. DroppyLiveActivityMetrics carries the real numbers: a 37pt row, 13pt glyphs, 12pt labels, 6pt content spacing, and 24pt circular controls. Use them rather than measuring from a screenshot.
Settings panes use the settings components
A droplet's settings pane is rendered inside Droppy's own Settings window. Build it from DropletSettingsCard and its rows, and it will match every native page — including after Droppy's styling changes, because those components are kept in step with the app.
DropletSettingsCard {
DropletToggleRow(title: "Show seconds", subtitle: "Ticks every second.", isOn: $showsSeconds)
DropletSettingsDivider()
DropletControlRow(title: "Clocks") { DropletValuePill(text: "\(count)") }
}Solo and paired are different layouts
A shelf widget alone gets the wide standalone canvas; sharing the row it gets a slice. These are different compositions, not one view at two widths. Switch on context.isCompact, never on a width comparison — see Shelf widgets.
Both notch shapes
context.surface is .builtInNotch on a MacBook and .dynamicIsland on a display without one, where Droppy draws a floating island instead. Content that assumes wings either side of a cut-out has nowhere to go in an island. The harness switches between them so you can check.
Motion
Use DroppyAnimation's presets. Do not invent springs: Droppy's open and close timings are the app's, and a droplet animating on its own curve inside a host spring reads as lag.
Accessibility
Every live activity carries an accessibility title and every HUD an accessibility label — they are required parameters, not options, because a surface this small is one a screen reader user cannot fall back on reading.