Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Spinner demo

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

MethodSuccess behaviourFailure behaviour
.Msg(s)Logs at INF with messageLogs at ERR with error string
.Msgf(…)Like .Msg with fmt.SprintfLike .Msg with fmt.Sprintf
.Err()Logs at INF with spinner messageLogs at ERR with error string as msg
.Send()Logs at configured levelLogs at configured level
.Silent()Returns error, no loggingReturns 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:

OptionDescription
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:

OptionDescription
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")
Spinner styles 1 Spinner styles 2 Spinner styles 3 Spinner styles 4 Spinner styles 5 Spinner styles 6 Spinner styles 7 Spinner styles 8 Spinner styles 9

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.