Here you go. 
Doesn't yet have control from Node-RED but that is easy to add.
I'm afraid that you will need to install node-red-contrib-uibuilder and then restart node-red before using this flow.
[{"id":"fa0240a25588cf67","type":"group","z":"b2f18a716bd20f99","name":"Countdown timer demo","style":{"label":true},"nodes":["e51ba7ef3e333151","a86aea213a59fe7d","2d170e4ebc0b7f1f","fa56099adb3acf7e","3dc0031905a11e96","7fc5dc8d632aca16","b0661e7aef09d16f","24be6cd926d1deb0"],"x":88,"y":1459,"w":648,"h":328},{"id":"e51ba7ef3e333151","type":"debug","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"debug 7","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"","statusType":"counter","x":655,"y":1500,"wires":[],"l":false},{"id":"a86aea213a59fe7d","type":"uibuilder","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"","topic":"","url":"becker","instancePath":"","okToGo":true,"fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"templateFolder":"blank","extTemplate":"","showfolder":false,"reload":true,"sourceFolder":"src","deployedVersion":"7.7.0","showMsgUib":false,"title":"","descr":"","editurl":"vscode://file/D:/src\\uibRoot/becker/?windowId=_blank","x":300,"y":1540,"wires":[["2d170e4ebc0b7f1f"],[]]},{"id":"2d170e4ebc0b7f1f","type":"switch","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"query","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":450,"y":1520,"wires":[["e51ba7ef3e333151","3dc0031905a11e96"],["fa56099adb3acf7e"]]},{"id":"fa56099adb3acf7e","type":"debug","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"debug 33","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"","statusType":"counter","x":565,"y":1560,"wires":[],"l":false},{"id":"3dc0031905a11e96","type":"link out","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"link out 62","mode":"link","links":["7fc5dc8d632aca16"],"x":695,"y":1540,"wires":[]},{"id":"7fc5dc8d632aca16","type":"link in","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"link in 19","links":["3dc0031905a11e96"],"x":135,"y":1540,"wires":[["b0661e7aef09d16f"]]},{"id":"b0661e7aef09d16f","type":"change","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"RESPONSE","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":195,"y":1540,"wires":[["a86aea213a59fe7d"]],"l":false},{"id":"24be6cd926d1deb0","type":"group","z":"b2f18a716bd20f99","g":"fa0240a25588cf67","name":"RUN ONCE: Initialise Front-end Code - Run after the uibuilder node has been deployed. \\n REMEMBER to change the uib node name before use \\n Sets up FE code, reloads connected clients. \\n ","style":{"label":true,"stroke":"#a4a4a4","fill-opacity":"0.33","color":"#000000","fill":"#ffffff"},"nodes":["54afea71bcf96374","c1d30ee72e72cbb0","c92d83bda675832c","5bcce172317b7bfd","34ff3e8e97de6fd5","5763fbb206b384ed"],"x":114,"y":1591,"w":554,"h":170},{"id":"54afea71bcf96374","type":"inject","z":"b2f18a716bd20f99","g":"24be6cd926d1deb0","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"setup all FE files","x":175,"y":1680,"wires":[["34ff3e8e97de6fd5","5763fbb206b384ed"]],"l":false},{"id":"c1d30ee72e72cbb0","type":"template","z":"b2f18a716bd20f99","g":"24be6cd926d1deb0","name":"index.html","field":"payload","fieldType":"msg","format":"html","syntax":"plain","template":"<!doctype html>\n<html lang=\"en\"><head>\n\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <link rel=\"icon\" href=\"../uibuilder/images/uib-world.svg\" type=\"image/svg+xml\">\n\n <title>Countdown Timer - Node-RED UIBUILDER</title>\n <meta name=\"description\" content=\"Node-RED UIBUILDER - Countdown Timer\">\n\n <!-- Your own CSS (defaults to loading uibuilders css)-->\n <link type=\"text/css\" rel=\"stylesheet\" href=\"./index.css\" media=\"all\">\n\n <style>\n #timer {\n font-size: 5rem;\n font-weight: bold;\n font-variant-numeric: tabular-nums;\n letter-spacing: 4px;\n }\n\n .labels {\n display: flex;\n justify-content: center;\n gap: 2.5rem;\n margin-top: -10px;\n color: #888;\n text-transform: uppercase;\n font-size: 0.8rem;\n }\n </style>\n\n <script type=\"module\" async src=\"./index.js\"></script>\n</head><body>\n <h1 class=\"with-subtitle\">Countdown Timer</h1>\n <div role=\"doc-subtitle\">Using UIBUILDER for Node-RED</div>\n \n <!-- '#more' is used as a parent for dynamic HTML content in examples\n Also, send {topic:\"more\", payload:\"Hello from <b>Node-RED</b>\"} to auto-display the payload -->\n <div id=\"more\" uib-topic=\"more\"></div>\n\n <article class='container'>\n <h2>COUNTDOWN</h2>\n <div id='timer'>00:00:00</div>\n <div class='labels'>\n <span>Hours</span>\n <span>Mins</span>\n <span>Secs</span>\n </div>\n </article>\n\n</body></html>\n","output":"str","x":380,"y":1680,"wires":[["c92d83bda675832c"]]},{"id":"c92d83bda675832c","type":"uib-save","z":"b2f18a716bd20f99","g":"24be6cd926d1deb0","url":"becker","uibId":"a86aea213a59fe7d","folder":"src","fname":"","createFolder":false,"reload":true,"usePageName":false,"encoding":"utf8","mode":438,"name":"","topic":"","x":550,"y":1680,"wires":[]},{"id":"5bcce172317b7bfd","type":"template","z":"b2f18a716bd20f99","g":"24be6cd926d1deb0","name":"index.js","field":"payload","fieldType":"msg","format":"javascript","syntax":"plain","template":"// Give VS Code IntelliSense for uibuilder\n/// <reference path=\"../types/uibuilder.d.ts\" />\n\n// @ts-ignore\nimport { uibuilder } from '../uibuilder/uibuilder.esm.min.js'\n\n// Set the initial countdown time in seconds (Max 43200 for 12 hours)\nlet timeLeft = 3600 // 1 hour\n\nconst timerDisplay = document.getElementById('timer')\n\nconst updateTimer = () => {\n if (timeLeft <= 0) {\n clearInterval(countdownInterval)\n timerDisplay.textContent = '00:00:00'\n timerDisplay.style.color = '#ff4b2b'\n return\n }\n\n const hours = Math.floor(timeLeft / 3600)\n const minutes = Math.floor((timeLeft % 3600) / 60)\n const seconds = timeLeft % 60\n\n // Format numbers to always show two digits\n const displayHours = String(hours).padStart(2, '0')\n const displayMinutes = String(minutes).padStart(2, '0')\n const displaySeconds = String(seconds).padStart(2, '0')\n\n timerDisplay.textContent = `${displayHours}:${displayMinutes}:${displaySeconds}`\n \n timeLeft--\n}\n\n// Run the timer immediately on load, then every second\nupdateTimer()\nconst countdownInterval = setInterval(updateTimer, 1000)\n","output":"str","x":390,"y":1720,"wires":[["c92d83bda675832c"]]},{"id":"34ff3e8e97de6fd5","type":"change","z":"b2f18a716bd20f99","g":"24be6cd926d1deb0","name":"index.html","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.html","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":265,"y":1680,"wires":[["c1d30ee72e72cbb0"]],"l":false},{"id":"5763fbb206b384ed","type":"change","z":"b2f18a716bd20f99","g":"24be6cd926d1deb0","name":"index.js","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.js","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":265,"y":1720,"wires":[["5bcce172317b7bfd"]],"l":false},{"id":"db5d34e28a4ae831","type":"global-config","env":[],"modules":{"node-red-contrib-uibuilder":"7.7.0"}}]
Oh, and you can add this into the index.js to be able to update the timer from Node-RED:
uibuilder.onTopic('setTime', (msg) => {
const newTime = parseInt(msg.payload, 10)
if (!isNaN(newTime) && newTime >= 0 && newTime <= 43200) {
timeLeft = newTime
updateTimer() // Update immediately when time is set
} else {
console.warn('Invalid time received. Please send a number between 0 and 43200.')
}
})
And add this to the end of the updateTimer function to get an alert in Node-RED when the timer is expiring:
if (timeLeft <= 1000) {
uibuilder.send({ topic: 'timeout', payload: timeLeft })
}