What You'll Build
A Unix timestamp converter with a live clock at the top (ticking every second), two conversion modes (timestamp → date and date → timestamp), and multiple output formats including ISO 8601, UTC, and local time.
- Live clock showing current Unix time and milliseconds
- Pause, copy, and "use as input" buttons for the clock
- Timestamp → Date conversion (auto-detects seconds vs. milliseconds)
- Date → Timestamp conversion
- Six output formats: ISO 8601, UTC, local, timezone offset
- Copy any format to clipboard
⏱️ Unix Timestamp Converter
Convert between epoch and human-readable dates
What You'll Learn
- The Date object: creating dates from timestamps and strings
- Unix time: seconds since Jan 1, 1970, UTC
- Seconds vs. milliseconds: auto-detecting which unit is used
- Timezone handling: UTC vs. local display
- setInterval: running code every second for the live clock
- Multiple formats: ISO 8601, UTC, local, and offset display
Core idea: A Unix timestamp is just a number — seconds since Jan 1, 1970, UTC. The same number means different calendar dates depending on the timezone you display it in. This project teaches you to navigate both.
Build the HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Unix Timestamp Converter</title>
</head>
<body>
<div class="card">
<h1>Unix Timestamp Converter</h1>
<p class="sub">Convert between epoch and human-readable dates</p>
<div class="live-clock">
<div class="lbl">Current Unix Time</div>
<div class="val" id="liveSeconds">—</div>
<div class="ms" id="liveMs">—</div>
<div class="clock-actions">
<button class="clock-btn" id="pauseBtn">Pause</button>
<button class="clock-btn" id="useNowBtn">Use Now</button>
</div>
</div>
<div class="modes">
<button class="mode active" data-mode="toDate">Timestamp to Date</button>
<button class="mode" data-mode="toTs">Date to Timestamp</button>
</div>
<div class="field" id="formToDate">
<label>Unix Timestamp</label>
<input type="text" id="tsInput" value="1700000000">
</div>
<div class="field" id="formToTs" style="display:none;">
<label>Date & Time</label>
<input type="datetime-local" id="dateInput">
</div>
<div class="error" id="errorMsg"></div>
<div class="format-grid" id="formatGrid"></div>
<div id="relativeWrap"></div>
</div>
<script src="script.js"></script>
</body>
</html>
Style It with CSS
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f1f5f9;
padding: 20px;
color: #0f172a;
}
.card {
max-width: 540px;
margin: 0 auto;
background: #fff;
border-radius: 16px;
padding: 24px;
box-shadow: 0 10px 40px rgba(15,23,42,0.08);
}
h1 { font-size: 1.3rem; margin-bottom: 4px; }
.sub { color: #64748b; font-size: 0.85rem; margin-bottom: 16px; }
/* Live clock */
.live-clock {
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
border-radius: 12px;
padding: 18px;
text-align: center;
color: #fff;
margin-bottom: 18px;
}
.live-clock .lbl {
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
opacity: 0.9;
font-weight: 700;
}
.live-clock .val {
font-size: 2rem;
font-weight: 900;
font-family: "Courier New", monospace;
line-height: 1;
margin-top: 6px;
letter-spacing: -0.02em;
}
.live-clock .ms {
font-size: 0.78rem;
opacity: 0.85;
margin-top: 4px;
font-family: "Courier New", monospace;
}
.clock-actions {
display: flex;
justify-content: center;
gap: 6px;
margin-top: 12px;
flex-wrap: wrap;
}
.clock-btn {
background: rgba(255,255,255,0.15);
border: 1px solid rgba(255,255,255,0.25);
color: #fff;
padding: 5px 12px;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
cursor: pointer;
transition: 0.2s;
font-family: inherit;
}
.clock-btn:hover { background: rgba(255,255,255,0.25); }
/* Mode buttons */
.modes {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-bottom: 14px;
}
.mode {
background: #f8fafc;
border: 2px solid #e2e8f0;
color: #64748b;
padding: 10px;
border-radius: 10px;
font-weight: 700;
font-size: 0.82rem;
cursor: pointer;
transition: 0.2s;
font-family: inherit;
}
.mode.active {
background: #eff6ff;
border-color: #3b82f6;
color: #1d4ed8;
}
/* Inputs */
.field { margin-bottom: 10px; }
label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #334155;
margin-bottom: 4px;
}
input[type="text"], input[type="datetime-local"] {
width: 100%;
padding: 10px 12px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 600;
font-family: "Courier New", monospace;
outline: none;
transition: 0.2s;
}
input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}
/* Format cards */
.format-grid {
display: grid;
grid-template-columns: 1fr;
gap: 6px;
margin-top: 14px;
}
.format-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 8px 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.format-card.highlight {
border-color: #3b82f6;
background: #eff6ff;
}
.format-card .info { flex: 1; min-width: 0; }
.format-card .lbl {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 700;
color: #64748b;
margin-bottom: 2px;
}
.format-card .val {
font-size: 0.82rem;
font-family: "Courier New", monospace;
font-weight: 600;
color: #0f172a;
word-break: break-all;
}
.format-card.highlight .val { color: #1d4ed8; }
.copy-s {
background: transparent;
border: 1px solid #cbd5e1;
color: #64748b;
padding: 3px 8px;
border-radius: 5px;
cursor: pointer;
font-size: 0.68rem;
transition: 0.2s;
font-family: inherit;
flex-shrink: 0;
}
.copy-s:hover {
background: #fff;
border-color: #3b82f6;
color: #1d4ed8;
}
/* Relative time badge */
.relative {
display: inline-block;
background: #eff6ff;
color: #1d4ed8;
padding: 4px 10px;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
margin-top: 8px;
}
/* Error */
.error {
background: #fef2f2;
border: 1px solid #fca5a5;
color: #dc2626;
padding: 8px 12px;
border-radius: 8px;
font-size: 0.78rem;
margin: 8px 0;
display: none;
}
.error.show { display: block; }
Write the JavaScript — Live Clock
// ============================================================
// STATE + DOM
// ============================================================
var paused = false;
var mode = "toDate";
var liveSecondsEl = document.getElementById("liveSeconds");
var liveMsEl = document.getElementById("liveMs");
var tsInputEl = document.getElementById("tsInput");
var dateInputEl = document.getElementById("dateInput");
var formatGrid = document.getElementById("formatGrid");
var errorEl = document.getElementById("errorMsg");
var relativeWrap = document.getElementById("relativeWrap");
// ============================================================
// LIVE CLOCK
// ============================================================
// Date.now() returns milliseconds since epoch.
// Divide by 1000 and floor to get seconds.
function tick() {
if (paused) return;
var now = Date.now();
liveSecondsEl.textContent = Math.floor(now / 1000).toLocaleString();
liveMsEl.textContent = now.toLocaleString() + " ms";
}
// setInterval runs the tick every 1000ms (1 second)
setInterval(tick, 1000);
tick(); // run once immediately
// Pause / Resume
document.getElementById("pauseBtn").addEventListener("click", function() {
paused = !paused;
this.textContent = paused ? "▶ Resume" : "⏸ Pause";
});
// Use current time as input
document.getElementById("useNowBtn").addEventListener("click", function() {
tsInputEl.value = Math.floor(Date.now() / 1000);
setMode("toDate");
convert();
});
Why 1000ms and not 1ms? setInterval fires at most every 4ms in browsers, and updating the DOM 1000 times per second wastes CPU. The live clock displays seconds — updating once per second is more than enough.
Write the JavaScript — Mode & Conversion
// ============================================================
// MODE SWITCHING
// ============================================================
document.querySelectorAll(".mode").forEach(function(btn) {
btn.addEventListener("click", function() { setMode(btn.dataset.mode); });
});
function setMode(m) {
mode = m;
document.querySelectorAll(".mode").forEach(function(b) {
b.classList.toggle("active", b.dataset.mode === m);
});
document.getElementById("formToDate").style.display = m === "toDate" ? "block" : "none";
document.getElementById("formToTs").style.display = m === "toTs" ? "block" : "none";
errorEl.classList.remove("show");
convert();
}
// ============================================================
// DEFAULT DATE for date-to-timestamp mode
// ============================================================
function setDefaultDate() {
var now = new Date();
var offset = now.getTimezoneOffset();
// Build local-time string for the datetime-local input
var local = new Date(now.getTime() - offset * 60000);
dateInputEl.value = local.toISOString().slice(0, 16);
}
setDefaultDate();
// ============================================================
// GET DATE — reads the current mode's input, returns a Date
// ============================================================
function getDate() {
errorEl.classList.remove("show");
if (mode === "toDate") {
var raw = tsInputEl.value.trim();
if (!raw) return null;
// Strip non-numeric chars and parse
var num = parseFloat(raw.replace(/[^\d.-]/g, ""));
if (isNaN(num)) {
showError("Please enter a valid number.");
return null;
}
// Auto-detect seconds vs. milliseconds
// 10-digit timestamp = seconds
// 13-digit timestamp = milliseconds
var ms = Math.abs(num) > 1e12 ? num : num * 1000;
var d = new Date(ms);
if (isNaN(d.getTime())) {
showError("Could not parse timestamp.");
return null;
}
return d;
} else {
var v = dateInputEl.value;
if (!v) return null;
var d = new Date(v);
if (isNaN(d.getTime())) {
showError("Could not parse date.");
return null;
}
return d;
}
}
function showError(msg) {
errorEl.textContent = "❌ " + msg;
errorEl.classList.add("show");
formatGrid.innerHTML = "";
relativeWrap.innerHTML = "";
}
Write the JavaScript — Render Formats
// ============================================================
// CONVERT — build the six formats and render
// ============================================================
function convert() {
var date = getDate();
if (!date) return;
var ms = date.getTime();
var s = Math.floor(ms / 1000);
var formats = [
{ lbl: "Unix (seconds)", val: String(s), hi: mode === "toTs" },
{ lbl: "Unix (milliseconds)", val: String(ms), hi: false },
{ lbl: "ISO 8601", val: date.toISOString(), hi: mode === "toDate" },
{ lbl: "UTC", val: date.toUTCString(), hi: false },
{ lbl: "Local", val: date.toLocaleString(), hi: false },
{ lbl: "Timezone Offset", val: formatOffset(date), hi: false }
];
formatGrid.innerHTML = formats.map(function(f) {
return '<div class="format-card' + (f.hi ? ' highlight' : '') + '">' +
'<div class="info"><div class="lbl">' + f.lbl + '</div>' +
'<div class="val">' + escapeHtml(f.val) + '</div></div>' +
'<button class="copy-s" data-val="' + escapeHtml(f.val) + '">Copy</button>' +
'</div>';
}).join("");
// Attach copy handlers
formatGrid.querySelectorAll(".copy-s").forEach(function(btn) {
btn.addEventListener("click", function() {
navigator.clipboard.writeText(btn.dataset.val).then(function() {
var orig = btn.textContent;
btn.textContent = "✅";
setTimeout(function() { btn.textContent = orig; }, 1200);
});
});
});
relativeWrap.innerHTML = '<div class="relative">' + getRelative(date) + '</div>';
}
// ============================================================
// HELPERS
// ============================================================
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
// Timezone offset like "UTC+05:00" (Pakistan) or "UTC-08:00" (California)
function formatOffset(date) {
var o = -date.getTimezoneOffset(); // getTimezoneOffset returns minutes WEST of UTC, so negate
var sign = o >= 0 ? "+" : "-";
var h = Math.floor(Math.abs(o) / 60);
var m = Math.abs(o) % 60;
return "UTC" + sign + String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0");
}
// Human-friendly relative time: "2 years ago", "3 months from now"
function getRelative(date) {
var diff = date.getTime() - Date.now();
var abs = Math.abs(diff);
var suffix = diff > 0 ? "from now" : "ago";
var s = Math.round(abs / 1000);
var m = Math.round(s / 60);
var h = Math.round(m / 60);
var d = Math.round(h / 24);
var mo = Math.round(d / 30.44);
var y = Math.round(mo / 12);
var out;
if (s < 45) out = s + " second" + (s === 1 ? "" : "s");
else if (m < 45) out = m + " minute" + (m === 1 ? "" : "s");
else if (h < 22) out = h + " hour" + (h === 1 ? "" : "s");
else if (d < 26) out = d + " day" + (d === 1 ? "" : "s");
else if (mo < 11) out = mo + " month" + (mo === 1 ? "" : "s");
else out = y + " year" + (y === 1 ? "" : "s");
return out + " " + suffix;
}
// ============================================================
// LIVE UPDATES + BOOT
// ============================================================
tsInputEl.addEventListener("input", convert);
dateInputEl.addEventListener("input", convert);
convert();
Save and open index.html. The clock ticks every second. Paste a timestamp — you see six formats. Switch to Date mode — pick a date, get the timestamp.
Understand the Big Ideas
1. The Unix epoch
January 1, 1970, 00:00:00 UTC is "timestamp zero". Every Unix timestamp is a count of seconds since that moment. Why 1970? Because when Unix was being built at Bell Labs, that was a recent round date that fit in a 32-bit integer.
2. Seconds vs. milliseconds
Unix timestamps are traditionally in seconds (10 digits). JavaScript's Date.now() returns milliseconds (13 digits). When you receive a timestamp, check the digit count:
10 digits (e.g., 1700000000) → seconds 13 digits (e.g., 1700000000000) → milliseconds 16 digits → microseconds Our code auto-detects: if num > 1e12, treat as ms; else multiply by 1000.
3. UTC vs. local time
A timestamp is the same number everywhere on Earth. But when you convert it to a human-readable string, you must decide: UTC or local?
date.toISOString()→ always UTC, always with Z suffixdate.toUTCString()→ always UTC, human-friendlydate.toLocaleString()→ uses the visitor's timezone
Real-world bug: Storing user birthdays as local timestamps causes off-by-one errors. A birthday entered as "Jan 1" in Tokyo might display as "Dec 31" in New York. Always store dates as UTC or date-only strings, never as local timestamps.
4. The getTimezoneOffset() quirk
Despite the name, getTimezoneOffset() returns minutes west of UTC. Pakistan (UTC+5) returns −300. California (UTC−8) returns 480. We negate it to get the intuitive "east is positive" value.
5. Date string parsing gotcha
new Date("2024-01-01") is treated as UTC midnight. new Date("2024-01-01T00:00:00") is treated as local midnight. Adding the time part changes the timezone interpretation. This trips up beginners constantly.
Rule: If you're parsing a date string, always include either Z (UTC) or a timezone offset. Otherwise, the browser guesses, and it guesses differently for different formats.
Practice Challenges
🟢 Beginner
- Add a stop/start toggle for the live clock
- Add a copy button on the live clock value
- Show day of week in the formats list
🟡 Intermediate
- Add batch conversion — paste multiple timestamps, convert all
- Add a timezone selector — display in any IANA timezone
- Add relative time auto-updating with the clock
🔴 Advanced
- Add a date difference calculator — days between two dates
- Add year 2038 countdown — live countdown to the 32-bit overflow
- Add a week number display (ISO week)
Compare your solution: We built the production version with batch conversion and more formats — see the live Timestamp Converter.
Full Source Code
Common Mistakes & Fixes
❌ "My date is off by one day"
Timezone issue. When constructing a date from a string, add Z for UTC or T00:00:00 for local. Avoid bare date strings like "2024-01-01".
❌ "Timestamp in 2024 shows as 1970"
You passed seconds to new Date(), which expects milliseconds. Multiply by 1000 first — or use our auto-detect: if the number is under 1e12, multiply.
❌ "Clock doesn't tick"
Check the interval is set with setInterval(tick, 1000), not setTimeout. setTimeout runs once and stops.
❌ "The paused clock still updates ms line"
Our pause toggles the whole tick function, so neither line updates when paused. If you want to freeze only the seconds line, wrap only that update in the check.
❌ "Copy buttons don't work"
The Clipboard API requires HTTPS or localhost. If you're on file://, run a local server (python -m http.server) to test.