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.WindowSizeMsgis 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
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?