Charm Bracelet
Examples

Table resize

What This Example Shows

This example demonstrates how to build a responsive table in a Bubble Tea app that automatically adapts when the terminal window is resized. It ensures your UI remains clean and readable across different terminal sizes.

The magic comes from handling tea.WindowSizeMsg, which Bubble Tea sends whenever the terminal is resized. You can then adjust your layout dynamically.

Why Resizing Matters

Imagine you're working with a table of data in your terminal. If you resize the window and the table doesn't adjust, the UI becomes broken or unreadable. By listening to resize events, your app can respond gracefully and maintain a consistent layout.

If you ignore resize events, your UI may overflow, clip, or display incorrectly when users change terminal dimensions.

Here’s what happens:

  • Bubble Tea detects when the terminal window size changes
  • A tea.WindowSizeMsg is sent with the new width and height
  • Your app updates the table dimensions accordingly
  • The table re-renders cleanly for the new terminal size

How It Works

The App State

We store a table inside our model:

type model struct {
    table *table.Table
}

Handling Resize Events

When Bubble Tea sends a tea.WindowSizeMsg, we update the table’s width and height:

case tea.WindowSizeMsg:
    m.table = m.table.Width(msg.Width)
    m.table = m.table.Height(msg.Height)

Rendering the Table

The View function simply calls m.table.String(), which prints the table with the current styles and dimensions.

By combining tea.WindowSizeMsg with lipgloss/table, you get a fully responsive terminal table with custom styling.

Code Breakdown

The model stores the table instance, which represents the entire UI state.

type model struct {
    table *table.Table
}

The update function processes incoming messages. Resize events adjust table dimensions, while key presses allow quitting the program.

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    var cmd tea.Cmd
    switch msg := msg.(type) {
    case tea.WindowSizeMsg:
        m.table = m.table.Width(msg.Width)
        m.table = m.table.Height(msg.Height)
    case tea.KeyMsg:
        switch msg.String() {
        case "q", "ctrl+c":
            return m, tea.Quit
        }
    }
    return m, cmd
}

The view function renders the table by returning its string output.

func (m model) View() string {
    return "\n" + m.table.String() + "\n"
}

The main function sets up table styles, data, and starts the Bubble Tea program.

func main() {
    re := lipgloss.NewRenderer(os.Stdout)
    baseStyle := re.NewStyle().Padding(0, 1)
    headerStyle := baseStyle.Foreground(lipgloss.Color("252")).Bold(true)
    selectedStyle := baseStyle.Foreground(lipgloss.Color("#01BE85")).Background(lipgloss.Color("#00432F"))

    headers := []string{"#", "NAME", "TYPE 1", "TYPE 2", "JAPANESE", "OFFICIAL ROM."}
    rows := [][]string{
        {"1", "Bulbasaur", "Grass", "Poison", "フシギダネ", "Bulbasaur"},
        {"2", "Ivysaur", "Grass", "Poison", "フシギソウ", "Ivysaur"},
        {"25", "Pikachu", "Electric", "", "ピカチュウ", "Pikachu"},
        {"6", "Charizard", "Fire", "Flying", "リザードン", "Lizardon"},
    }

    t := table.New().
        Headers(headers...).
        Rows(rows...).
        Border(lipgloss.NormalBorder()).
        BorderStyle(re.NewStyle().Foreground(lipgloss.Color("238"))).
        StyleFunc(func(row, col int) lipgloss.Style {
            if row == 0 {
                return headerStyle
            }
            if rows[row-1][1] == "Pikachu" {
                return selectedStyle
            }
            return baseStyle
        })

    m := model{t}
    if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil {
        fmt.Println("Error running program:", err)
        os.Exit(1)
    }
}

Final Code

package main

import (
	"fmt"
	"os"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/charmbracelet/lipgloss/table"
)

type model struct {
	table *table.Table
}

func (m model) Init() tea.Cmd { return nil }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmd tea.Cmd
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.table = m.table.Width(msg.Width)
		m.table = m.table.Height(msg.Height)
	case tea.KeyMsg:
		switch msg.String() {
		case "q", "ctrl+c":
			return m, tea.Quit
		case "enter":
		}
	}
	return m, cmd
}

func (m model) View() string {
	return "\n" + m.table.String() + "\n"
}

func main() {
	re := lipgloss.NewRenderer(os.Stdout)
	baseStyle := re.NewStyle().Padding(0, 1)
	headerStyle := baseStyle.Foreground(lipgloss.Color("252")).Bold(true)
	selectedStyle := baseStyle.Foreground(lipgloss.Color("#01BE85")).Background(lipgloss.Color("#00432F"))
	typeColors := map[string]lipgloss.Color{
		"Bug":      lipgloss.Color("#D7FF87"),
		"Electric": lipgloss.Color("#FDFF90"),
		"Fire":     lipgloss.Color("#FF7698"),
		"Flying":   lipgloss.Color("#FF87D7"),
		"Grass":    lipgloss.Color("#75FBAB"),
		"Ground":   lipgloss.Color("#FF875F"),
		"Normal":   lipgloss.Color("#929292"),
		"Poison":   lipgloss.Color("#7D5AFC"),
		"Water":    lipgloss.Color("#00E2C7"),
	}
	dimTypeColors := map[string]lipgloss.Color{
		"Bug":      lipgloss.Color("#97AD64"),
		"Electric": lipgloss.Color("#FCFF5F"),
		"Fire":     lipgloss.Color("#BA5F75"),
		"Flying":   lipgloss.Color("#C97AB2"),
		"Grass":    lipgloss.Color("#59B980"),
		"Ground":   lipgloss.Color("#C77252"),
		"Normal":   lipgloss.Color("#727272"),
		"Poison":   lipgloss.Color("#634BD0"),
		"Water":    lipgloss.Color("#439F8E"),
	}
	headers := []string{"#", "NAME", "TYPE 1", "TYPE 2", "JAPANESE", "OFFICIAL ROM."}
	rows := [][]string{
		{"1", "Bulbasaur", "Grass", "Poison", "フシギダネ", "Bulbasaur"},
		{"2", "Ivysaur", "Grass", "Poison", "フシギソウ", "Ivysaur"},
		{"3", "Venusaur", "Grass", "Poison", "フシギバナ", "Venusaur"},
		{"4", "Charmander", "Fire", "", "ヒトカゲ", "Hitokage"},
		{"5", "Charmeleon", "Fire", "", "リザード", "Lizardo"},
		{"6", "Charizard", "Fire", "Flying", "リザードン", "Lizardon"},
		{"7", "Squirtle", "Water", "", "ゼニガメ", "Zenigame"},
		{"8", "Wartortle", "Water", "", "カメール", "Kameil"},
		{"9", "Blastoise", "Water", "", "カメックス", "Kamex"},
		{"10", "Caterpie", "Bug", "", "キャタピー", "Caterpie"},
		{"11", "Metapod", "Bug", "", "トランセル", "Trancell"},
		{"12", "Butterfree", "Bug", "Flying", "バタフリー", "Butterfree"},
		{"13", "Weedle", "Bug", "Poison", "ビードル", "Beedle"},
		{"14", "Kakuna", "Bug", "Poison", "コクーン", "Cocoon"},
		{"15", "Beedrill", "Bug", "Poison", "スピアー", "Spear"},
		{"16", "Pidgey", "Normal", "Flying", "ポッポ", "Poppo"},
		{"17", "Pidgeotto", "Normal", "Flying", "ピジョン", "Pigeon"},
		{"18", "Pidgeot", "Normal", "Flying", "ピジョット", "Pigeot"},
		{"19", "Rattata", "Normal", "", "コラッタ", "Koratta"},
		{"20", "Raticate", "Normal", "", "ラッタ", "Ratta"},
		{"21", "Spearow", "Normal", "Flying", "オニスズメ", "Onisuzume"},
		{"22", "Fearow", "Normal", "Flying", "オニドリル", "Onidrill"},
		{"23", "Ekans", "Poison", "", "アーボ", "Arbo"},
		{"24", "Arbok", "Poison", "", "アーボック", "Arbok"},
		{"25", "Pikachu", "Electric", "", "ピカチュウ", "Pikachu"},
		{"26", "Raichu", "Electric", "", "ライチュウ", "Raichu"},
		{"27", "Sandshrew", "Ground", "", "サンド", "Sand"},
		{"28", "Sandslash", "Ground", "", "サンドパン", "Sandpan"},
	}

	t := table.New().
		Headers(headers...).
		Rows(rows...).
		Border(lipgloss.NormalBorder()).
		BorderStyle(re.NewStyle().Foreground(lipgloss.Color("238"))).
		StyleFunc(func(row, col int) lipgloss.Style {
			if row == 0 {
				return headerStyle
			}

			rowIndex := row - 1
			if rowIndex < 0 || rowIndex >= len(rows) {
				return baseStyle
			}

			if rows[rowIndex][1] == "Pikachu" {
				return selectedStyle
			}

			even := row%2 == 0

			switch col {
			case 2, 3: // Type 1 + 2
				c := typeColors
				if even {
					c = dimTypeColors
				}

				if col >= len(rows[rowIndex]) {
					return baseStyle
				}

				color, ok := c[rows[rowIndex][col]]
				if !ok {
					return baseStyle
				}
				return baseStyle.Foreground(color)
			}

			if even {
				return baseStyle.Foreground(lipgloss.Color("245"))
			}
			return baseStyle.Foreground(lipgloss.Color("252"))
		}).
		Border(lipgloss.ThickBorder())

	m := model{t}
	if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil {
		fmt.Println("Error running program:", err)
		os.Exit(1)
	}
}

How is this guide?