bubble-table: an interactive table component for Bubble Tea
A customizable, interactive table component for the Bubble Tea framework
At a glance
- What is it?
- bubble-table is a Go component for the Bubble Tea terminal UI framework that turns a list of columns and rows into a navigable table, with frozen columns, horizontal scrolling, sorting, filtering, styling overrides and two pagination modes. The data model is deliberately loose, with any value in a cell, and the project holds a 98.3 percent coverage floor in its own Makefile.
- Who is it for?
- bubble-table suits a Go developer building a terminal application that has to show a data set and let the user sort, filter, page and select it, particularly when the data is only known at run time and a static renderer will not do. It does not suit you if your table is short and read-only, because a plain print of the data is less code, and it does not suit a team that wants a graphical interface.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 40 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Tables in a terminal are a different problem
bubble-table is a customizable, interactive table component for the Bubble Tea framework, and the primary language is Go.
The setting matters, because a table in a terminal is not a table in a browser. There is no scrolling container to lean on, no DOM to reflow, and the width available is whatever the terminal happens to be, which changes when the user resizes it. A table component for this environment has to decide how to spend the width, how to signal that more columns exist, and how to keep the cursor somewhere sensible while the data underneath is being sorted or filtered.
Bubble Tea is the Elm architecture framework from Charm, and the dependency list shows this library is built on the current major versions of the Charm stack: charm.land/bubbletea/v2 for the program loop and update model, charm.land/bubbles/v2 from the same Charm stack, and charm.land/lipgloss/v2 for the styling that produces the borders and the coloured cells.
The audience is therefore Go developers building terminal applications, and the topic tags say so plainly: bubble, bubble-tea, bubbles, go, golang, table and tui. The licence is MIT, the module path is github.com/evertras/bubble-table, and the repository has an examples directory that doubles as the documentation.
There are seventeen example directories, and several of them exist purely to answer a specific question a user would otherwise have to guess, which is the clearest sign of how the library is meant to be learned.
Columns, rows, and a cell that can hold anything
The data model is the first thing to understand, and it is unusually permissive.
A table is defined by a list of Column values, each associated with a unique string key. A table contains a list of Rows, and each Row contains a RowData object, which the README describes simply as a map of string column IDs to arbitrary any data values.
Rendering follows directly from that. Each row is checked for each column key, and if the key exists in the row's RowData it is rendered with fmt.Sprintf("%v"). If the key does not exist, nothing is rendered for that cell. Extra data in the RowData object is ignored.
That last part is the design decision worth pausing on. Because unknown keys are ignored rather than an error, you can dump a whole record into RowData and then choose columns to display, and you can change those columns later, for example when the view options change. The metadata example in the examples directory exists to show that.
The construction code looks like this:
columns := []table.Column{
table.NewColumn(columnKeyID, "ID", 5),
table.NewColumn(columnKeyName, "Name", 10),
}Each column carries a key, a title and a width, and the key is what ties the column to the data. The README notes that the keys are arbitrary and only have to be unique, and the sample code says the same thing with a wry comment about one key being a string in a script.
The consequence for a user is that a table is a projection. The data is a bag of values, and a column is a named view of it.
Fixed width, flexible width, frozen columns and horizontal scrolling
Width is where a terminal table gets difficult, and the library offers the three answers you would expect.
Columns can be fixed width, or they can be flexible width, which is what the flex example demonstrates. A maximum width can be specified, and doing so enables horizontal scrolling, with the scrolling example showing it. And left-most columns can be frozen for easier reference, which is the behaviour every spreadsheet user expects and which a naive implementation gets wrong.
Freezing is the interesting one, because it means the table is wider than the terminal and the user needs to be able to scroll horizontally while keeping the identifier column visible. Once a table can be wider than the screen, the focus model has to cope with a row that is partly off screen, and the horizontal scrolling has to move the frozen columns out of the way.
The dimensions example covers the sizing behaviour, and the rowborder example is there because a table with a border on every cell looks very different from one with borders around the table only.
For a user, the practical sequence is usually to start with the simplest example, add fixed widths, then decide whether the data needs horizontal scrolling and frozen columns. The README points at the simplest example for people who just want a default table, and at the full feature example as a code reference for most of what is available.
The style override order is base, column, row, cell
Styling is where a table component earns its keep, and the rule is stated clearly: styles can be applied globally and to columns, rows and individual cells, and when determining overrides the base style is applied first, then column, then row, then cell.
That ordering is a design decision, and it is the one you would want. A global base style sets the table's overall look, a column style says something about a column as a whole, a row style says something about a row, and a cell style wins for that one cell. It also means you can express a rule like right-align every numeric column by setting it at the column level and not repeat it per cell.
The default base style is a basic right-alignment, and the border shape is customizable from a basic thick square default. Colour is changed by applying a base style with lipgloss.NewStyle().BorderForeground(...), so the border colouring is a lipgloss concern rather than a special case in the table.
Styles can also be applied through a style function, which the README points at for zebra striping and data-specific formatting. A function is the right tool when the style depends on the row, which a static style cannot express.
The features example is the reference for all of this, and the README asks people to open an issue if a feature is confusing to use or could use a better example, which is a reasonable way to run a component library with this many options.
Two pagination modes that cannot both be on
Pagination gets a table of options, and one of them is a genuine constraint you need to know about before you write the code.
WithPageSize(n) fixes the number of rows per page, and the auto footer then shows current/total. WithTargetHeight(n) fixes the terminal line height, and the rows per page are calculated from the actual rendered height, with the footer shown only when there is more than one page. Truly fixed height, which never grows or shrinks, is WithTargetHeight(n) combined with WithMinimumHeight(n), where the minimum height pads the bottom when rows do not fill the page. Custom footer text is WithStaticFooter(text), which replaces the automatic page count, and WithFooterVisibility(false) hides the footer altogether.
The constraint is that WithPageSize and WithTargetHeight are mutually exclusive, and the README recommends WithTargetHeight when WithMultiline is enabled, because the height-based mode pages by rendered line count rather than by row count.
That recommendation is the important part. If your rows can wrap onto more than one line, a row-count page size produces a page that overflows the screen, and the table silently renders off the bottom. The height-based mode measures what it actually drew, which is the only approach that survives variable row heights.
The pagination and targetheight examples demonstrate both, and the footer text is part of the API rather than something you draw yourself, which keeps the page indicator in step with the data.
Filtering with a footer text box, and sorting that understands humans
Two features handle the part of a table that a static renderer cannot: narrowing it and ordering it.
Built-in filtering is enabled by setting any columns as filterable, which adds a text box in the footer, and the slash key starts filtering, with the key binding customisable. The filter and filterapi examples show the built-in version and a version driven from your own code, which is what you would use if the filter input belongs somewhere else in your layout.
There is also a missing indicator you can set to show missing data in rows, so that an absent value looks like an absent value rather than an empty cell that might be mistaken for a zero.
Sorting works in either direction, on one column or several. The multi-column rule is specific and worth reading: if multiple columns are specified, first the table is sorted by the first specified column, then each group within that column is sorted in smaller and smaller groups. By default a column is sorted by numeric value if every value in it is a number, by time if every value is a time, and as strings otherwise.
Two escape hatches exist for when the default is wrong. A column's SortType can be set to SortTypeNatural to sort strings containing numbers the way a human would, so item2 comes before item10, and WithSortFunc supplies a custom comparator.
The natural sort is a small thing that matters, because the default string sort puts item10 before item2, and anyone who has shipped a sorted list of filenames knows how that goes.
Go 1.25, the v2 Charm modules, and a coverage floor of 98.3
The build configuration tells you what this project treats as non-negotiable.
The module requires Go 1.25.0 and depends on the current Charm v2 modules, charm.land/bubbletea/v2 at 2.0.5, charm.land/bubbles/v2 at 2.1.0 and charm.land/lipgloss/v2 at 2.0.3, along with github.com/charmbracelet/x/ansi and stretchr/testify for the tests. The repository also carries a .go-version file, a .golangci.yaml, a flake.nix with a flake.lock, and an .editorconfig.
The Makefile is where the project states its quality bar. The test target runs go test with the race detector and coverage over the table package. A separate check-coverage target runs the coverage profile through an awk script and fails the build if the total is below a threshold, and that threshold is a variable named COVERAGE_THRESHOLD set to 98.3. There is a benchmark target that runs the benchmarks with memory reporting, a lint target, and a set of example targets, each of which runs one example directory with go run.
A coverage floor enforced in the build is an unusual and effective practice. It means a new pull request that adds untested code fails before review rather than after.
The release history is a steady line of patch versions with a minor bump every so often: v0.22.2 on 2026-06-04, v0.22.3 on 2026-06-05, and v0.23.0 on 2026-09-09, with the last push to the repository on 2026-09-13. The project is pre-1.0, so the API is not frozen, and it is at version 0.23 after several years, which is the usual pattern for a component that grows one feature at a time.
Editorial conclusion
bubble-table suits a Go developer building a terminal application that has to show a data set and let the user sort, filter, page and select it, particularly when the data is only known at run time and a static renderer will not do. It does not suit you if your table is short and read-only, because a plain print of the data is less code, and it does not suit a team that wants a graphical interface. Check three things before you adopt it: which pagination mode fits your rows, because WithPageSize and WithTargetHeight are mutually exclusive and the target height mode is the one to prefer with multiline cells, how your cells will be styled, since the override order is base, column, row, cell, and whether the terminal you target handles the widths the table needs. The last push to the repository was on 2026-09-13 and the newest release is v0.23.0 from 2026-09-09.
Frequently asked questions
What is bubble-table?
It is a customizable, interactive table component for the Bubble Tea framework, written in Go and MIT licensed. It provides a navigable table with a header, rows, footer and borders, built on the Charm v2 modules for bubbletea, bubbles and lipgloss.
How is table data defined in bubble-table?
A table is a list of Column values, each with a unique string key, and a list of Rows, where each Row holds a RowData map of column IDs to arbitrary values. A cell renders with fmt.Sprintf("%v") when its key is present and renders nothing when it is absent, and extra data in the map is ignored.
What is the difference between WithPageSize and WithTargetHeight?
They are mutually exclusive. WithPageSize fixes the number of rows per page, while WithTargetHeight fixes the terminal line height and calculates the rows from the rendered height, and the README recommends the height based mode when WithMultiline is enabled.
How does bubble-table handle sorting?
Columns can be sorted ascending or descending, and multiple columns can be given, in which case the table sorts by the first column and then sorts each group by the next. A column is sorted numerically or by time when all its values are of that kind, and SortTypeNatural or WithSortFunc override the default.
What are the project's testing standards?
The Makefile runs the tests with the race detector and coverage over the table package, and a check-coverage target fails the build when the total drops below a COVERAGE_THRESHOLD variable set to 98.3. There are also benchmark and lint targets, and seventeen example programs that each run with go run.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/evertras-bubble-table)Was this useful?