All examples below are complete Shiny apps using
bslib with Bootstrap 5. Copy, paste, and run.
Basic Usage
The minimal DT2 Shiny app uses dt2_output() in the UI
and render_dt2() in the server. DT2 tables integrate
naturally with bslib cards and Bootstrap 5 themes — the
table inherits the page theme automatically:
library(shiny)
library(bslib)
library(DT2)
ui <- page_fillable(
theme = bs_theme(version = 5, bootswatch = "flatly"),
padding = "1rem",
card(
card_header("DT2 in Shiny"),
card_body(dt2_output("my_table"))
)
)
server <- function(input, output, session) {
output$my_table <- render_dt2({
dt2(iris, options = list(pageLength = 10))
})
}
shinyApp(ui, server)Reading Table State
Every DT2 table pushes its state (order,
search, page, selected rows) to
input$<id>_state automatically:
library(shiny)
library(bslib)
library(DT2)
ui <- page_fillable(
theme = bs_theme(version = 5, bootswatch = "flatly"),
padding = "1rem",
card(
card_header("Table State"),
card_body(dt2_output("tbl"))
),
card(
card_header("Current state"),
card_body(verbatimTextOutput("state_info"))
)
)
server <- function(input, output, session) {
output$tbl <- render_dt2({
dt2(iris, options = list(pageLength = 10))
})
output$state_info <- renderPrint({
state <- input$tbl_state
if (is.null(state)) return("(interact with the table)")
cat("Reason:", state$reason, "\n")
cat("Page:", state$page$page + 1, "of", state$page$pages, "\n")
cat("Search:", if (nzchar(state$search)) state$search else "(none)", "\n")
cat("Order:", paste(state$order, collapse = ", "), "\n")
})
}
shinyApp(ui, server)Proxy: Server-Side Manipulation
Use dt2_proxy() to update a table without re-rendering
it. This is essential for responsive dashboards: you can replace the
data, change the page, adjust sorting, or update the search filter — all
without the flicker of a full re-render:
library(shiny)
library(bslib)
library(DT2)
ui <- page_sidebar(
theme = bs_theme(version = 5, bootswatch = "flatly"),
title = "Proxy Demo",
sidebar = sidebar(
actionButton("refresh", "Refresh data", class = "btn-sm btn-outline-primary w-100 mb-2"),
actionButton("go_last", "Go to last page", class = "btn-sm btn-outline-primary w-100 mb-2"),
actionButton("sort_sl", "Sort Sepal.Length \u2193", class = "btn-sm btn-outline-primary w-100 mb-2"),
hr(),
textInput("search_text", "Search", placeholder = "e.g. setosa"),
actionButton("search_btn", "Apply", class = "btn-sm btn-primary w-100")
),
card(
card_body(dt2_output("tbl"))
)
)
server <- function(input, output, session) {
output$tbl <- render_dt2({
dt2(iris, options = list(pageLength = 10))
})
proxy <- dt2_proxy("tbl")
observeEvent(input$refresh, {
new_data <- iris[sample(nrow(iris), 50), ]
dt2_replace_data(proxy, new_data)
})
observeEvent(input$go_last, {
dt2_proxy_page(proxy, "last")
})
observeEvent(input$sort_sl, {
dt2_proxy_order(proxy, c("Sepal.Length", "desc"),
columns = names(iris))
})
observeEvent(input$search_btn, {
dt2_proxy_search(proxy, input$search_text)
})
}
shinyApp(ui, server)Buttons with Custom Style
Override the default button appearance with any Bootstrap 5 button
class. The dt2_use_buttons() helper accepts a
button_class parameter that applies to all buttons in the
group. Here, solid primary blue buttons replace the default outlined
style:
library(shiny)
library(bslib)
library(DT2)
ui <- page_fillable(
theme = bs_theme(version = 5, bootswatch = "flatly"),
padding = "1rem",
card(
card_header("Styled Buttons"),
card_body(dt2_output("tbl"))
)
)
server <- function(input, output, session) {
output$tbl <- render_dt2({
opts <- dt2_use_buttons(
buttons = c("copy", "csv", "excel", "print"),
position = "topEnd",
button_class = "btn btn-sm btn-primary"
)
dt2(mtcars, options = opts)
})
}
shinyApp(ui, server)Infinite Scrolling (Scroller)
Use virtual scrolling instead of pagination for large datasets. The
Scroller extension renders only the visible rows inside a fixed-height
container; more rows load seamlessly as the user scrolls. Combined with
deferRender = TRUE, this keeps memory usage low even for
tens of thousands of rows:
library(shiny)
library(bslib)
library(DT2)
ui <- page_fillable(
theme = bs_theme(version = 5, bootswatch = "flatly"),
padding = "1rem",
card(
card_header("Scroller \u2014 10 000 rows"),
card_body(dt2_output("tbl"))
)
)
server <- function(input, output, session) {
big <- data.frame(
id = 1:10000,
value = round(rnorm(10000), 3),
group = sample(LETTERS[1:5], 10000, replace = TRUE)
)
output$tbl <- render_dt2({
dt2(big, options = list(
scroller = TRUE,
scrollY = 400,
deferRender = TRUE,
buttons = list("copy", "csv"),
layout = list(
topStart = list(search = list(placeholder = "Filter...")),
topEnd = "buttons"
)
))
})
}
shinyApp(ui, server)Custom Layout
DataTables v2 uses layout to position toolbar elements
around the table. Each slot (topStart, topEnd,
bottomStart, bottomEnd) accepts a string,
list, or NULL. This example rearranges search, buttons,
info, and pagination into non-default positions:
library(shiny)
library(bslib)
library(DT2)
ui <- page_fillable(
theme = bs_theme(version = 5, bootswatch = "flatly"),
padding = "1rem",
card(
card_header("Custom Layout"),
card_body(dt2_output("tbl"))
)
)
server <- function(input, output, session) {
output$tbl <- render_dt2({
dt2(iris, options = list(
pageLength = 10,
layout = list(
topStart = "pageLength",
topEnd = list(
buttons = list("copy", "csv", "excel"),
search = list(placeholder = "Search...")
),
bottomStart = "info",
bottomEnd = "paging"
)
))
})
}
shinyApp(ui, server)Pagination Types
DataTables v2 configures pagination through the layout
option using paging as a named list, replacing the
deprecated pagingType parameter. This example shows three
common configurations side by side: simple navigation (prev/next only),
full navigation with all elements, and no pagination at all.
library(shiny)
library(bslib)
library(DT2)
ui <- page_fillable(
theme = bs_theme(version = 5, bootswatch = "flatly"),
padding = "1rem",
layout_columns(
col_widths = c(6, 6),
card(
card_header("Simple (prev / next only)"),
card_body(dt2_output("tbl_simple"))
),
card(
card_header("Full (first / 1 2 3 / last)"),
card_body(dt2_output("tbl_full"))
)
),
card(
card_header("No pagination"),
card_body(dt2_output("tbl_none"))
)
)
server <- function(input, output, session) {
output$tbl_simple <- render_dt2({
dt2(iris, options = list(
pageLength = 5,
layout = list(bottomEnd = list(paging = list(
type = "simple"
)))
))
})
output$tbl_full <- render_dt2({
dt2(iris, options = list(
pageLength = 5,
layout = list(bottomEnd = list(paging = list(
firstLast = TRUE, previousNext = TRUE, numbers = TRUE
)))
))
})
output$tbl_none <- render_dt2({
dt2(iris[1:15, ], options = list(paging = FALSE))
})
}
shinyApp(ui, server)Inline Inputs (Checkboxes and Buttons)
DT2 can embed Shiny inputs (checkboxes, action buttons) directly
inside table cells. When the user clicks a checkbox or button, DT2 sends
the event to Shiny via input$<id>_state with
reason = "input". This pattern is useful for row-level
actions like selection, approval workflows, or delete buttons:
library(shiny)
library(bslib)
library(DT2)
ui <- page_fillable(
theme = bs_theme(version = 5, bootswatch = "flatly"),
padding = "1rem",
card(
card_header("Inline Inputs"),
card_body(dt2_output("tbl"))
),
card(
card_header("Events"),
card_body(verbatimTextOutput("events"))
)
)
server <- function(input, output, session) {
df <- data.frame(
name = c("Alice", "Bob", "Carol", "Dave"),
score = c(95, 82, 91, 78),
select = NA,
action = NA
)
output$tbl <- render_dt2({
opts <- list(columns = names(df), pageLength = 10)
opts <- dt2_col_checkbox(opts, "select")
opts <- dt2_col_button(opts, "action", label = "View")
dt2(df, options = opts)
})
event_log <- reactiveVal("")
observeEvent(input$tbl_row_check, {
info <- input$tbl_row_check
msg <- paste0("Checkbox row ", info$row, ": ", info$value, "\n")
event_log(paste0(event_log(), msg))
})
observeEvent(input$tbl_row_button, {
info <- input$tbl_row_button
msg <- paste0("Button clicked row ", info$row, "\n")
event_log(paste0(event_log(), msg))
})
output$events <- renderText(event_log())
}
shinyApp(ui, server)Server-Side Processing
For very large datasets (100k+ rows), use server-side processing. Only the visible page is sent to the browser — sorting, filtering, and pagination happen on the R server. This avoids sending the entire dataset to the client, dramatically improving initial load time:
library(shiny)
library(bslib)
library(DT2)
ui <- page_fillable(
theme = bs_theme(version = 5, bootswatch = "flatly"),
padding = "1rem",
card(
card_header("Server-Side Processing \u2014 100k rows"),
card_body(dt2_output("tbl"))
)
)
server <- function(input, output, session) {
large_data <- data.frame(
id = seq_len(100000),
value = round(rnorm(100000), 4),
group = sample(LETTERS[1:5], 100000, replace = TRUE)
)
dt2_bind_server("tbl", large_data)
output$tbl <- render_dt2({
dt2(large_data, options = list(
columns = names(large_data),
pageLength = 25,
server_side = TRUE,
language = list(info = "Showing _START_ to _END_ of _TOTAL_ rows")
))
})
}
shinyApp(ui, server)Dataset Switcher with bslib
A common dashboard pattern: let the user choose a dataset and a theme
preset from the sidebar. The table re-renders reactively whenever either
input changes. This demonstrates how DT2 integrates naturally with
bslib’s page_sidebar() layout:
library(shiny)
library(bslib)
library(DT2)
ui <- page_sidebar(
theme = bs_theme(version = 5, bootswatch = "flatly"),
title = "DT2 + bslib",
sidebar = sidebar(
selectInput("dataset", "Dataset",
c("iris", "mtcars", "airquality")),
selectInput("theme_preset", "Theme preset",
c("default", "clean", "minimal", "compact"))
),
card(
card_header("Table"),
card_body(dt2_output("tbl"))
)
)
server <- function(input, output, session) {
output$tbl <- render_dt2({
data <- switch(input$dataset,
iris = iris,
mtcars = mtcars,
airquality = airquality
)
dt2(data,
theme = input$theme_preset,
options = list(pageLength = 10))
})
}
shinyApp(ui, server)Shared Theme Across Tables
Define a theme once with dt2_theme() and reuse it across
all tables in your app. This ensures consistent font scaling, button
styling, and compactness without repeating configuration. Changes
propagate instantly to every table that uses the theme:
library(shiny)
library(bslib)
library(DT2)
my_theme <- dt2_theme("clean",
compact = TRUE,
font_scale = 0.85,
button_class = "btn btn-sm btn-outline-dark"
)
ui <- page_fillable(
theme = bs_theme(version = 5, bootswatch = "flatly"),
padding = "1rem",
layout_columns(
col_widths = c(6, 6),
card(
card_header("iris"),
card_body(dt2_output("tbl1"))
),
card(
card_header("mtcars"),
card_body(dt2_output("tbl2"))
)
)
)
server <- function(input, output, session) {
output$tbl1 <- render_dt2({
dt2(iris, theme = my_theme, options = dt2_use_buttons(
buttons = c("copy", "csv")
))
})
output$tbl2 <- render_dt2({
dt2(mtcars, theme = my_theme, options = list(pageLength = 15))
})
}
shinyApp(ui, server)Complete Example: Custom Renderers, Flags, and ColumnControl
This Shiny app demonstrates most DT2 features together: ColumnControl dropdowns, export buttons with spacer, custom JS renderers (links, flags, progress bars, colored currency), and full pt-BR translation.
Copy and run it directly — it is also available at
system.file("examples/app_complete.R", package = "DT2").
library(jsonlite)
library(dplyr)
library(tibble)
library(lubridate)
# ── Sample data (57 employees) ────────────────────────────────────────────────
json_url <- "https://raw.githubusercontent.com/StrategicProjects/DT2/main/inst/examples/employees.json"
# For offline use, the same JSON is bundled in inst/examples/employees.json
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)
)
# ── Shiny App ─────────────────────────────────────────────────────────────────
library(shiny)
library(bslib)
library(DT2)
library(htmlwidgets)
ui <- page_sidebar(
theme = bs_theme(version = 5, bootswatch = "spacelab"),
title = "DT2 — Complete Example",
sidebar = sidebar(
h5("Features"),
tags$ul(
tags$li("ColumnControl (order + search dropdowns)"),
tags$li("Export buttons with spacer"),
tags$li("Custom JS renderers"),
tags$li("pt-BR translation")
)
),
# Flag sprites
tags$head(
tags$link(
rel = "stylesheet", type = "text/css",
href = "https://cdn.jsdelivr.net/gh/lafeber/world-flags-sprite/stylesheets/flags32-both.css"
),
tags$style(HTML("
.f32 .flag { display:inline-block; width:32px; height:32px;
vertical-align:middle; margin-right:6px; }
table.dataTable tbody td { vertical-align: middle; }
"))
),
card(
card_header("Employee Table"),
card_body(dt2_output("tbl", height = "auto"))
)
)
server <- function(input, output, session) {
output$tbl <- render_dt2({
# ── 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'};
var flag = cc[data] || '';
return '<span class=\"flag ' + flag + '\"></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;
}
")
# ── Options ───────────────────────────────────────────────
opts <- list(
pageLength = 10,
lengthMenu = c(10, 25, -1),
columns = names(df),
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 = "paging"
),
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")
)
)
)
)
dt2(df,
compact = TRUE, striped = TRUE, hover = TRUE,
font_scale = 0.85, responsive = FALSE,
options = opts)
})
}
shinyApp(ui, server)