Tick Mark Celebration Pattern

Concept · refreshed Search related

**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.)

When to use this pattern

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)

When NOT to use this pattern

text they need to see)

a streak unlock — those want more animation, not less)

the visual noise

The animation sequence

Total: ~1.7s. Cyan stroke on dark background. Stroke width: 3pt.

TimeElementBehavior
0.0sBothInvisible, scaled to 0.6
0.0-0.4sCircleStroke draws in (trim(from: 0, to: 1))
0.2-0.6sTickStroke draws in (overlapping circle by 0.2s)
0.6-1.1sBothStay visible, gentle breathing pulse
1.1-1.7sBothFade to 0.0 opacity, scale to 0.95

The haptic companion

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.

Implementation sketch (SwiftUI)

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.hasSavedPairedLaps via .onChange(of:) in

SessionReviewView. When it becomes true post-confirmAll,

set @State var showCelebration = true and overlay this

component for ~1.7s.

- Don't fire on alert cancel — only on actual confirm.

- On the .withFlagged path ("Confirm Anyway"), still fire —

the coach IS approving the matches, just with low-confidence

ones.

Cross-project reuse

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.

Cross-cutting observation (posture, not craft)

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.

See also

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")

Source

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.