tibble tribble() in R: Build Tibbles Row by Row
The tribble() function in the tibble package builds tibbles row-by-row using a transposed formula syntax, ideal for small lookup tables, fixtures, and test data where the layout reads naturally as rows.
tribble(~x, ~y, 1, "a", 2, "b") # 2 rows, 2 cols tribble(~name, ~score, "Ana", 91) # one row tribble(~id, ~tag, 1, "ok", 2, NA) # NA values allowed tribble(~a, ~b, ~c, 1, 2, 3, 4, 5, 6) # 3 cols, 2 rows tribble(~`bad name`, ~x, 1, 2) # back-ticked names tribble(~k, ~v, "a", list(1:3)) # list-columns work tribble(~x, ~y) # empty 0-row shell
Need explanation? Read on for examples and pitfalls.
What tribble() does in one sentence
tribble() builds a tibble row-by-row using a transposed layout. You pass column names as formulas (~name), then list the cell values in row-major order. The function returns a tibble with one row per group of values that matches the column count.
The name is short for "transposed tibble". Where tibble() reads column-first (you write a whole column, then the next column), tribble() reads row-first (you write the headers, then row 1's values, then row 2's). For small tables you would otherwise type by hand, this layout matches how the data looks on paper, which is why test fixtures, lookup tables, and documentation examples lean on it.
Syntax
tribble() accepts formula-headers followed by cell values. Headers start with ~ and are listed first. Cell values follow, separated by commas, in row-major order: row 1 column 1, row 1 column 2, then row 2 column 1, and so on.
The full signature is:
tribble(...)
There is only one argument: the variadic .... tribble decides where headers end and rows begin by reading until it sees a non-formula value. After that, every value is a cell. Column types are inferred from the values, using the same coercion rules as c().
tribble() is that the source code looks like the table. Use spaces to align cells under their column headers. Editors with a fixed-width font let you see the table shape at a glance, which catches missing or extra values immediately.Six common patterns
1. Author a small lookup table
Lookup tables are the canonical use case. Storing the mapping inline keeps the join visible in the same script that uses it.
2. Build a fixture for tests
Each row is a test case. Iterating with purrr::pmap() or a simple for loop checks them all.
3. Mix types and NA values
NA works as a value. Columns infer their type from the non-NA cells.
4. Non-syntactic column names with back-ticks
tribble() keeps these names exactly. No make.names() mangling.
5. List-columns inside a row layout
Each cell holds an R object. Useful for ragged data or nested model results.
6. Empty-shell tibble with named columns
Useful when a function should return a typed empty frame. Column types default to logical until you populate the first row.
tribble() vs tibble() vs data.frame()
Use tribble() when the table is small and the visual layout matters. Below roughly 30 rows or wherever the author wants the code to mirror the printed table, tribble() reads more naturally than its column-first cousin.
| Behavior | tribble() |
tibble() |
data.frame() |
|---|---|---|---|
| Layout | Row-by-row | Column-by-column | Column-by-column |
| Header syntax | ~name formulas |
name = vector |
name = vector |
| Best for | Small tables, fixtures | Vectors of any length | Legacy code |
| Refer to prior col | No | Yes | No |
| Type coercion | Per column from values | Per vector | Per vector |
| Row names | None | None | Always |
| String to factor | Never | Never | Was default before R 4.0 |
When to use which:
- Use
tribble()for small inline tables that should read like a printed table. - Use
tibble()when you already have vectors or want to compute one column from another. - Use
data.frame()when you need legacy compatibility or want to avoid the tibble dependency.
tribble() is a writing tool, not a runtime tool. Once the table grows past 30 rows, the formula layout stops helping and starts hiding mistakes. Past that size, switch to a CSV file plus read_csv(), or generate the rows programmatically with tibble(). The row-by-row layout earns its keep on tables you would otherwise type into a Markdown document or paste from a spreadsheet.Common pitfalls
Pitfall 1: wrong number of values. Values must be a multiple of the column count. Anything else errors.
Pitfall 2: forgetting the ~ on headers. tribble() decides where headers end by scanning for the first non-formula value. A header without ~ becomes data, shifting every column.
tribble(~v, 1, "two") gives v as <chr>, not a mix. Check the printed column type if a downstream join or filter fails unexpectedly.Pitfall 3: trying to use it for large data. tribble() parses every value at call time. For tables past a few hundred rows, prefer a CSV file with read_csv() or a vector-based tibble() call. Generating thousands of rows via tribble() is slow and brittle.
Try it yourself
Try it: Build a tibble named ex_students with three rows and three columns (name, score, passed). Use the values Aki/88/TRUE, Mei/55/FALSE, Sol/72/TRUE.
Click to reveal solution
Explanation: Every header starts with ~. Cell values follow in row-major order, three values per row to match three headers. Column types (<chr>, <dbl>, <lgl>) are inferred from the values.
Related tibble functions
After tribble(), look at:
tibble(): build a tibble column-by-column from named vectors.as_tibble(): convert adata.frame, matrix, or list into a tibble.enframe(): convert a named vector into a two-column tibble.add_row()andadd_column(): extend an existing tibble with more rows or columns.glimpse(): print a tibble structure horizontally, one line per column.
For the full constructor reference, see the official tibble documentation.
FAQ
What is the difference between tibble() and tribble() in R?
tibble() builds a data frame column-by-column from named vectors: tibble(x = 1:3, y = c("a", "b", "c")). tribble() builds the same kind of data frame row-by-row using formula headers: tribble(~x, ~y, 1, "a", 2, "b", 3, "c"). Both return an object of class tbl_df. Use tribble() for small inline tables where the source code should mirror the printed layout. Use tibble() when you already have vectors or want a column to reference a prior one.
How do you create a tibble row by row in R?
Call tribble() with column headers as formulas, then list cell values in row-major order. Example: tribble(~id, ~name, 1, "Ana", 2, "Bo") creates a two-row, two-column tibble. The number of cell values must be a multiple of the number of headers, otherwise you get an "Data must be rectangular" error. Align cells visually under their headers so the source reads like the printed table.
Is tribble() part of base R?
No. tribble() lives in the tibble package, which is part of the tidyverse. Install it with install.packages("tibble") or get it as part of install.packages("tidyverse"). After loading with library(tibble), the function is available directly. The base R equivalent would be writing out a data.frame() call column-by-column, which is harder to read for small fixed tables.
Can tribble() handle NA and list-columns?
Yes to both. Bare NA is a valid cell value, and the column type is inferred from the non-NA cells: tribble(~x, 1, NA, 3) gives an <dbl> column. List-columns work by wrapping cell values in list(): tribble(~k, ~v, "a", list(1:3)). The cells store the R object as-is, useful for nested data or per-row model outputs.
Why use tribble() instead of read_csv() for small tables?
For tables under about 30 rows that ship with code (lookup maps, test fixtures, examples), tribble() keeps the data in the same file as the code that uses it. There is no file to ship, no path to manage, and the table is version-controlled along with the script. Beyond 30 rows the visual layout stops paying off; switch to a CSV plus read_csv() at that point.