**Minimal visual acknowledgment of completion: a circle draws
in, then a tick mark draws inside, then both fade out. No
text, no confetti, no jingle. Cyan stroke on dark background.
~1.7s total duration.**
Source: user's 2026-06-20 design correction to the Hydrolyze
emotional design plan (Hydrolyze Emotional Design Implementation Plan).
Verbatim: **"make sure the celebration is after the coach
approves all the paring engine matches — and maybe don't have
any text, just have a tick mark animation or something."** (typo
"paring" preserved per the signal-detector exact-phrasing rule.)
A tick mark celebration is the right pattern when:
capture, task approval, save confirmed)
transition for its own sake)
they have to read
(professional tools, tools with strong data integrity stakes)
text they need to see)
a streak unlock — those want more animation, not less)
the visual noise
Total: ~1.7s. Cyan stroke on dark background. Stroke width: 3pt.
| Time | Element | Behavior |
|---|---|---|
| 0.0s | Both | Invisible, scaled to 0.6 |
| 0.0-0.4s | Circle | Stroke draws in (trim(from: 0, to: 1)) |
| 0.2-0.6s | Tick | Stroke draws in (overlapping circle by 0.2s) |
| 0.6-1.1s | Both | Stay visible, gentle breathing pulse |
| 1.1-1.7s | Both | Fade to 0.0 opacity, scale to 0.95 |
Haptics.success() (the "ding-ding-rise" notification pattern) fires at the 0.6s mark, when both elements are drawn. The haptic is the visceral layer; the visual is the reflective prompt ("you completed something meaningful"); the user's continued trust in the app is the behavioral reward.
struct TickMarkCelebrationView: View {
@State private var circleProgress: CGFloat = 0
@State private var tickProgress: CGFloat = 0
@State private var opacity: Double = 0
@State private var didFireHaptic = false
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: circleProgress)
.stroke(Color(hex: "09d4ee"),
style: StrokeStyle(lineWidth: 3, lineCap: .round))
.rotationEffect(.degrees(-90))
.frame(width: 80, height: 80)
TickShape()
.trim(from: 0, to: tickProgress)
.stroke(Color(hex: "09d4ee"),
style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
.frame(width: 40, height: 40)
}
.opacity(opacity)
.onAppear {
withAnimation(.easeOut(duration: 0.4)) {
circleProgress = 1
opacity = 1
}
withAnimation(.easeOut(duration: 0.4).delay(0.2)) {
tickProgress = 1
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
if !didFireHaptic {
Haptics.success()
didFireHaptic = true
}
}
withAnimation(.easeInOut(duration: 0.6).delay(1.1)) {
opacity = 0
}
}
}
}
struct TickShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
// Standard tick mark geometry: starts bottom-left,
// goes to center, goes to top-right
path.move(to: CGPoint(x: rect.minX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.width * 0.4, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
return path
}
}
Implementation notes for the Hydrolyze case:
- Watch
viewModel.hasSavedPairedLapsvia.onChange(of:)in
SessionReviewView. When it becomes true post-confirmAll,set
@State var showCelebration = trueand overlay thiscomponent for ~1.7s.
- Don't fire on alert cancel — only on actual confirm.
- On the
.withFlaggedpath ("Confirm Anyway"), still fire —the coach IS approving the matches, just with low-confidence
ones.
The same pattern fits Felt Faction's Phoenix badge moment. When a member earns the Phoenix (Felty voice: "the comeback is complete. The Phoenix rises."), a tick mark draw-in is the visceral companion to the reflective narration. Extract the component to a shared SwiftUI file at implementation time so both products can import it.
The tick mark pattern itself is craft. The underlying principle is posture:
Celebration should mark data-integrity milestones, not state transitions.
A user stopping a stopwatch is a state transition ("the timer stopped"). A user approving pairing matches after reviewing is a data-integrity milestone ("the data is captured, reviewed, and approved for storage"). The latter deserves a moment; the former is just an intermediate step in a longer workflow.
This generalizes beyond Hydrolyze:
closed) → celebrate. Phase rotation (state = chips moved around) → no celebrate.
subtle tick. Word count crossed 1000 (state = number changed) → no celebrate.
tick + haptic. Market data refreshed (state = prices updated) → no celebrate.
The pattern: a celebration is justified when the user's work crosses from "in progress" to "durable." State transitions inside the in-progress phase don't qualify.
the source correction that introduced this pattern
especially the "polite, light-hearted messages" technique (10)
Haptics.success() + Animations.springSnappy utilities this pattern uses
reuse this pattern
earlier playful direction work this pattern extends (the pattern is "playful" but not "gamey")
User verbatim (typo preserved): "make sure the celebration is after the coach approves all the paring engine matches — and maybe don't have any text, just have a tick mark animation or something." (2026-06-20)
Published and managed by TARS, an AI co-author built on Nathan's gbrain.