Introduction
A countdown timer looks trivial until you hit Streamlit's core constraint: the script only runs when something happens. A timer needs to tick with no user input. This tutorial solves that properly with st.empty placeholders and automatic refresh — and the solution is the foundation for any real-time Streamlit app, from the typing test to live dashboards.
By the end you'll have a timer with presets, a big live display, pause/resume, and a completion celebration — about 90 lines total.
Features
Prerequisites
pip install streamlit streamlit-autorefreshStep 1: Create the Script
Save as countdown.py:
import streamlit as st
import time
from datetime import datetime, timedelta
from streamlit_autorefresh import st_autorefresh
st.set_page_config(page_title="Countdown Timer", page_icon="⏲️")
st.title("⏲️ Countdown Timer")
# Tick every second while a timer is running
if st.session_state.get("deadline") and not st.session_state.get("paused"):
st_autorefresh(interval=1000, key="tick")
preset_cols = st.columns(4)
for mins, label in [(5, "5 min"), (10, "10 min"), (25, "Pomodoro"), (60, "1 hour")]:
if preset_cols[list((5, 10, 25, 60)).index(mins)].button(label):
st.session_state.deadline = datetime.now() + timedelta(minutes=mins)
st.session_state.duration = mins * 60
st.session_state.paused = False
st.rerun()
if "deadline" in st.session_state:
remaining = (st.session_state.deadline - datetime.now()).total_seconds()
if st.session_state.get("paused"):
remaining = st.session_state["paused_remaining"]
if remaining <= 0:
st.success("⏰ Time's up!")
st.balloons()
if st.button("Start another"):
del st.session_state["deadline"]
st.rerun()
else:
mins, secs = divmod(int(remaining), 60)
placeholder = st.empty()
placeholder.metric("Time remaining", f"{mins:02d}:{secs:02d}")
st.progress(1 - remaining / st.session_state.duration)
c1, c2 = st.columns(2)
if c1.button("⏸ Pause"):
st.session_state.paused = True
st.session_state.paused_remaining = remaining
st.rerun()
if c2.button("▶️ Resume"):
st.session_state.deadline = datetime.now() + timedelta(seconds=remaining)
st.session_state.paused = False
st.rerun()
else:
st.info("Pick a preset above to start the countdown.")Step 2: Run the App
streamlit run countdown.pyStart a Pomodoro and watch the display tick down every second — no clicks needed.
How It Works
The design stores a deadline, not a countdown: deadline = now + duration. Every rerun recomputes remaining from the current clock. This matters because reruns are unpredictable — storing "seconds left" and decrementing it drifts, while comparing against a fixed deadline never does. The same deadline thinking drives the typing test's start-time snapshot.
The ticking comes from st_autorefresh(interval=1000), which makes the browser re-request the page every second — each refresh reruns the script, recomputes the remaining time, and repaints the st.metric. It only registers while a timer runs, so idle pages don't churn requests.
Pause is deadline surgery: pausing snapshots the remaining seconds and stops the autorefresh; resuming builds a *new* deadline from the snapshot. The timer never counts time while paused because the deadline itself moves.
Common Errors & Fixes
st_autorefresh isn't registered (check it runs before any return/branch skips it), or the timer isn't in session state.deadline - now.duration before a preset sets it; initialize both keys together.timedelta(seconds=remaining) but remaining was recomputed *after* unpausing; capture the snapshot at pause time only.Key Concepts
What to Try Next
st.audio and a short beep file.st.plotly_chart instead of a bar.FAQ
Why not just time.sleep(1) in a loop?
It blocks the entire Streamlit script — no other widget responds while it sleeps. Autorefresh reruns the whole script cleanly instead, keeping the UI interactive.
Does the timer keep running if I close the tab?
The deadline lives on the server, so yes — reopening the page shows the correct remaining time. Only a server restart clears it.
Can I run multiple timers at once?
Yes — store a dict of deadlines in session state and render one column per timer, each with its own placeholder and pause state.