Spinner
Display animated spinners during long-running operations:
err := clog.Spinner("Downloading").
Str("url", fileURL).
Wait(ctx, func(ctx context.Context) error {
return download(ctx, fileURL)
}).
Msg("Downloaded")
The spinner animates with moon phase emojis (🌘🌗🌖🌕🌔🌓🌒🌑) while the action runs, then logs the result. This is the default style (spinner.DefaultStyle), which is used when no spinner.WithStyle option is passed.

Dynamic Status Updates
Use Progress to update the spinner message, fields, and symbol during execution:
err := clog.Spinner("Processing").
Progress(ctx, func(ctx context.Context, update *clog.Update) error {
for i, item := range items {
update.Msg("Processing").Str("progress", fmt.Sprintf("%d/%d", i+1, len(items))).Send()
if err := process(ctx, item); err != nil {
return err
}
}
return nil
}).
Msg("Processed all items")
SetSymbol changes the icon beside the message mid-task:
update.SetSymbol("📡").Msg("Connecting").Send()
SetLevel overrides the log level on the final done line:
update.SetLevel(clog.LevelError).SetSymbol("❌").Msg("Failed").Send()
WaitResult Finalisers
| Method | Success behaviour | Failure behaviour |
|---|---|---|
.Msg(s) | Logs at INF with message | Logs at ERR with error string |
.Msgf(…) | Like .Msg with fmt.Sprintf | Like .Msg with fmt.Sprintf |
.Err() | Logs at INF with spinner message | Logs at ERR with error string as msg |
.Send() | Logs at configured level | Logs at configured level |
.Silent() | Returns error, no logging | Returns error, no logging |
.Err() is equivalent to calling .Send() with default settings (no OnSuccess/OnError overrides).
All finalisers return the error from the action. You can chain any field method (.Str(), .Int(), .Bool(), .Duration(), etc.) and .Symbol() on a WaitResult before finalising.
Custom Success/Error Behaviour
Use OnSuccessLevel, OnSuccessMessage, OnErrorLevel, and OnErrorMessage to customise how the result is logged, then call .Send():
// Fatal on error instead of the default error level
err := clog.Spinner("Connecting to database").
Str("host", "db.internal").
Wait(ctx, connectToDB).
OnErrorLevel(clog.LevelFatal).
Send()
When OnErrorMessage is set, the custom message becomes the log message and the original error is included as an error= field. Without it, the error string is used directly as the message with no extra field.
Animated Symbol on Other Animations
The spinner symbol animation can be composed with any animation type using .Spinner(). This cycles the spinner frames in the symbol slot independently of the main animation:
// Progress bar with a spinning symbol instead of a static icon
clog.Bar("Cloning", 100, opts...).
Spinner().
Progress(ctx, task).
Msg("Cloned")
// Pulse with spinning symbol
clog.Pulse("Syncing").
Spinner().
Wait(ctx, sync).
Msg("Synced")
// Custom spinner style on a bar
clog.Bar("Downloading", total).
Spinner(spinner.WithStyle(spinner.Dots)).
Progress(ctx, task).
Msg("Downloaded")
.Spinner() with no arguments uses the default spinner style (moon phases). Pass spinner.Option values to customise:
| Option | Description |
|---|---|
spinner.WithStyle(s) | Replace the entire spinner style |
spinner.WithFrames(fs) | Animation frames (e.g. []string{"⠋","⠙","⠹","⠸"}) |
spinner.WithInterval(d) | Duration per frame (values ≤ 0 keep existing) |
spinner.WithBoomerang() | Ping-pong playback - reverses at each end instead of jumping |
spinner.WithReverse() | Play frames in reverse order |
The spinner animation is driven by wall-clock time, so it continues to animate even when the bar progress is stalled or the pulse/shimmer effect is slow.
When both .Symbol() and .Spinner() are called, .Spinner() takes precedence - the animated symbol is shown during animation, and the static symbol is used for the initial (not-started) and done states.
Default Spinner Style
Set the default spinner style for all spinners on a logger:
clog.SetSpinnerStyle(spinner.Dots)
Individual spinners can still override the default with spinner.WithStyle.
Custom Spinner Style
clog.Spinner("Loading", spinner.WithStyle(spinner.Dot)).
Wait(ctx, action).
Msg("Done")
See fx/spinner/presets.go for the full list of available spinner types.
Individual style properties can be overridden without replacing the entire style:
| Option | Description |
|---|---|
spinner.WithFrames(fs) | Animation frames (e.g. []string{"⠋","⠙","⠹","⠸"}) |
spinner.WithInterval(d) | Duration per frame (values ≤ 0 keep existing) |
spinner.WithBoomerang() | Ping-pong playback - reverses at each end instead of jumping |
spinner.WithReverse() | Play frames in reverse order |
// Custom frames with a slower tick
clog.Spinner("Loading",
spinner.WithFrames([]string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}),
spinner.WithInterval(120*time.Millisecond),
).
Wait(ctx, action).
Msg("Done")
Hyperlink Fields on Animations
Animations support the same clickable hyperlink field methods as events:
clog.Spinner("Building").
Path("dir", "src/").
Line("config", "config.yaml", 42).
Column("loc", "main.go", 10, 5).
URL("docs", "https://example.com").
Link("help", "https://example.com", "docs").
Wait(ctx, action).
Msg("Built")
Elapsed Timer
Add a live elapsed-time field to any animation with .Elapsed(key):
err := clog.Spinner("Processing batch").
Str("batch", "1/3").
Elapsed("elapsed").
Int("workers", 4).
Wait(ctx, processBatch).
Msg("Batch processed")
// INF ✅ Batch processed batch=1/3 elapsed=2s workers=4
The elapsed field respects its position relative to other field methods - it appears between batch and workers in the output above because .Elapsed("elapsed") was called between .Str() and .Int().
The display format uses SetElapsedPrecision (default 0 decimal places), rounds to SetElapsedRound (default 1s), hides values below SetElapsedMinimum (default 1s), and can be fully overridden with SetElapsedFormatFunc. Durations >= 1m use composite format (e.g. “1m30s”, “2h15m”).
Per-Event Parts Override
Override the part order for a spinner and its completion message without mutating the logger:
err := clog.Spinner("Indexing files").
Parts(clog.PartSymbol, clog.PartMessage).
Wait(ctx, indexFiles).
Msg("Indexed")
// ✅ Indexed (no level label or fields)
When set on animations, the override applies to both the animation rendering and the default completion message. You can further override on the WaitResult if the completion needs different parts:
clog.Spinner("Syncing").
Parts(clog.PartMessage). // animation: message only
Wait(ctx, sync).
Parts(clog.PartLevel, clog.PartMessage). // completion: add level back
Msg("Synced")
Non-TTY Silent
By default, animations print a static status line on non-TTY writers (CI, piped output) so the user knows something is in progress. Use NonTTYSilent to suppress this line entirely - the task still runs, only the output is hidden:
err := clog.Spinner("Reticulating splines").
NonTTYSilent(true).
Wait(ctx, reticulateSplines).
Msg("Splines reticulated")
On a terminal, the spinner animates normally. When piped, no output is produced until the completion message.
This is useful for decorative animations that add noise in CI logs. For level-based suppression across all animations, see SetNonTTYLevel.
NonTTYSilent works with all animation types: spinners, bars, pulses, shimmers, and groups.
Delayed Animation
Use .After(d) to suppress the animation for an initial duration. If the task finishes before the delay, no animation is shown at all - useful for operations that are usually fast but occasionally slow:
err := clog.Spinner("Fetching config").
After(time.Second).
Wait(ctx, fetchConfig).
Msg("Config loaded")
If fetchConfig completes in under 1 second, the user sees nothing until the final “Config loaded” message. If it takes longer, the spinner appears after 1 second.