Layouts modernos con bslib

r
shiny
bslib como el reemplazo moderno de shinydashboard. page_sidebar() y page_navbar(), cards, value_box para KPIs, nav_panel para tabs, theming con bs_theme.

¿Por qué bslib?

Durante años el patrón estándar para dashboards en Shiny fue shinydashboard, un paquete con su propia maquinaria de cabeceras, sidebars y boxes. Funcional, pero atado a Bootstrap 3 y con estética envejecida.

En 2021 Posit lanzó bslib como el reemplazo moderno:

  • Bootstrap 5 por defecto (responsive, mobile-first).
  • Sintaxis composable: cards, columns, navsets construidos con la misma API que shiny::fluidPage.
  • Themes nativos: cambias colores y fuentes con una función, sin tocar CSS.
  • Integración con Quarto: la misma API funciona en documentos y apps.
install.packages("bslib")
library(shiny)
library(bslib)

bslib es lo que recomienda Posit hoy y lo que vas a ver en código nuevo. shinydashboard sigue funcionando pero está en modo mantenimiento.

Pages: page_sidebar y page_navbar

Dos páginas cubren el 90 % de los layouts:

page_sidebar(), el clásico sidebar + main:

ui <- page_sidebar(
  title = "Mi dashboard",
  sidebar = sidebar(
    sliderInput("n", "Tamaño:", min = 10, max = 1000, value = 100),
    selectInput("region", "Región:", c("Norte", "Sur"))
  ),
  card(
    card_header("Resultados"),
    plotOutput("grafico")
  )
)

Sustituye al patrón fluidPage(sidebarLayout(sidebarPanel(...), mainPanel(...))) de Shiny clásico. Más limpio.

page_navbar(), para apps con varias páginas:

ui <- page_navbar(
  title = "Mi app",
  nav_panel("Resumen", card(plotOutput("g1"))),
  nav_panel("Detalle", card(tableOutput("t1"))),
  nav_panel("Ajustes", card(uiOutput("config"))),
  nav_spacer(),
  nav_item(tags$a("GitHub", href = "https://github.com/..."))
)

nav_panel() es cada pestaña. nav_spacer() y nav_item() añaden elementos secundarios (links, ayuda, logout) alineados a la derecha.

Para apps con sidebar dentro de cada tab:

ui <- page_navbar(
  title = "Mi app",
  sidebar = sidebar("Controles globales", sliderInput(...)),
  nav_panel("Tab 1", ...),
  nav_panel("Tab 2", ...)
)

card: el bloque básico de la UI moderna

Una card es un contenedor con borde sutil y sombra que agrupa contenido relacionado:

card(
  card_header("Ventas por región"),
  plotOutput("grafico_ventas"),
  card_footer("Datos actualizados: junio 2026")
)

Estructura típica:

  • card_header(): título (opcional).
  • card_body(): contenido principal (implícito si pasas otros widgets directamente).
  • card_footer(): notas, fecha, créditos (opcional).

Argumentos útiles:

card(
  height = 400,           # altura fija
  full_screen = TRUE,     # botón para expandir a fullscreen
  ...
)

Para layouts complejos, combinas cards en layout_columns() o layout_column_wrap():

layout_columns(
  card(card_header("KPI 1"), value_box("Ingresos", "$12k")),
  card(card_header("KPI 2"), value_box("Usuarios", "1.2k")),
  card(card_header("KPI 3"), value_box("Conversión", "3.4%")),
  col_widths = c(4, 4, 4)
)

col_widths usa el sistema de 12 columnas de Bootstrap, 4+4+4 = 12 distribuye el ancho en tres.

value_box: KPIs decentes

Los KPI cards son el componente más común en dashboards corporativos. value_box() los hace sin pelearse con HTML:

value_box(
  title = "Ingresos del mes",
  value = "$12.4k",
  showcase = bsicons::bs_icon("currency-dollar"),
  theme = "success",
  p("8.2% sobre el mes pasado")
)
  • title: etiqueta superior.
  • value: número grande destacado.
  • showcase: icono o gráfico pequeño.
  • theme: color preset ("primary", "success", "warning", "danger"…).
  • Cualquier otro contenido va debajo del valor (descripciones, deltas, micrográficos).

Para micrográficos dentro de un value box (los famosos sparklines):

value_box(
  title = "Tráfico semanal",
  value = "1.2M",
  showcase = plotOutput("sparkline_trafico", height = "80px"),
  showcase_layout = "bottom"
)

Es lo que distingue un dashboard amateur de uno profesional, KPIs con contexto visual, no solo números.

Tabs y navegación con nav_panel

Para subdividir contenido dentro de un panel:

navset_card_tab(
  nav_panel("Tabla", DT::dataTableOutput("tabla")),
  nav_panel("Gráfico", plotOutput("grafico")),
  nav_panel("Resumen", verbatimTextOutput("resumen"))
)

navset_card_tab da pestañas estilo tab dentro de una card. Variantes:

  • navset_card_tab: pestañas clásicas.
  • navset_card_pill: pills (más visuales).
  • navset_card_underline: solo subrayado (minimalista).

Si necesitas referirte a las tabs desde server (saber cuál está activa, cambiarla):

ui <- navset_card_tab(
  id = "tabs",
  nav_panel(value = "tabla", "Tabla", DT::dataTableOutput("tabla")),
  nav_panel(value = "grafico", "Gráfico", plotOutput("grafico"))
)

server <- function(input, output, session) {
  observe({
    message("Tab activa: ", input$tabs)
  })

  # Cambiar tab programáticamente
  observeEvent(input$ir_a_grafico, {
    nav_select("tabs", "grafico")
  })
}

input$tabs te da el value del panel activo. nav_select() cambia de tab desde server.

Themes y customización con bs_theme

Cambiar la apariencia de la app sin escribir CSS:

mi_theme <- bs_theme(
  version = 5,
  bg = "#FAF9F6",        # fondo
  fg = "#2A2A2A",        # foreground (texto principal)
  primary = "#5F8575",   # color de acento
  base_font = font_google("Inter Tight"),
  heading_font = font_google("Newsreader")
)

ui <- page_sidebar(
  theme = mi_theme,
  title = "Mi app",
  ...
)

bs_theme() expone los parámetros de Sass de Bootstrap como argumentos de R. Customizas tipografía, colores, espaciado, redondeo de esquinas, etc., sin tocar CSS.

font_google() carga la fuente de Google Fonts y la pasa a Bootstrap. Útil para alinear la app con la identidad de marca.

Para experimentar interactivamente:

bslib::bs_theme_preview(mi_theme)

Abre una mini-app con todos los componentes de Bootstrap renderizados con tu theme, el equivalente a un style guide para validar antes de aplicar.

Comparación con shinydashboard

Equivalencias rápidas si vienes de shinydashboard:

shinydashboard bslib
dashboardPage() page_sidebar() o page_navbar()
dashboardHeader() argumento title en page_*()
dashboardSidebar() sidebar()
dashboardBody() contenido directo en page_*()
box(title = ...) card(card_header(...))
valueBox() value_box()
tabBox() navset_card_tab()
menuItem() nav_panel()

Si tienes apps en shinydashboard ya en producción: déjalas. Funcionan. Migrar tiene coste sin beneficio inmediato. Para apps nuevas, usa bslib.

Trampas habituales

  • page_sidebar() con muchísimo contenido directamente. Si el body crece sin estructura, mete cards. Un dashboard sin cards parece un blob. Con cards organizadas, tiene jerarquía.
  • Mezclar bslib y shinydashboard. Cargar los dos en la misma app genera conflictos de CSS y resultados impredecibles. Compromete uno.
  • value_box() con texto largo en value. El campo value está optimizado para 1-3 palabras o un número. Si pones una frase, se desborda. Usa el cuerpo de la card para texto.
  • Olvidar bs_theme_preview() antes de aplicar un theme custom. Aplicar un theme directamente sin previsualizarlo puede romper la legibilidad de componentes que no esperabas, confirma primero.

En la siguiente entrega

Has aprendido layouts. La siguiente pieza son las tablas interactivas, el componente más usado en dashboards reales. DT y reactable son las dos opciones modernas. Cada una con su filosofía. Lo siguiente.