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)
