Tutorial: Crea aplicaciones web interactivas en R con Shiny
7/5/2026
Shiny es un paquete para desarrollar aplicaciones web interactivas con R. Con Shiny puedes crear aplicaciones que tus usuarios/as podrán ver desde su navegador, y pueden contener todo tipo de contenido: gráficos, tablas, botones, textos dinámicos, mapas, etc. Es una forma de usar R para crear aplicaciones web centradas en datos que simplifica muchos aspectos complejos del desarrollo web.
Shiny le da vida a todo lo que hayas desarrollado con R sin cambiarte a otro lenguaje ni aprender uno distinto. Esto significa que la distancia se acorta entre el gráfico que hiciste o el resultado de tus estudios y el tener un producto presentable para tu público o clientes.
En este tutorial veremos cómo crear una aplicación Shiny desde cero!
Índice
Beneficios de Shiny
Control del stack completo de la aplicación desde un mismo lenguaje:
- Shiny se encarga de que todas las piezas (HTML, CSS, JavaScript) funcionen desde R
- No necesitas ningún aprender ningún lenguaje nuevo porque todo funciona con R
- Todo lo que hayas hecho en R se podrá incluir en tu aplicación sin necesidad de cambios, como tus gráficos, tablas y más.
Tiempo de desarrollo reducido incluso para personas sin experiencia en informática:
- El trabajo necesario para producir una aplicación es muy bajo; en unos minutos puedes tener algo funcional
- No necesitas expertos o equipos externos para desarrollar una aplicación, ya que Shiny simplifica al máximo el proceso
Algunas aplicaciones hechas con Shiny
Estructura de una app Shiny
Las aplicaciones Shiny son, en su forma más básica, un solo archivo: app.R.
En este script estará todo lo que necesita la aplicación para funcionar!
Obviamente este script debe estar dentro de una carpeta, idealmente un
proyecto de R. Todo lo que esté en la carpeta donde se encuentre app.R estará al alcance de tu app.
Dentro de app.R, la aplicación se divide en tres secciones principales:
shinyApp()
En cada una de estas secciones usaremos código de R para establecer lo que necesita la aplicación, como paquetes, datos, configuraciones (global), para diseñar la interfaz gráfica (UI), y para procesar los datos y outputs de la aplicación (server).
Al final de app.R, una línea mágica transformará el script en una aplicación: shinyApp()
Creando una aplicación vacía
Si no tienes Shiny instalado, instálalo con:
install.packages("shiny")
Todas las aplicaciones Shiny empiezan muy sencillas, con unas cuantas líneas que definen lo mínimo para que funcione, y después van creciendo en complejidad.
Para entender los conceptos básicos, haremos una aplicación completamente vacía. Empezamos creando un nuevo script, que llamaremos app.R.
Global
Lo inicial siempre va a ser cargar los paquetes necesarios en la sección global de la aplicación, que corresponde a las primeras líneas del script app.R.
En la primera línea de app.R agrega:
library(shiny)
Interfaz
Luego tenemos que crear la interfaz de la aplicación. Esto es todos los aspectos visuales e interactivos de la aplicación, lo que las y los usuarios/as verán y usarán.
La interfaz de la aplicación se crea con una función. Hay varias disponibles, pero usaremos una muy básica, page_fillable()
del paquete {bslib}.
library(shiny)
library(bslib)
ui <- page_fillable()
La función page_fillable() creará el objeto ui, que es necesario para que la aplicación funcione. Este objeto va a contener toda la interfaz de la aplicación.
Agreguemos un título y un texto a la interfaz de nuestra aplicación:
library(shiny)
library(bslib)
ui <- page_fillable(
h1("Título"),
p("Texto dentro de la app")
)
Los elementos que pongamos dentro de la ui tienen que ir separados por comas!
Server
En la sección server de la aplicación es donde ocurren todos los cálculos que normalmente hacemos en R. Por ejemplo, filtrar datos, procesar información y crear gráficos.
El server es simplemente una función:
server <- function(input, output) {
}
Dentro de server iremos poniendo más funciones que creen las partes de la aplicación que requieren de cálculos y/o datos. Podemos dejarla vacía por mientras.
Aplicación vacía
Unamos las piezas en un solo script app.R, agregando la última pieza para que la aplicación sea ejecutable: shinyApp()
# global
library(shiny)
library(bslib)
# interfaz
ui <- page_fillable(
h1("Título"),
p("Texto dentro de la app")
)
# servidor
server <- function(input, output) {
}
# ejecutar la app
shinyApp(ui, server)
Estas son las partes básicas de una app Shiny. Si guardas esto en un script, notarás que aparece el botón Run App en la parte superior derecha del panel de scripts:
Al presionarlo, tu aplicación se ejecutará en el panel Viewer de RStudio. Si hiciste todo bien (cargar los paquetes, separar elementos de UI con comas, poner shinyApp() al final) debería aparecer la aplicación en el panel!
Hasta ahora hemos hecho lo más simple posible: una aplicación con un poco de texto, pero sin datos, sin interacción, ni nada.
Ver código de la app vacía
Pon este código en el script app.R:
# global
library(shiny)
library(bslib)
# interfaz
ui <- page_fluid(
h1("Título"),
p("Texto dentro de la app")
)
# servidor
server <- function(input, output) {
}
# ejecutar la app
shinyApp(ui, server)
Tutorial básico recomendado
Creando una aplicación básica con datos
La gracia de las aplicaciones Shiny es que usen datos y permitan a sus usuarios explorar y visualizar información de manera interactiva.
Ahora iremos poco a poco construyendo una app simple que cargue datos y los use para demostrar conceptos básicos de Shiny.
Cargar datos en la app
Bajemos un conjunto de datos para probar: el catastro de campamentos 2024 realizado por el Ministerio de Vivienda y Urbanismo de Chile. Esta versión de los datos corresponde a una base previamente limpiada con código de este repositorio.
El archivo Excel se llama datos.xlsx. Guardamos el archivo en la carpeta de nuestra aplicación:
Al estar el archivo de datos en la misma carpeta que app.R, se puede cargar tan solo llamando su nombre. Entonces, cargamos con la función apropiada antes de la interfaz de la app:
library(shiny)
library(bslib)
library(readxl)
datos <- read_excel("datos.xlsx")
ui <- page_fillable(
# ...
Todo lo que se ejecute en la sección global (al inicio de la app y antes de la UI) estará disponible constantemente durante tu aplicación.
Código de la app hasta ahora
library(shiny)
library(bslib)
library(readxl)
datos <- read_excel("datos.xlsx")
# interfaz
ui <- page_fillable(
h1("Título"),
p("Texto dentro de la app")
)
# servidor
server <- function(input, output) {
}
# ejecutar la app
shinyApp(ui, server)
Textos básicos
Agreguemos un título y una descripción breve a la app. Estos cambios los hacemos en la interfaz de la app:
ui <- page_fillable(
# título
h1("Campamentos en Chile"),
# párrafo
p("Aplicación Shiny para explorar datos
de campamentos registrados en Chile.")
)
En HTML (el código usado para construir páginas web) los titulares se definen como h1, h2, hasta h5, donde el número representa la jerarquía, en 1 correspondiendo al título principal de la página.
Shiny también crea sus aplicaciones web con HTML, pero nos ayuda a usarlo desde R. Creamos títulos con las funciones h1(), h2(), h3(), etc.
Los párrafos se crean con la función p().
Componer textos con datos
Podemos usar objetos de R para crear textos estáticos (que no cambian) en la app. Para ello podemos ayudarnos de la función glue() que nos facilita pegar textos. Veamos la comparación entre usar paste() versus glue() para componer textos:
x <- "gatos"
y <- "patos"
paste("me gustan los", x, "y los", y, "también")
glue::glue("me gustan los {x} y los {y} también")
Con glue() el código resulta mucho más legible, porque te ahorra tener que poner tantas comas y comillas!
Mejoremos entonces el párrafo p() de la app:
library(glue)
ui <- page_fillable(
# título
h1("Campamentos en Chile"),
# párrafo
p(
glue("Aplicación Shiny para explorar los datos de
los {nrow(datos)} campamentos registrados en Chile.")
),
)
Ahora el texto del párrafo muestra el número de filas de la tabla de datos!
Textos con Markdown
Escribir textos con estilos como negrita o itálicas, o textos con enlaces en HTML puede ser engorroso, pero podemos usar Markdown, un lenguaje de marcado que nos facilita la escritura de HTML mediante símbolos que representan las propiedades del texto:
| texto | código | resultado |
|---|---|---|
| Negrita | **hola** |
hola |
| Itálica | _hola_ |
hola |
| Enlace | [hola](www.com) |
hola |
| Tachado | ~~hola~~ |
|
| Código | `hola` |
hola |
La función markdown() nos permite incluir este tipo de textos en nuestras apps Shiny. En una sola función, y usando algunos símbolos, podemos escribir un párrafo con enlaces, negritas e itálicas:
# párrafo
p(
glue("Aplicación Shiny para explorar los datos de
los {nrow(datos)} campamentos registrados en Chile.")
),
# párrafo markdown
markdown("Los **campamentos** son definidos por el
[Minvu](https://www.minvu.gob.cl/catastro-campamentos-2022/)
como _Asentamientos de ocho o más viviendas precarias que
habitan en posesión irregular un terreno, con carencia de
servicios básicos, agrupadas y contiguas._")
Así va quedando la app:
Fíjate que los elementos dentro de la UI van separados por comas (porque en realidad son argumentos dentro de la función que genera la UI!), así que después de cada cierre de paréntesis no olvides la coma!
Código de la app hasta ahora
# global
library(shiny)
library(bslib)
library(readxl)
library(glue)
# cargar datos
datos <- read_excel("datos.xlsx")
# interfaz
ui <- page_fillable(
# título
h1("Campamentos en Chile"),
# párrafo
p(
glue("Aplicación Shiny para explorar
los datos de los {nrow(datos)}
campamentos registrados en Chile.")
),
# párrafo markdown
markdown("Los campamentos son definidos por el
[Minvu](https://www.minvu.gob.cl/catastro-campamentos-2022/)
como _Asentamientos de ocho o más viviendas precarias que
habitan en posesión irregular un terreno, con carencia de
servicios básicos, agrupadas y contiguas._")
)
# servidor
server <- function(input, output) {}
# ejecutar la app
shinyApp(ui, server)
Interacción básica con inputs
Hasta ahora la aplicación es estática. Pero la gracia es agregar elementos interactivos con los que l@s usuari@s puedan explorar los datos.
Un elemento con el que los usuarios interactúan en la app se denomina input. Los inputs se crearon con funciones, y se ponen en la interfaz de la aplicación.
Al crear cualquier input, el primer argumento de la función es su inputId, que es el identificador o nombre del input, que usaremos luego para obtener sus resultados.
Crear un selector de alternativas
Quizás el input más simple es selectInput(), que agrega un selector de opciones. Estas opciones podemos escribirlas a mano en el argumento choices, o podemos usar un vector o la columna de un dataframe como opciones para el selector.
Crearemos un input que permita seleccionar una opción desde una lista de regiones. Haremos que las regiones posibles de elegir vengan directamente de los datos. El conjunto de datos
que descargamos (y que cargamos como datos) tiene el nombre de la región a la que pertenece cada observación en la columna region, así que si usamos unique(datos$region) obtendremos un vector con sus valores únicos.
Entonces creamos el input con selectInput(), le damos el ID "region" (porque es un selector de regiones), y las elecciones posibles (choices) serán las regiones que vienen en el conjunto de datos:
# interfaz
ui <- page_fillable(
h1("Campamentos en Chile"),
...
selectInput(
inputId = "region", # ID único del input
label = "Explorar regiones", # titular del input
choices = sort(unique(datos$region)) # vector con regiones desde los datos
)
...
)
Si probamos la app, veremos que aparece un cuadro donde se puede hacer la selección:
Inputs de una app
Por ahora, nuestro selector no hace nada, pero internamente, cuando seleccionamos una opción, Shiny actualiza un objeto especial: el objeto input. Este objeto contiene los valores de todos los inputs de nuestra app, según el ID que le dimos.
selectInput("selector")→
input$selectorAl seleccionar una opción de un selector, inmediatamente se actualiza el objeto input${inputId}, donde {inputId corresponde al ID que le dimos al input. En nuestro ejemplo del selector de regiones, el valor elegido estaría en input$region, dado que el ID del input era region.
Pronto veremos cómo usar este valor!
Uniendo inputs y datos en server
Ahora que tenemos datos y un input, pasamos a hacer cosas con eso datos y esos inputs! ✨
Dentro de la sección server de una app (la función server) es donde se conectan los inputs con los datos.
En la función server es donde vamos a usar R como el motor de la app.
En server, lo que principalmente haremos será crear outputs o salidas. Los outputs son la forma de hacer que un cálculo que haga la aplicación se muestre en la interfaz.
Considerando las secciones UI y server, el ciclo de interacción de una app es:
- En la interfaz de la app hay inputs
- El/la usuario/a interactúa con un input
- El input se usa en server para producir una salida
- La salida se muestra en la interfaz de la app.
Naturalmente pueden haber pasos intermedios en operaciones más complejas, pero en resumen lo anterior describe el funcionamiento básico de una app Shiny.
Usando inputs dentro de server
Pasemos un input al server para hacer algo con su valor. Cuando hacemos esto, tenemos que tener una idea de qué es lo que queremos lograr, o para qué usaremos el input. Esto significa que tenemos que pensar en el resultado que esperamos poner en la app; es decir, en el output que se mostrará en la interfaz de la app.
Pensemos en un caso simple: queremos contar la cantidad de observaciones por región. Tenemos nuestro dataframe datos, el cual podemos filtrar por región y luego contar las filas para saber la cantidad de campamentos en una región:
casos_region <- datos |>
filter(region == "Maule") |>
nrow()
casos_region
[1] 25
Entonces, lo que haremos en la sección server de la app será algo equivalente a esto:
# definir input de prueba
region_elegida <- "Biobío"
casos_region <- datos |>
# aplicar input de prueba en el filtro
filter(region == region_elegida) |>
nrow()
casos_region
[1] 225
En este ejemplo creamos un input “falso” en el objeto region_elegida para poder probar la lógica en un scirpt aparte, pero en la app podremos usar el objeto input$region:
casos_region <- datos |>
filter(region == input$region) |>
nrow()
Esto lo haremos en server a continuación.
Generando salidas o outputs
En la sección server de la app, creamos el output que será la salida con lo que vamos a mostrar en la interfaz de la app. Aquí es donde podemos usar R normalmente para crear lo que queramos, pero dentro de ciertos marcos que circunscriben el código de R a la lógica de una aplicación interactiva Shiny.
Uno de estos marcos para apps interactivas es que, a diferencia de un script normal donde podemos generar resultados (cifras, tablas, gráficos) en cualquier parte, en una app tenemos que definir las salidas de manera clara, con un nombre y una función apropiada para crear la salida, y luego ubicar las salidas en algún lugar de la interfaz de la aplicación.
La salida que crearemos (el conteo de casos por región) se va a llamar casos_region, y como hay que explicitar que el código será una salida para la app, asignamos el resultado que queremos mostrar a un objeto preexistente llamado output que se encargará de recibir todas las salidas de nuestra app. Empecemos simplemente creando el objeto casos_region asignado a output, lo que quedaría así: output$casos_region
) # final de la ui
...
# servidor
server <- function(input, output) {
# conteo de casos filtrados por región
output$casos_region <- ... # aquí vamos...
}
# ejecutar la app
shinyApp(ui, server)
Ahora tenemos que pensar qué tipo de output crearemos, porque dependiendo del tipo de salida será la función que necesitamos usar para convertir el resultado de R en algo visible en nuestra app. En nuestro caso crearemos un texto con el conteo de casos, así que usaremos renderText() para crear o renderizar la cifra en un texto legible para la aplicación.
) # final de la ui
...
# servidor
server <- function(input, output) {
# conteo de casos filtrados por región
output$casos_region <- renderText({
datos |>
filter(region == "Maule") |>
nrow()
})
}
# ejecutar la app
shinyApp(ui, server)
La función renderText() convertirá el texto en un objeto HTML apropiado para la aplicación. Si quisiéramos hacer un gráfico, usaríamos renderPlot(), y así.
Ahora podemos reemplazar el filtro de prueba que hicimos por un filtro que use el input:
output$casos_region <- renderText({
datos |>
filter(region == input$region) |>
nrow()
})
Esto significa que el/la usuario/a eligirá qué es lo que se filtra, y que cada vez que se cambie el selector, se actualizará el objeto input$region, y por consiguiente se volverá a ejecutar el código de output$casos_region para entregar el nuevo resultado!
Hasta ahora tenemos algo como esto:
output$casos_region que identifica la salida para poder usarla
renderText() que renderiza el código de R en una salida HTML para la app
El último paso es conectar el output con el UI para que nuestra salida aparezca en alguna parte de la app. Para ello, invocamos el nombre del output en la función textOutput(), y la ubicamos en la UI de nuestra app; en este caso, debajo del selector mismo:
selectInput(
inputId = "region",
label = "Explorar regiones",
choices = sort(unique(datos$region))
),
# salida de texto de conteo de observaciones
textOutput("casos_region")
Lo que hicimos fue:
- Crear un input
- Crear un output
- Hacer que el output use el input
- Poner el output en la parte visible de nuestra app (UI)
Pasamos a tener esto:
textOutput() que ubica la salida en la interfaz de la app
renderText() que renderiza la salida a partir de código R
Veamos cómo funciona la aplicación!
Mejoremos la salida de texto para que sea más descriptiva: volvamos a server, al output que creamos, y hagamos que incluya más texto y el nombre de la región seleccionada:
output$casos_region <- renderText({
conteo <- datos |>
filter(region == input$region) |>
nrow()
glue("La región {input$region} tiene registrados {conteo} casos.")
})
Siguiendo estas mismas ideas podemos crear un párrafo de texto más complejo usando distintas variables, combinando múltiples inputs, y más!
Ver código completo de la app hasta ahora
Guarda este código en un script app.R para poder ejecutarlo:
# global
library(shiny)
library(bslib)
library(readxl)
library(glue)
# cargar datos
datos <- read_excel("datos.xlsx")
# interfaz
ui <- page_fillable(
# título
h1("Campamentos en Chile"),
# párrafo
p(
glue(
"Aplicación Shiny para explorar
los datos de los {nrow(datos)}
campamentos registrados en Chile."
)
),
# párrafo markdown
markdown(
"Los **campamentos** son definidos por el
[Minvu](https://www.minvu.gob.cl/catastro-campamentos-2022/)
como _Asentamientos de ocho o más viviendas precarias que
habitan en posesión irregular un terreno, con carencia de
servicios básicos, agrupadas y contiguas._"
),
# selector de regiones
selectInput(
inputId = "region",
label = "Explorar regiones",
choices = sort(unique(datos$region))
),
# salida de texto de conteo de observaciones
textOutput("casos_region")
)
# servidor
server <- function(input, output) {
# conteo de casos filtrados por región
output$casos_region <- renderText({
conteo <- datos |>
filter(region == input$region) |>
nrow()
glue("{input$region} tiene registrados {conteo} casos.")
})
}
# ejecutar la app
shinyApp(ui, server)
Cómo funciona una app Shiny
En los pasos anteriores hicimos una app básica que usa datos para mostrar un selector, y al cambiar el selector se ejecuta código de R para entregar un resultado.
En otras palabras, tuvimos código de R que se re-ejecutó automáticamente cada vez que cambió un elemento interactivo del cual dependía.
Esta idea de que el código se ejecute cuando cambia algo se llama reactividad, y es la idea base detrás del funcionamiento de Shiny.
En términos resumidos, al ejecutar la aplicación por primera vez se crea la interfaz de la aplicación. Luego Shiny identifica todos los outputs que hay en la aplicación. Cada output tiene una cadena de dependencias; es decir, depende de otros elementos de la app, los que a su vez pueden depender de inputs.
Al iniciar la app, se identifica que existen outputs que requieren calcularse:
Pero al querer calcular los outputs se identifica que se requieren calcular otros elementos anteriores primero; es decir, los outputs dependen de otros resultados. En la mayoría de los casos estos son objetos reactivos:
Los objetos reactivos son código de R que son envueltos en funciones de Shiny que hacen que se re-calculen automáticamente cuando cambien sus dependencias. Son básicamente pasos intermedios entre los inputs o los datos y los outputs.
Entonces, estos pasos intermedios pueden requerir los valores de inputs, o bien, el resultado de otros objetos reactivos. En el caso de este ejemplo, los dos outputs dependen de un objeto reactivo, y éste depende de un input:
Con este paso se termina de determinar la cadena de dependencias; es decir, Shiny identifica todos los pasos requeridos para calcular el output. Entonces se empiezan a entregar los datos para iniciar los cálculos. Primero se identifican los inputs:
Gracias a los inputs se pueden calcular los objetos reactivos o pasos intermedios:
Teniendo los objetos reactivos, los outputs ya cuentan con todo lo necesario, y pueden calcularse para mostrarse en la app.
En este punto nuestra app ya está lista y mostrando los resultados!
Cuando se calculan todos los pasos de la cadena, todo vuelve a un estado neutral, donde ya no necesita calcularse nada nuevo 😌
Pero si en la app se cambia un input, toda la cadena se marca como invalidada: los resultados que se tenían ya no son válidos, porque cambiaron elementos de la cadena!
Como la cadena se invalidó, Shiny nuevamente identifica que los outputs deben calcularse, y los marca como pendientes de cálculo, reiniciando el ciclo:
Los outputs requieren recalcularse, pero los outputs dependen de los reactivos, y los reactivos de otros reactivos o de inputs… entonces se navega hacia atrás por la cadena, marcando como pendiente de cálculo a los elementos requeridos por el output, hasta llegar a los inputs o a elementos sin ninguna dependencia, los cuales pueden calcularse y entregar sus resultados a los pasos siguientes.
Podemos ver este proceso en vivo y en directo
usando el paquete {reactlog}:
Publicaciones relacionadas
Publicaciones sobre shiny
Diapositivas y código de mi curso de Shiny