Skip to content

Theme the output

The progress-family renderers (Status, Progress, Spin) draw their icons, colours, spinner frames and bar runes from a Theme. Override it with WithTheme to match your tool's palette.

The default

DefaultTheme() provides check / warning / cross icons, a magenta braille spinner, and an ASCII progress bar:

r := output.New() // uses DefaultTheme()

A custom theme

Start from the default and change only what you need:

th := output.DefaultTheme()
th.SuccessIcon = "✔"
th.FailIcon = "✖"
th.SpinnerFrames = []string{"◐", "◓", "◑", "◒"}
th.ProgressFilled = '█'
th.ProgressEmpty = '░'
th.ProgressHead = 0 // 0 → reuse ProgressFilled for the leading edge

r := output.New(output.WithWriter(os.Stderr), output.WithTheme(th))

Theme fields

Field Used by
SuccessIcon / WarnIcon / FailIcon Status.Success / .Warn / .Fail
SuccessStyle / WarnStyle / FailStyle the lipgloss style of the finalised line
SpinnerStyle spinner frames (Spin, and Status.Update)
SpinnerFrames the animation frame set — see an empty frame set
ProgressFilled / ProgressEmpty / ProgressHead the progress-bar runes

Styles are lipgloss.Style values, so any lipgloss styling (foreground, background, bold, …) applies.

What happens if SpinnerFrames is empty

The two consumers of SpinnerFrames do different things when it is empty, which matters if you build a Theme from scratch rather than starting from DefaultTheme():

  • Spin substitutes the built-in bubbles dot spinner, so the animation still runs.
  • Status.Update renders a single space as the working icon — no dot, no animation. The message still appears; the icon column is blank.

If you want both to animate, set at least one frame.

Does the renderer turn colour off when it is not interactive?

No. WithInteractive(false) changes layout — no in-place redraw, no animation, plain sequential lines — but it does not change styling. A Status.Success call renders its icon through SuccessStyle in both modes, so a non-interactive run still emits the ANSI colour sequence:

\x1b[38;5;42m✓\x1b[m done

The NO_COLOR and TERM=dumb conventions are not consulted either. If your tool needs a --no-color switch, implement it by passing a theme whose styles carry no colour:

plain := output.DefaultTheme()
for _, s := range []*lipgloss.Style{
    &plain.SuccessStyle, &plain.WarnStyle, &plain.FailStyle, &plain.SpinnerStyle,
} {
    *s = lipgloss.NewStyle()
}

r := output.New(output.WithWriter(os.Stderr), output.WithTheme(plain))

An unstyled lipgloss.Style renders its input unchanged, so this produces icons and messages with no escape sequences at all.