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
▶ Live Preview — this is what you'll build

What You'll Learn

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.

1

Build the HTML Structure

HTML
<!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>
2

Style It with CSS

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; }
3

Write the JavaScript — Live Clock

JavaScript — Part 1: 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.

4

Write the JavaScript — Mode & Conversion

JavaScript — Part 2: Convert
// ============================================================
// 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 = "";
}
5

Write the JavaScript — Render Formats

JavaScript — Part 3: 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.

6

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:

Rule of thumb
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?

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.

7

Practice Challenges

🟢 Beginner

🟡 Intermediate

🔴 Advanced

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.

What to Build Next