Lightweight tables for R, inspired by gt.
lt provides a small grammar of tables that covers the structure most reports need — titles, column spanners, row groups, footnotes, and number formatting — without the heavy dependency stack. It targets HTML only (no LaTeX or RTF), which keeps the implementation minimal: the entire runtime is a single vanilla-JS file (about 10 KB minified).
# CRAN version
install.packages("lt")
# development version
install.packages("lt", repos = "https://yihui.r-universe.dev")You may also play with the package at https://pkg.yihui.org/lt/playground/ without installing it.
lt() creates a table object from a data frame. The lt_*() functions build
on it via the pipe. See https://pkg.yihui.org/lt/examples/01-lt#sec:cheatsheet
for a "cheat table" as an overview of these functions.
Structure
lt_header()— title and optional subtitle above the table.lt_spanner()— column-spanner label spanning a group of columns; or auto-infer spanners from column name prefixes.lt_group()— partition rows into labeled groups (rowspan or full-width separator style), either by column values or manual row indices.
Content & labels
lt_label()— override column header labels.lt_footnote()— attach a numbered footnote to any region (title, subtitle, column, spanner, group, or body cells).lt_note()— append a plain unnumbered note below the table.lt_html()— render selected columns' cells as raw HTML instead of escaping them. For raw HTML in text (title, labels, footnotes, ...), wrap the text inI()in the corresponding function.
Formatting
lt_format()— numeric formatting: decimal places, thousand separators, prefix/suffix, percentage, scientific notation, etc.lt_date()— date/datetime formatting using the browser locale.lt_sub()— substitute specific values (e.g., replace0with"—"orNAwith"n/a").lt_merge()— merge several columns into one using a sprintf-style pattern.lt_indent()— indent selected rows (useful for hierarchical row labels).
Appearance
lt_align()— set column alignment (left / center / right).lt_width()— set column widths.lt_style()— apply CSS classes or inline styles to cells, conditionally or unconditionally.lt_css()— attach an external CSS file or URL to the table.
Column order
lt_move()— reorder columns.
Export
lt_export()— save a table to a file:.html(optionally baking a static<table>that needs no JavaScript to view),.pdf, or.png(rendered via a headless Chromium browser).
Shiny
lt_output()/render_lt()— Shiny UI and server bindings.
The R code below builds a table spec. Under the hood lt serializes it to a
compact JSON object and ships it to the browser, where a tiny vanilla-JS runtime
renders the <table>.
library(lt)
d = data.frame(
Group = c("Treatment", "Treatment", "Control", "Control"),
Endpoint = c("Primary", "Secondary", "Primary", "Secondary"),
Estimate = c(0.6123, 0.7891, 0.4567, 0.5432),
CI_Lower = c(0.4012, 0.5678, 0.2345, 0.3210),
CI_Upper = c(0.8234, 1.0104, 0.6789, 0.7654),
P_Value = c(0.0012, NA, 0.1234, NA)
)
lt(d) |>
lt_group(~ Group) |>
lt_header("Study Results", "Primary and secondary endpoints") |>
lt_spanner(`95% CI` ~ CI_Lower + CI_Upper) |>
lt_format(~ Estimate + CI_Lower + CI_Upper, decimals = 3) |>
lt_sub(~ P_Value, missing = "—") |>
lt_footnote("Two-sided p-value from log-rank test.", "column", ~ P_Value)The same table can be built directly in JavaScript. Load lt.js once on the
page, then call LT.build() from an inline <script> with the JSON spec:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xiee/utils/css/lt.min.css">
<script src="https://cdn.jsdelivr.net/npm/@xiee/utils/js/lt.min.js"></script><script>
LT.build({
"data": {
"Group": ["Treatment", "Treatment", "Control", "Control"],
"Endpoint": ["Primary", "Secondary", "Primary", "Secondary"],
"Estimate": [0.6123, 0.7891, 0.4567, 0.5432],
"CI_Lower": [0.4012, 0.5678, 0.2345, 0.3210],
"CI_Upper": [0.8234, 1.0104, 0.6789, 0.7654],
"P_Value": [0.0012, null, 0.1234, null]
},
"ops": [
{ "type": "fmt_number", "columns": ["Estimate", "CI_Lower", "CI_Upper"], "decimals": 3 },
{ "type": "sub", "columns": ["P_Value"], "missing": "—" }
],
"row_group": ["Group"],
"header": { "title": "Study Results", "subtitle": "Primary and secondary endpoints" },
"spanners": [{ "label": "95% CI", "columns": ["CI_Lower", "CI_Upper"] }],
"footnotes": [{
"text": "Two-sided p-value from log-rank test.",
"location": { "type": "column_labels", "columns": ["P_Value"] }
}]
});
</script>LT.build() renders the table in place of the calling <script> tag. One
lt.js inclusion handles any number of tables on the page.
You can find more examples at https://pkg.yihui.org/lt/examples.html.
This package was written with the help of Claude Code. lt is directly inspired by gt by Rich Iannone and the RStudio/Posit team. The grammar of tables that gt pioneered — layering titles, spanners, footnotes, and formatters onto a data frame — is a great idea; lt aims to provide a minimal re-implementation for contexts where a lighter footprint is preferred.