Notifications

bubble-overlay ships three ready-to-use Bubble Tea notification models that composite themselves onto any base view using the core overlay.Block primitive.

TypeColorDefault iconConstructor
ErrorRedNewError
WarningYellowNewWarning
InfoCyanNewInfo

Quick example

import overlay "github.com/floatpane/bubble-overlay"

// Inside your tea.Model:
type Model struct {
    notice overlay.Notification
}

func (m Model) Init() tea.Cmd {
    m.notice = overlay.NewError(
        overlay.WithTitle("Connection lost"),
        overlay.WithMessage("Could not reach the server."),
        overlay.WithKey("esc"),
        overlay.WithPosition(2, 4),
    )
    return m.notice.Init()
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    updated, cmd := m.notice.Update(msg)
    m.notice = updated.(overlay.Notification)
    return m, cmd
}

func (m Model) View() string {
    base := m.renderBase()
    return m.notice.PlaceOn(base) // no-op once Done() is true
}

Dismiss modes

1 — Key only (DismissOnKey)

The default. The notification stays until the user presses the configured key. A [key] close hint is shown at the bottom.

n := overlay.NewWarning(
    overlay.WithTitle("Low disk space"),
    overlay.WithKey("q"),               // default is already "q"
)

2 — Timer only (DismissAfterTimer)

Closes automatically after the configured duration. No key dismissal is available. A Unicode progress bar (█░) counts down the remaining time.

n := overlay.NewInfo(
    overlay.WithTitle("Saved"),
    overlay.WithDismissMode(overlay.DismissAfterTimer),
    overlay.WithDuration(3 * time.Second),
)
// Init() must be called (or returned from the parent Init) so ticks start.

3 — Timer with early-exit key (DismissEither)

Closes after the configured duration or when the user presses the key, whichever comes first. Both the progress bar and the key hint are shown.

n := overlay.NewError(
    overlay.WithTitle("Build failed"),
    overlay.WithMessage("See logs for details."),
    overlay.WithDismissMode(overlay.DismissEither),
    overlay.WithDuration(10 * time.Second),
    overlay.WithKey("esc"),
)

Placement

WithPosition(row, col int) sets where the notification is painted over the base view (0-indexed, in terminal cells). The default is (0, 0).

// Top-right corner of a 80×24 terminal, assuming the box is ~46 cells wide
n := overlay.NewInfo(
    overlay.WithPosition(1, 34),
)

PlaceOn(base string) calls overlay.Block internally, so the same cell-accurate, SGR-safe compositing rules apply.

Customising the icon

Each type has a default Unicode icon. Override it with WithIcon:

n := overlay.NewError(WithIcon("🔥"))

Default constants are exported so you can reference them:

overlay.DefaultErrorIcon   // "✗"
overlay.DefaultWarningIcon // "⚠"
overlay.DefaultInfoIcon    // "ℹ"

All options

OptionDefaultDescription
WithTitle(s)""Title shown next to the icon.
WithMessage(s)""Body text shown below the title.
WithKey(s)"q"Key string used to dismiss ("q", "esc", "enter", …).
WithDismissMode(m)DismissOnKeyHow the notification is closed (see above).
WithDuration(d)5sAuto-close duration for timer-based modes.
WithPosition(row, col)(0, 0)Cell position in the base view.
WithIcon(s)type defaultUnicode icon prepended to the header.
WithWidth(n)40Inner content width in terminal cells.

Checking dismissal

if m.notice.Done() {
    // notification has been closed; stop forwarding messages to it
}

Done also gates View and PlaceOn: both are no-ops once the notification is dismissed.

Multiple notifications

Layer them with chained PlaceOn calls:

func (m Model) View() string {
    base := m.renderBase()
    base = m.errNotice.PlaceOn(base)
    base = m.warnNotice.PlaceOn(base)
    return base
}

Later calls paint over earlier ones, so render in back-to-front order.

Tip

Run Init() on each notification model and merge the resulting tea.Cmd values with tea.Batch so all timers start together.

func (m Model) Init() tea.Cmd {
    return tea.Batch(m.errNotice.Init(), m.warnNotice.Init())
}