bloom

Layout, motion, and effects for Bubble Tea.

Describe your Bubble Tea view as a tree of rows, columns, and layers, built from your model every frame. bloom sizes it to the terminal, draws dialogs and toasts on top, animates what changes, and tells you what the mouse is over.

go get github.com/lrstanley/bloom@latest

Pre-1.0: expect breaking changes until v1.0.0.

The notes app from the tutorials: a modal dialog over a dimmed screen, then a toast.

What bloom takes off your hands

Bubble Tea gives you a model, messages, and a view that returns a string. Most of what makes a terminal app feel finished happens between those, and it's code you'd otherwise write for every program.

Splitting the screen into panes
Measure rendered strings, subtract borders and padding, and redo the math on every resize.
bloom.Row(bloom.Fixed(24, nav), bloom.Fill(main))
Drawing a dialog over the screen
Strings can be joined side by side or stacked, not layered, so you splice lines by hand.
bloom.Overlay(screen, bloom.Layer(dialog).Modal())
Dimming what's behind it
Re-render everything underneath in muted colors, and undo it when the dialog closes.
.Backdrop(fx.Dim{Amount: 0.6})
Showing a toast that hides itself
A timer message, a flag to track it, and a tick loop to slide it in and out.
m.toast.ShowFor(2 * time.Second)
Showing data that's still loading
Placeholder strings sized to match the content by hand, and a tick loop to shimmer them.
bloom.Skeleton(8, 0, 5, 11).Shimmer(t)
Knowing what was clicked
Turn an X and Y back into whichever list item or button you drew there.
id, ok := msg.Target[noteID]()
Keeping focus inside a dialog
Track tab order yourself, and remember where focus was before the dialog opened.
m.focus.Push(confirmDialog)
Animating anything
Tick messages, easing math, and a little state machine for every moving part.
anim.NewPresence(anim.Spring(9, 0.35))
Fitting a narrow terminal
Decide which panels to drop as the window shrinks, and bring them back as it grows.
bloom.Fixed(30, details).Collapsible(1)

One view, the whole screen

This is the View behind the terminal above. The layout, the dialog with its dimmed backdrop and iris entrance, and the toast in the corner are all described here, from model state. There's no separate place where layers are registered or animations are kept running.

  • Slots decide how panes share space: fixed, filling, by weight, or by percentage, with minimums and panels that collapse.
  • Layers sit over the screen, anchored to a corner or to another node, with backdrops, shadows, and modal input.
  • Presences are animated values in your model. Show one, and the layer plays its effects in; hide it, and they play back out.

Components you already have, from Bubbles or your own, go anywhere in the tree as they are.

func (m *model) View() tea.View {
	return m.ui.View(m.width, m.height, bloom.Overlay(
		bloom.Column(
			bloom.Fill(bloom.Row(
				bloom.Fixed(24, sidebar).Collapsible(1),
				bloom.Fill(reader).Min(30),
			)),
			statusbar,
		),
		bloom.Layer(dialog).
			Modal().
			Backdrop(fx.Dim{Amount: 0.6}).
			Presence(m.confirm, fx.Iris{}),
		bloom.Layer(toast).
			Anchor(bloom.End, bloom.End).
			Presence(m.toast,
				fx.Slide{From: bloom.Right}, fx.Typewriter{}),
	))
}

Effects are values

Every effect takes a progress from 0 to 1. The same effect is a transition on a node, a backdrop behind a dialog, or a reaction to something happening, and effects combine in a list.

Where to start

New to Bubble Tea

Build a small notes app from an empty file: panes, clicks, then a dialog and a toast.

Bringing bloom to an existing app

Your model and components stay as they are. Anything with a View() string drops into a layout.

Looking something up

Every effect, node, and option on one page each, and the full API on pkg.go.dev.