DT2 is a modern R binding for DataTables v2 built on
htmlwidgets. It works with or without
Shiny — render interactive tables in R Markdown, Quarto, and
the RStudio Viewer.
With zero configuration, DT2 produces a Bootstrap 5 styled table with search, sorting, pagination, and the Jost font — all from a single function call:
DT2 enables the Responsive extension by default. Tables fill 100% of the available width and gracefully adapt to narrow screens by collapsing columns that don’t fit. This is essential for dashboards and reports viewed on mobile devices or embedded in narrow layout containers:
On wide screens all columns are visible. On narrow screens, columns
that overflow are hidden and accessible via a toggle (+) on
each row — the user can expand any row to reveal the hidden columns in a
child row.
To disable responsive behaviour (for example, when using custom
renderers that inject complex HTML, or when you prefer horizontal
scrolling with scrollX = TRUE):
DT2 provides multiple levels of control over table appearance, from quick inline parameters to reusable theme objects. All styling options cascade: direct arguments override theme values, which override the built-in defaults.
Style parameters are first-class arguments of dt2().
Override any individual setting without creating a theme object — useful
for quick one-off adjustments:
DT2 ships with four built-in presets that cover common use cases. Each preset adjusts striping, compactness, and font scaling to match a particular aesthetic:
| Preset | Striped | Compact | Font scale |
|---|---|---|---|
"default" |
✓ | ✓ | 0.80 |
"clean" |
✓ | — | 0.85 |
"minimal" |
— | — | 0.90 |
"compact" |
✓ | ✓ | 0.75 |
DataTables 2 uses layout to control where elements
(search box, page length selector, buttons, pagination, info text)
appear around the table. This replaces the old dom string
from DataTables v1 with a clean, named-position system that is much
easier to read and maintain.
Each position is a slot in a grid above and below the table. You can
place any element in any slot, combine multiple elements in the same
slot, or set a slot to NULL to remove it entirely.
+------------------+------------------+
| topStart | topEnd |
+------------------+------------------+
| top2Start | top2End |
+------------------+------------------+
| TABLE |
+------------------+------------------+
| bottomStart | bottomEnd |
+------------------+------------------+
| bottom2Start | bottom2End |
+------------------+------------------+
By default, DataTables places:
Move the search box to the left and info to the right:
Set a position to NULL to remove it:
Wrap multiple elements in a list:
Instead of pagination, you can use virtual scrolling for large datasets. The Scroller extension renders only the visible rows and loads more as the user scrolls — ideal for datasets with thousands of rows where pagination would create an overwhelming number of pages.
The table container gets a fixed height (set via scrollY
in pixels) and rows are rendered on demand as the user scrolls, keeping
memory usage low even for very large datasets:
dt2(iris, options = list(
scroller = TRUE,
scrollY = 300, # viewport height in pixels
paging = TRUE # required for Scroller
))DT2 auto-detects the Scroller extension when
scroller = TRUE is set.
With a larger dataset:
big <- data.frame(
id = 1:5000,
value = round(rnorm(5000), 3),
group = sample(LETTERS[1:5], 5000, replace = TRUE)
)
dt2(big, options = list(
scroller = TRUE,
scrollY = 400,
deferRender = TRUE # improves performance: renders rows on demand
))Combine with buttons:
dt2(big, options = list(
scroller = TRUE,
scrollY = 350,
deferRender = TRUE,
buttons = list("copy", "csv", "excel"),
layout = list(
topEnd = "buttons",
topStart = list(search = list(placeholder = "Search..."))
)
))For datasets over ~50k rows, consider server-side processing instead.
See vignette("shiny-integration") for details.
DT2 provides helper functions that build DataTables
columnDefs for common formatting tasks — number separators,
decimal places, prefixes, and suffixes. These modify your options list
in-place and generate the correct JavaScript render
functions behind the scenes, so you don’t need to write any JS:
opts <- list(columns = names(mtcars))
opts <- dt2_format_number(opts, "hp", thousands = ",", digits = 0)
opts <- dt2_format_number(opts, "wt", digits = 2, prefix_right = " tons")
dt2(mtcars[1:10, ], options = opts)For large numbers (population, budgets, revenues), use abbreviated
formatting that automatically converts values to human-readable
suffixes. The locale parameter controls the decimal
separator and number grouping:
df <- data.frame(
city = c("São Paulo", "Recife", "NYC", "Tokyo"),
pop = c(12.33e6, 1.65e6, 8.34e6, 13.96e6),
budget = c(6.5e10, 4.2e9, 1.07e11, 7.36e10)
)
opts <- list(columns = names(df))
opts <- dt2_format_number_abbrev(opts, c("pop", "budget"),
digits = 1, locale = "pt-BR")
dt2(df, options = opts)Every option from the DataTables JS API maps 1:1 to an R named list. If you find an example on the DataTables website, you can translate it directly to R without any wrapper functions. This means the full power of DataTables is available — including options not covered by DT2’s helper functions:
dt2(iris, options = list(
pageLength = 5,
searching = TRUE,
ordering = TRUE,
language = list(search = "Filter:", info = "_TOTAL_ rows")
))See vignette("js-config") for the complete translation
guide.
Any DT2 table can be saved as a standalone HTML file using
htmlwidgets::saveWidget(). The resulting file includes all
CSS and JavaScript — it works offline and can be emailed, hosted, or
embedded in other pages:
DT2 bundles DataTables core, extensions, jQuery, and other JS/CSS dependencies inside the package. You can check whether newer versions are available on npm and CDN without leaving R:
If you are developing DT2 itself, apply updates automatically:
Version constraints prevent breaking upgrades (jQuery stays on 3.x,
pdfmake on 0.2.x). See ?.dt2_version_constraints for
details.
This example brings together most DT2 features in a single, realistic table: ColumnControl dropdown menus in the header for sorting and filtering, export buttons separated by a visual spacer, custom JavaScript renderers (country flags via CSS sprites, coloured salary values, progress bars), and a complete Portuguese (Brazil) translation.
Copy and run it directly — the full 57-row version is also available
at
system.file("examples/app_complete.R", package = "DT2").
library(jsonlite)
library(dplyr)
library(tibble)
library(lubridate)
library(DT2)
library(htmlwidgets)
# ── Flag sprite CSS (carregado via dependency para garantir que entra no HTML) ──
flag_dep <- htmltools::htmlDependency(
name = "world-flags-sprite",
version = "0.0.1",
head = '<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lafeber/world-flags-sprite/stylesheets/flags32-both.css">',
src = c(href = ".")
)
# ── Dados ─────────────────────────────────────────────────────────────────────
json_txt <- '{
"data": [
{"name":"Tiger Nixon","position":"System Architect","salary":"320800","start_date":"2011-04-25","office":"Edinburgh","extn":"5421"},
{"name":"Garrett Winters","position":"Accountant","salary":"170750","start_date":"2011-07-25","office":"Tokyo","extn":"8422"},
{"name":"Ashton Cox","position":"Junior Technical Author","salary":"86000","start_date":"2009-01-12","office":"San Francisco","extn":"1562"},
{"name":"Cedric Kelly","position":"Senior JavaScript Developer","salary":"433060","start_date":"2012-03-29","office":"Edinburgh","extn":"6224"},
{"name":"Airi Satou","position":"Accountant","salary":"162700","start_date":"2008-11-28","office":"Tokyo","extn":"5407"},
{"name":"Brielle Williamson","position":"Integration Specialist","salary":"372000","start_date":"2012-12-02","office":"New York","extn":"4804"},
{"name":"Herrod Chandler","position":"Sales Assistant","salary":"137500","start_date":"2012-08-06","office":"San Francisco","extn":"9608"},
{"name":"Rhona Davidson","position":"Integration Specialist","salary":"327900","start_date":"2010-10-14","office":"Tokyo","extn":"6200"},
{"name":"Colleen Hurst","position":"JavaScript Developer","salary":"205500","start_date":"2009-09-15","office":"San Francisco","extn":"2360"},
{"name":"Sonya Frost","position":"Software Engineer","salary":"103600","start_date":"2008-12-13","office":"Edinburgh","extn":"1667"}
]
}'
df <- fromJSON(json_txt, flatten = TRUE)$data %>%
as_tibble() %>%
mutate(
salary = as.numeric(salary),
extn = as.integer(extn),
start_date = ymd(start_date)
)
# ── JS Renderers ──────────────────────────────────────────────────────────────
office_js <- JS("
function(data, type) {
if (type !== 'display') return data;
var cc = {Argentina:'ar', Edinburgh:'_Scotland', London:'_England',
'New York':'us', 'San Francisco':'us', Sydney:'au', Tokyo:'jp'};
return '<span class=\"flag ' + (cc[data]||'') + '\"></span> ' + data;
}
")
salary_js <- JS("
(function() {
var nfmt = DataTable.render.number('.', ',', 2, 'R$ ');
return function(data, type) {
var txt = nfmt.display(data);
if (type !== 'display') return txt;
var c = data < 250000 ? 'red' : data < 500000 ? 'orange' : 'green';
return '<span style=\"color:' + c + '\">' + txt + '</span>';
};
})()
")
extn_js <- JS("
function(data, type) {
return type === 'display'
? '<progress value=\"' + data + '\" max=\"9999\"></progress>'
: data;
}
")
# ── Tabela ────────────────────────────────────────────────────────────────────
w <- dt2(df,
compact = TRUE,
striped = TRUE,
hover = TRUE,
font_scale = 0.85,
responsive = FALSE,
options = list(
pageLength = 10,
lengthMenu = c(5, 10, 25, -1),
columns = names(df),
scrollX = TRUE,
layout = list(
topStart = "pageLength",
topEnd = list(
buttons = list(
list(extend = "copyHtml5", text = "Copiar"),
list(extend = "csvHtml5"),
list(extend = "excelHtml5"),
list(extend = "spacer", style = "bar"),
list(extend = "colvis", text = "Colunas")
),
search = list(placeholder = "")
),
bottomEnd = list(paging = list(numbers = FALSE))
),
columnControl = list(
target = 0,
content = list("order", "searchDropdown", list(
list(extend = "orderAsc", text = "Ordem crescente"),
list(extend = "orderDesc", text = "Ordem decrescente"),
"spacer",
list(extend = "colVisDropdown", text = "Selecionar colunas")
))
),
ordering = list(indicators = FALSE, handler = FALSE),
columnDefs = list(
list(targets = which(names(df) == "office") - 1L,
className = "f32", render = office_js),
list(targets = which(names(df) == "salary") - 1L,
className = "dt-body-right", render = salary_js),
list(targets = which(names(df) == "extn") - 1L,
render = extn_js)
),
language = list(
lengthMenu = "Mostrar _MENU_",
search = "Buscar",
info = "Mostrando _START_ a _END_ de _TOTAL_ registros",
infoEmpty = "Nenhum registro",
zeroRecords = "Nenhum registro encontrado",
emptyTable = "Nenhum dado disponível",
decimal = ",", thousands = ".", infoThousands = ".",
lengthLabels = list(`10` = "10", `25` = "25", `-1` = "Todas"),
paginate = list(first = "«", previous = "‹", `next` = "›", last = "»"),
buttons = list(
copyTitle = "Copiado!",
copySuccess = list(`_` = "%d linhas copiadas", `1` = "1 linha copiada")
),
columnControl = list(
orderAsc = "Crescente", orderDesc = "Decrescente",
searchDropdown = "Pesquisar", colVisDropdown = "Colunas",
searchClear = "Limpar",
search = list(
text = list(contains = "Contém", starts = "Começa por",
ends = "Termina em", equal = "Igual a"),
number = list(greater = "Maior que", less = "Menor que",
equal = "Igual a")
)
)
)
)
)
# Anexa a dependency do flag sprite ao widget
w$dependencies <- c(w$dependencies, list(flag_dep))
wvignette("extensions-guide") — Select, Responsive,
ColumnControl, SearchBuilder, and more.vignette("shiny-integration") —
proxy, events, SSP.vignette("js-config") —
translating datatables.net examples to R, advanced layout,
callbacks.vignette("formatting") —
all column formatting helpers.