What You'll Build
An age calculator that takes a date of birth and shows the exact age in years, months, and days. It updates live as you pick a different date, handles edge cases like leap years, and shows the total days lived.
- Accepts a date of birth from the user
- Calculates exact age — years, months, and days
- Shows the total days you've been alive
- Validates input (no future dates)
- Updates instantly as you change the date
What You'll Learn
- JavaScript Date object: creating, reading, and comparing dates
- Date math: calculating differences in days, months, and years
- Edge cases: leap years, month-end handling, future dates
- HTML date input: using
<input type="date">with min/max constraints - Pattern reinforcement: input → calculate → display, now with a harder data type
Build the HTML Structure
Create index.html. The structure is similar to the BMI project — a card, a date input, and a result area.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Age Calculator</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="card"> <h1>🎂 Age Calculator</h1> <p class="sub">Pick your date of birth</p> <label for="dob">Date of Birth</label> <input type="date" id="dob"> <div class="result" id="result"> <div class="years" id="years">—</div> <div class="years-label">Years Old</div> <div class="detail" id="detail">Enter your date of birth</div> <div class="days" id="days"></div> </div> <div class="error" id="error"></div> </div> <script src="script.js"></script> </body> </html>
Key HTML piece: <input type="date"> gives you a native date picker in every modern browser — no library needed. The max attribute (set in JavaScript) prevents future dates.
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;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
color: #0f172a;
}
.card {
background: #fff;
border-radius: 16px;
padding: 28px;
max-width: 400px;
width: 100%;
box-shadow: 0 10px 40px rgba(15,23,42,0.1);
}
h1 { font-size: 1.4rem; margin-bottom: 4px; }
.sub { color: #64748b; font-size: 0.85rem; margin-bottom: 20px; }
label {
display: block;
font-size: 0.82rem;
font-weight: 600;
color: #334155;
margin-bottom: 6px;
}
input[type="date"] {
width: 100%;
padding: 12px 14px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-size: 1rem;
font-weight: 600;
outline: none;
transition: 0.2s;
font-family: inherit;
}
input[type="date"]:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}
.result {
margin-top: 20px;
padding: 20px;
border-radius: 12px;
text-align: center;
background: #f1f5f9;
}
.result .years {
font-size: 3rem;
font-weight: 900;
line-height: 1;
color: #3b82f6;
font-family: "Courier New", monospace;
}
.result .years-label {
font-size: 0.85rem;
color: #64748b;
margin-top: 6px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.result .detail {
font-size: 0.9rem;
color: #475569;
margin-top: 12px;
font-family: "Courier New", monospace;
}
.result .days {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #cbd5e1;
font-size: 0.85rem;
color: #64748b;
}
.result .days span {
color: #16a34a;
font-weight: 700;
font-family: "Courier New", monospace;
}
.error {
color: #dc2626;
font-size: 0.85rem;
margin-top: 12px;
text-align: center;
display: none;
}
.error.show { display: block; }
Write the JavaScript Logic
This is the interesting part. The Date object is trickier than it looks — that's why this project matters.
// 1. Grab elements const dobEl = document.getElementById("dob"); const yearsEl = document.getElementById("years"); const detailEl = document.getElementById("detail"); const daysEl = document.getElementById("days"); const errorEl = document.getElementById("error"); // 2. Prevent future dates const todayStr = new Date().toISOString().split("T")[0]; dobEl.max = todayStr; // 3. Main calculate function function calculate() { const value = dobEl.value; errorEl.classList.remove("show"); // Guard: empty input if (!value) { yearsEl.textContent = "—"; detailEl.textContent = "Enter your date of birth"; daysEl.textContent = ""; return; } // 4. Parse the date — add T00:00:00 to force local timezone const dob = new Date(value + "T00:00:00"); const now = new Date(); // Guard: future date if (dob > now) { errorEl.textContent = "Date of birth can't be in the future."; errorEl.classList.add("show"); yearsEl.textContent = "—"; detailEl.textContent = "Invalid date"; daysEl.textContent = ""; return; } // 5. Calculate years, months, days let years = now.getFullYear() - dob.getFullYear(); let months = now.getMonth() - dob.getMonth(); let days = now.getDate() - dob.getDate(); // If days is negative, borrow from the previous month if (days < 0) { months--; // Day 0 of next month = last day of previous month const prevMonth = new Date(now.getFullYear(), now.getMonth(), 0); days += prevMonth.getDate(); } if (months < 0) { years--; months += 12; } // 6. Total days alive const diffMs = now - dob; const totalDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); // 7. Render yearsEl.textContent = years; detailEl.textContent = years + " year" + (years !== 1 ? "s" : "") + ", " + months + " month" + (months !== 1 ? "s" : "") + ", " + days + " day" + (days !== 1 ? "s" : ""); daysEl.innerHTML = "Total: <span>" + totalDays.toLocaleString() + "</span> days alive"; } // 8. Live updates dobEl.addEventListener("change", calculate); dobEl.addEventListener("input", calculate);
Save all three files, open index.html, and pick a date. You should see your exact age in years, months, and days, plus the total days you've been alive.
Understand the Tricky Parts
Why add T00:00:00 to the date string?
new Date("1990-05-15") is interpreted as UTC midnight, but new Date("1990-05-15T00:00:00") is interpreted as local midnight. In timezones behind UTC (like the US), this difference can shift the date by one day. Adding T00:00:00 ensures the date is treated consistently in the user's local timezone.
This is a famous JavaScript gotcha. new Date("2024-01-01") will show as December 31, 2023 in US timezones. The T00:00:00 suffix is the standard fix.
Why is getMonth() 0-indexed?
JavaScript's getMonth() returns 0 for January and 11 for December — a quirk from the 1990s that can't be fixed without breaking every website in the world. In this project we don't care because we're only subtracting months, and the difference is the same either way.
Why borrow from the previous month?
If today is May 10 and you were born May 15, getDate() returns 10 − 15 = −5. Negative days don't make sense, so we borrow from the month: −5 + (days in April) = 25, and decrement the months count.
The trick new Date(year, month, 0) gives you the last day of the previous month — that's how we know how many days to borrow.
Practice Challenges
🟢 Beginner
- Show the next birthday countdown in days
- Display the day of the week you were born on
- Add a reset button that clears the input
🟡 Intermediate
- Show age in hours, minutes, and seconds
- Add a "calculate as of" date so users can see their age at a past or future date
- Show the total heartbeats (age in minutes × 75) for fun
🔴 Advanced
- Add zodiac sign and Chinese zodiac based on the date
- Show a timeline of major world events the user has lived through
- Save the date of birth to
localStorageso it persists
Compare your solution: We built a full production version with all the advanced features — see the live Age Calculator.
Full Source Code
Common Mistakes & Fixes
❌ "My age is off by one day"
This is almost always the UTC vs. local timezone issue. Make sure you add "T00:00:00" to the date string.
❌ "The input shows no calendar on mobile"
Some older Android browsers don't support <input type="date"> natively. It degrades gracefully — users can still type the date. For full compatibility, use a library like Flatpickr (covered in a later project).
❌ "My 'total days' number is off"
Check that you're using Math.floor() and not rounding. Days should be whole numbers — fractions would represent partial days.