I integrated an answer from this post to shiny.
library(shiny)
ui <- fluidPage(
actionButton("run", "Run"),
p(id = "scenarioRuntime", tags$label(class = "minutes"), tags$label(class = "seconds")),
tags$script(HTML(
'
$(function(){
var timer;
Shiny.addCustomMessageHandler("timer", function(data){
if(data.event === "end") return clearInterval(timer);
var minutesLabel = document.querySelector(`#${data.id} .minutes`);
var secondsLabel = document.querySelector(`#${data.id} .seconds`);
var totalSeconds = 0;
function pad(val) {
var valString = val + "";
if (valString.length < 2) {
return "0" + valString;
} else {
return valString;
}
}
function setTime() {
++totalSeconds;
secondsLabel.innerHTML = pad(totalSeconds % 60);
minutesLabel.innerHTML = `${pad(parseInt(totalSeconds / 60))} : `;
}
timer = setInterval(setTime, 1000);
});
});
'
))
)
# Server logic
server <- function(input, output, session) {
observeEvent(input$run, {
# start singal
session$sendCustomMessage('timer', list(id = "scenarioRuntime", event = "start"))
# end signal, on.exit makes sure that the timer will stop no matter if it is
# complete or stop due to error
on.exit(session$sendCustomMessage('timer', list(id = "scenarioRuntime", event = "end")))
Sys.sleep(5)
})
}
shinyApp(ui = ui, server = server)

timer with async
To use more than one timers at the same time, we would need to use shiny async library {promises} and {future}.
This is an example to show you how you can run two processes in parallel in Shiny with timers.
library(shiny)
library(promises)
library(future)
plan(multisession)
ui <- fluidPage(
actionButton("run1", "Run 1"),
p(id = "scenarioRuntime1", tags$label(class = "minutes"), tags$label(class = "seconds")),
actionButton("run2", "Run 2"),
p(id = "scenarioRuntime2", tags$label(class = "minutes"), tags$label(class = "seconds")),
tags$script(HTML(
'
$(function(){
var timer = {};
Shiny.addCustomMessageHandler("timer", function(data){
if(data.event === "end") return clearInterval(timer[data.id]);
var minutesLabel = document.querySelector(`#${data.id} .minutes`);
var secondsLabel = document.querySelector(`#${data.id} .seconds`);
var totalSeconds = 0;
function pad(val) {
var valString = val + "";
if (valString.length < 2) {
return "0" + valString;
} else {
return valString;
}
}
function setTime() {
++totalSeconds;
secondsLabel.innerHTML = pad(totalSeconds % 60);
minutesLabel.innerHTML = `${pad(parseInt(totalSeconds / 60))} : `;
}
timer[data.id] = setInterval(setTime, 1000);
});
});
'
))
)
# Server logic
server <- function(input, output, session) {
mydata1 <- reactiveVal(FALSE)
observeEvent(input$run1, {
future_promise({
Sys.sleep(5)
TRUE
}) %...>%
mydata1()
# the future_promise will return right away, so if it runs then we start timer
session$sendCustomMessage('timer', list(id = "scenarioRuntime1", event = "start"))
})
observeEvent(mydata1(), {
req(mydata1())
session$sendCustomMessage('timer', list(id = "scenarioRuntime1", event = "end"))
})
mydata2 <- reactiveVal(FALSE)
observeEvent(input$run2, {
future_promise({
Sys.sleep(5)
TRUE
}) %...>%
mydata2()
session$sendCustomMessage('timer', list(id = "scenarioRuntime2", event = "start"))
})
observeEvent(mydata2(), {
req(mydata2())
session$sendCustomMessage('timer', list(id = "scenarioRuntime2", event = "end"))
})
}
shinyApp(ui = ui, server = server)

shinycssloadersis great for this. Try:textOutput("scenarioRuntime") %>% shinycssloaders::withSpinner()observeEventdoesn't return until it's complete, no other code will run. It's blocking all other server code from running. shiny code doesn't run in parallel by default. TherenderTextcan't run while theobserveEventis blocking. You'll probably need to get Javascript involved for such a behavior. See stackoverflow.com/questions/17325521/…