Every 20 minutes, look at something 20 feet away for 20 seconds. Fixed on purpose: that's the whole rule, not a setting to tune. Use it right here, or copy the code below into your own project.
Focus20:00Cycle 1
Why 20-20-20, Specifically
Digital eye strain isn't caused by screens being uniquely harmful, it's caused by staring at anything at a fixed close distance for long stretches without the eye ever getting to relax its focus. The 20-20-20 rule exists because it's specific and memorable enough to actually get followed, not because those exact three numbers were scientifically singled out over every nearby alternative.
What's Actually Happening to Your Eyes
Focusing up close keeps the ciliary muscle inside the eye contracted to bend the lens for near vision, and holding that contraction for an hour straight is what produces the ache, blurred distance vision, and fatigue people describe as eye strain. Looking at something 20 feet away lets that muscle relax fully, since anything past roughly 20 feet requires essentially no focusing effort at all, it's the eye equivalent of standing up after sitting too long.
Why the Rule Isn't Adjustable Here
Every other tool on this site lets you tune the numbers, but this one deliberately doesn't. 20-20-20 is a known, quotable rule precisely because it's the same three numbers everywhere you encounter it, and turning it into a configurable interval would trade that recognizability for a feature nobody actually asked for.
Pairing It With Other Habits
This timer handles the interval, but it can't fix a monitor sitting too close, a bad glare source, or air that's too dry, all of which compound eye strain independently of screen time. It's most useful stacked alongside the basics: correct viewing distance, blinking deliberately since screen use measurably reduces blink rate, and decent ambient lighting so the screen isn't the brightest thing in the room.
Copy the Code
Plain HTML, CSS, and JavaScript, no dependencies, no build step. Comments walk through what each block is doing, so it's easy to adapt rather than just paste and forget.
<!-- 20-20-20 Eye Break Reminder -- copy this whole block into any HTML page. -->
<!-- No build step, no dependencies -- plain HTML, CSS, and JS. -->
<div id="eye-break">
<div class="eb-display">
<span class="eb-phase" id="ebPhase">Focus</span>
<span class="eb-clock" id="ebClock">20:00</span>
<span class="eb-cycle" id="ebCycle">Cycle 1</span>
</div>
<div class="eb-controls">
<button id="ebStart" type="button">Start</button>
<button id="ebReset" type="button">Reset</button>
</div>
</div>
<style>
/* Minimal styling -- restyle freely to match your own site. */
#eye-break { font-family: sans-serif; max-width: 280px; text-align: center; }
.eb-display { margin-bottom: 12px; }
.eb-clock { display: block; font-size: 2.5rem; font-weight: 700; }
.eb-phase { display: block; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
.eb-cycle { display: block; font-size: 0.8rem; opacity: 0.7; }
.eb-controls button { padding: 8px 16px; margin: 0 4px; cursor: pointer; }
</style>
<script>
(function () {
// The rule: 20 minutes of focus, then 20 seconds looking at
// something 20 feet away. Both durations are fixed on purpose --
// that's the whole rule, not a setting to tune.
var FOCUS_SECONDS = 20 * 60;
var BREAK_SECONDS = 20;
var startBtn = document.getElementById('ebStart');
var resetBtn = document.getElementById('ebReset');
var clockEl = document.getElementById('ebClock');
var phaseEl = document.getElementById('ebPhase');
var cycleEl = document.getElementById('ebCycle');
var phase = 'focus';
var secondsLeft = FOCUS_SECONDS;
var cycle = 1;
var intervalId = null;
// Turn a raw seconds count into "MM:SS" for display.
function formatClock(totalSeconds) {
var m = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
var s = Math.floor(totalSeconds % 60).toString().padStart(2, '0');
return m + ':' + s;
}
// Push the current state onto the page.
function render() {
clockEl.textContent = formatClock(secondsLeft);
phaseEl.textContent = phase === 'focus' ? 'Focus' : 'Look Away';
cycleEl.textContent = 'Cycle ' + cycle;
}
// A short tone on each phase change, built with the Web Audio API
// so there's no audio file to host or load.
function beep() {
try {
var Ctx = window.AudioContext || window.webkitAudioContext;
var ctx = new Ctx();
var osc = ctx.createOscillator();
var gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.value = phase === 'focus' ? 660 : 880;
gain.gain.setValueAtTime(0.15, ctx.currentTime);
osc.start();
osc.stop(ctx.currentTime + 0.35);
} catch (err) {
// If the browser blocks audio (e.g. no user interaction yet), just skip it.
}
}
// Flip from focus to break, or break back to focus, and load the
// clock with whichever phase's fixed duration comes next.
function switchPhase() {
beep();
if (phase === 'focus') {
phase = 'break';
secondsLeft = BREAK_SECONDS;
} else {
phase = 'focus';
cycle += 1;
secondsLeft = FOCUS_SECONDS;
}
render();
}
// Runs once a second while the timer is active.
function tick() {
secondsLeft -= 1;
if (secondsLeft <= 0) {
switchPhase();
return;
}
render();
}
function stop() {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
startBtn.textContent = 'Start';
}
function start() {
intervalId = setInterval(tick, 1000);
startBtn.textContent = 'Pause';
}
// Start/pause toggle.
startBtn.addEventListener('click', function () {
if (intervalId) {
stop();
} else {
start();
}
});
// Reset always goes back to a fresh focus phase, cycle 1.
resetBtn.addEventListener('click', function () {
stop();
phase = 'focus';
cycle = 1;
secondsLeft = FOCUS_SECONDS;
render();
});
render();
})();
</script>