Format a date in JavaScript

A Date holds one moment in time. Formatting is choosing how to show it: in whose language, in which time zone, and for a person or for a program.

To format a date in JavaScript, pass it to toLocaleDateString or Intl.DateTimeFormat with a locale and an options object. date.toLocaleDateString('en-US', { dateStyle: 'long' }) returns "September 26, 2026". For a string a program reads, use toISOString().

Try the options here. Change the locale, the style and the time zone, and the code under the result changes with them.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Date format playground</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
  label { display: block; font-size: 12px; font-weight: 600; color: #4b5563; }
  input, select { width: 100%; box-sizing: border-box; margin-top: 4px; padding: 7px; font: inherit; font-size: 14px;
                  border: 1px solid #cfd4dc; border-radius: 8px; background: #fff; }
  .out { margin-top: 14px; background: #fff; border-radius: 12px; padding: 12px 14px; box-shadow: 0 2px 10px rgba(0,0,0,.06); }
  .big { font-size: 20px; font-weight: 600; margin: 4px 0 10px; overflow-wrap: anywhere; }
  .row { font-size: 13px; margin: 6px 0; overflow-wrap: anywhere; }
  .row b { display: inline-block; min-width: 118px; color: #6b7280; font-weight: 600; }
  pre { margin: 10px 0 0; background: #1d2330; color: #e5e7eb; padding: 10px 12px; border-radius: 8px;
        font-size: 12.5px; white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<div class="grid">
  <label>Date and time <input type="datetime-local" id="when" value="2026-09-26T14:30"></label>
  <label>Locale
    <select id="locale">
      <option value="">Your browser's default</option>
      <option>en-US</option><option>en-GB</option><option>de-DE</option>
      <option>fr-FR</option><option>ja-JP</option><option>ko-KR</option>
    </select>
  </label>
  <label>Style
    <select id="style">
      <option value="full">dateStyle: full</option>
      <option value="long" selected>dateStyle: long</option>
      <option value="medium">dateStyle: medium</option>
      <option value="short">dateStyle: short</option>
      <option value="both">dateStyle + timeStyle</option>
      <option value="parts">weekday, day, month name</option>
    </select>
  </label>
  <label>Time zone
    <select id="zone">
      <option value="">Your time zone</option>
      <option>UTC</option><option>America/New_York</option>
      <option>Europe/London</option><option>Asia/Tokyo</option>
    </select>
  </label>
</div>

<div class="out">
  <div class="big" id="result"></div>
  <div class="row"><b>toISOString()</b> <span id="iso"></span></div>
  <div class="row"><b>Your time zone</b> <span id="tz"></span></div>
  <pre id="code"></pre>
</div>

<script>
  const $ = (id) => document.getElementById(id);

  // each preset is an options object for Intl.DateTimeFormat
  const presets = {
    full: { dateStyle: 'full' },
    long: { dateStyle: 'long' },
    medium: { dateStyle: 'medium' },
    short: { dateStyle: 'short' },
    both: { dateStyle: 'medium', timeStyle: 'short' },
    parts: { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' },
  };

  function render() {
    const d = new Date($('when').value);  // "YYYY-MM-DDTHH:mm" is read as local time
    if (isNaN(d)) { $('result').textContent = 'Pick a date and time'; return; }

    const locale = $('locale').value || undefined;  // undefined = browser default
    const options = { ...presets[$('style').value] };
    if ($('zone').value) options.timeZone = $('zone').value;

    $('result').textContent = new Intl.DateTimeFormat(locale, options).format(d);
    $('iso').textContent = d.toISOString();
    $('tz').textContent = Intl.DateTimeFormat().resolvedOptions().timeZone;
    $('code').textContent =
      'const d = new Date("' + $('when').value + '");\n' +
      'new Intl.DateTimeFormat(' + (locale ? '"' + locale + '"' : 'undefined') + ', ' +
      JSON.stringify(options).replace(/"(\w+)":/g, '$1: ') + ').format(d);';
  }

  ['when', 'locale', 'style', 'zone'].forEach((id) => $(id).addEventListener('input', render));
  render();
</script>
</body>
</html>
The same Date through Intl.DateTimeFormat. The first locale option is whatever your browser is set to.

The rest of this guide covers what a Date actually holds, why dates come out a day off, and when to use each formatting method.

What a Date object holds

A Date is one number: milliseconds since midnight UTC on 1 January 1970. It has no time zone and no format of its own. Every method that shows it picks a time zone and a layout at that moment.

One stored moment, three readers, two different calendar dates.
One stored moment, three readers, two different calendar dates.

So new Date() is "now", everywhere at once. Methods such as getDate() and toString() show it in the device's time zone. getUTCDate() and toISOString() show it in UTC. Neither is wrong; they answer different questions.

Formatting for people: toLocaleDateString and Intl.DateTimeFormat

Both take the same two arguments: a locale such as 'en-GB', and an options object. Leave the locale out, or pass undefined, and the reader's browser language is used.

const d = new Date('2026-09-26T14:30');

d.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
// "Saturday, 26 September 2026"

const fmt = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' });
fmt.format(d);  // reuse fmt for every date in a list

There are two ways to fill the options object, and they do not mix:

Options Example Gives you
dateStyle, timeStyle { dateStyle: 'full' } A complete, locale-appropriate layout: full, long, medium or short
Individual fields { weekday: 'short', month: 'long', day: 'numeric' } Only the parts you name, arranged the locale's way
timeZone { timeZone: 'Asia/Tokyo' } The date as seen in that zone; works with either kind above
hour12 { hour: 'numeric', hour12: false } 24-hour or 12-hour clock regardless of locale

Combining dateStyle with a field such as weekday throws a TypeError. So does passing timeStyle to toLocaleDateString; use toLocaleString or Intl.DateTimeFormat for date and time together.

The locale decides order and words, not you. en-US puts the month first, en-GB the day, ja-JP the year. If you need exact pieces, fmt.formatToParts(d) returns them as an array you can rearrange.

Why the date is one day off

This bug comes from parsing, not formatting. The specification reads a date-only ISO string such as "2026-09-26" as midnight UTC. The same string with a time, "2026-09-26T00:00", is read as midnight local time.

Date-only is UTC. Date plus time is local. West of UTC, the first one shows as the day before.
Date-only is UTC. Date plus time is local. West of UTC, the first one shows as the day before.

In New York, midnight UTC is 8 PM the evening before, so toLocaleDateString() prints September 25. East of UTC it looks fine, which is why the bug often passes testing in one place and appears for users somewhere else.

The lab below runs the cases in your own time zone, then shows the same date-only value formatted for other zones.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Date pitfalls lab</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; font-size: 14px; }
  .tz { margin-bottom: 10px; color: #4b5563; }
  .box { background: #fff; border-radius: 12px; padding: 12px 14px; margin-bottom: 12px; box-shadow: 0 2px 10px rgba(0,0,0,.06); }
  h3 { margin: 0 0 8px; font-size: 14px; }
  table { border-collapse: collapse; width: 100%; font-size: 13px; }
  td { padding: 5px 4px; border-top: 1px solid #eef0f3; vertical-align: top; overflow-wrap: anywhere; }
  td:first-child { font-family: ui-monospace, Consolas, monospace; font-size: 12px; width: 48%; }
  .bad { color: #b45309; font-weight: 600; }
  .good { color: #047857; font-weight: 600; }
  .try { display: flex; gap: 8px; flex-wrap: wrap; }
  input { flex: 1 1 160px; padding: 7px; font: inherit; border: 1px solid #cfd4dc; border-radius: 8px; }
  button { padding: 7px 10px; font: inherit; font-size: 13px; border: 1px solid #cfd4dc; border-radius: 8px; background: #fff; cursor: pointer; }
  #parsed { margin-top: 8px; font-family: ui-monospace, Consolas, monospace; font-size: 12.5px; overflow-wrap: anywhere; }
</style>
</head>
<body>
<div class="tz">Your time zone: <b id="zone"></b></div>

<div class="box">
  <h3>1. The same text, read two ways</h3>
  <table id="parse"></table>
</div>

<div class="box">
  <h3>2. The date-only string, shown in other time zones</h3>
  <table id="zones"></table>
</div>

<div class="box">
  <h3>3. getMonth() starts at 0</h3>
  <table id="month"></table>
</div>

<div class="box">
  <h3>4. Try your own string</h3>
  <div class="try">
    <input id="text" value="26/09/2026">
    <button data-v="2026-09-26">2026-09-26</button>
    <button data-v="2026-09-26T09:00">…T09:00</button>
    <button data-v="2026-09-26T09:00Z">…T09:00Z</button>
  </div>
  <div id="parsed"></div>
</div>

<script>
  const $ = (id) => document.getElementById(id);
  const row = (code, value, cls) =>
    '<tr><td>' + code + '</td><td class="' + (cls || '') + '">' + value + '</td></tr>';
  const day = (d, tz) => d.toLocaleDateString('en-US',
    { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZone: tz });

  $('zone').textContent = Intl.DateTimeFormat().resolvedOptions().timeZone;

  // 1. date-only = UTC midnight, date + time = local time
  const utc = new Date('2026-09-26');
  const local = new Date('2026-09-26T00:00');
  const off = utc.getDate() !== 26;
  $('parse').innerHTML =
    row('new Date("2026-09-26")', day(utc) + (off ? ' (a day off here)' : ' (fine in your zone, see 2)'), off ? 'bad' : '') +
    row('new Date("2026-09-26T00:00")', day(local), 'good') +
    row('new Date(2026, 8, 26)', day(new Date(2026, 8, 26)), 'good') +
    row('utc.toLocaleDateString("en-US", { timeZone: "UTC" })', utc.toLocaleDateString('en-US', { timeZone: 'UTC' }), 'good');

  // 2. the same moment formatted for people in other zones
  $('zones').innerHTML = ['America/Los_Angeles', 'America/New_York', 'UTC', 'Europe/Berlin', 'Asia/Tokyo']
    .map((tz) => {
      const dom = new Intl.DateTimeFormat('en-US', { day: 'numeric', timeZone: tz }).format(utc);
      return row(tz, day(utc, tz), dom === '26' ? 'good' : 'bad');
    }).join('');

  // 3. months are 0-11, days are 1-31
  const d = new Date(2026, 8, 26);
  const pad = (n) => String(n).padStart(2, '0');
  $('month').innerHTML =
    row('d.getMonth()', d.getMonth() + ' (September)', 'bad') +
    row('d.getFullYear() + "-" + d.getMonth() + "-" + d.getDate()',
        d.getFullYear() + '-' + d.getMonth() + '-' + d.getDate(), 'bad') +
    row('`${y}-${pad(m + 1)}-${pad(day)}`',
        d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()), 'good');

  // 4. anything that is not an ISO string is up to the browser
  function parse() {
    const p = new Date($('text').value);
    $('parsed').innerHTML = isNaN(p)
      ? '<span class="bad">Invalid Date</span>'
      : '<span class="good">' + p.toString() + '</span><br>toISOString(): ' + p.toISOString();
  }
  $('text').addEventListener('input', parse);
  document.querySelectorAll('button[data-v]').forEach((b) =>
    b.addEventListener('click', () => { $('text').value = b.dataset.v; parse(); }));
  parse();
</script>
</body>
</html>
Your time zone decides whether section 1 is off. Section 2 shows other zones wherever you are.

Three fixes, depending on what the string means:

  1. It is a calendar date with no time. Build it as local: new Date(2026, 8, 26), or append "T00:00" before parsing.
  2. You must keep it as UTC. Format with { timeZone: 'UTC' } so display matches how it was read.
  3. It came from a date input. HTML date input default value covers value versus valueAsDate, which has the same trap.

Strings in other shapes, such as "26/09/2026", are outside the specification. Each browser decides, and the result can be Invalid Date. Parse only ISO strings, or split the parts yourself.

Formatting for machines: toISOString

toISOString() always returns UTC in one fixed shape: 2026-09-26T14:30:00.000Z. The Z means UTC. Use it for storage, for sending to a server, and for comparing or sorting dates as text.

It does not depend on the reader's locale, which is the point. It also throws a RangeError on an invalid date, so check isNaN(d) first when the input came from a user.

Format a date as yyyy-mm-dd

For a fixed pattern in local time, build it from the parts. getMonth() counts from 0, so September is 8.

const pad = (n) => String(n).padStart(2, '0');
const ymd = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;

ymd(new Date(2026, 8, 26));  // "2026-09-26"

toISOString().slice(0, 10) gives the same shape but the UTC date, which is the off-by-one bug again near midnight. Use the getUTC... methods in the helper instead if you do want UTC.

Four jobs, four methods. Pick by who reads the result.
Four jobs, four methods. Pick by who reads the result.

"3 days ago": Intl.RelativeTimeFormat

Intl.RelativeTimeFormat words a number and a unit. It does not measure the gap; you pass it in.

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-3, 'day');  // "3 days ago"
rtf.format(1, 'day');   // "tomorrow"
rtf.format(2, 'week');  // "in 2 weeks"

For whole days, set both dates to local midnight with setHours(0, 0, 0, 0), subtract, divide by 86,400,000 and round. Rounding absorbs the 23-hour and 25-hour days around daylight saving changes.

A finished example: an events list

This list takes ISO timestamps, shows each in the reader's language and time zone, adds a relative label, and marks up every date with <time datetime>.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Upcoming events</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  header { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
  h2 { margin: 0; font-size: 18px; }
  select { padding: 6px; font: inherit; font-size: 13px; border: 1px solid #cfd4dc; border-radius: 8px; background: #fff; }
  ul { list-style: none; margin: 0; padding: 0; }
  li { background: #fff; border-radius: 12px; padding: 12px 14px; margin-bottom: 10px;
       box-shadow: 0 2px 10px rgba(0,0,0,.06); display: flex; justify-content: space-between; gap: 12px; align-items: baseline; }
  .name { font-weight: 600; }
  time { display: block; font-size: 13px; color: #4b5563; margin-top: 3px; }
  .rel { font-size: 12px; font-weight: 600; white-space: nowrap; padding: 3px 8px; border-radius: 99px; background: #e0f2fe; color: #075985; }
  .rel.past { background: #eef0f3; color: #6b7280; }
  pre { margin: 6px 0 0; background: #1d2330; color: #e5e7eb; padding: 10px 12px; border-radius: 8px;
        font-size: 12px; white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<header>
  <h2>Upcoming events</h2>
  <select id="locale" aria-label="Locale">
    <option value="">Your browser's language</option>
    <option>en-US</option><option>en-GB</option><option>de-DE</option><option>ja-JP</option>
  </select>
</header>
<ul id="list"></ul>
<div style="font-size:12px;color:#6b7280">Markup of the first item:</div>
<pre id="markup"></pre>

<script>
  // Example data: offsets from today so the labels stay meaningful.
  // Real data would be ISO strings from a server, e.g. "2026-09-28T16:00:00Z".
  function at(daysFromToday, hour, minute) {
    const d = new Date();
    d.setDate(d.getDate() + daysFromToday);
    d.setHours(hour, minute, 0, 0);
    return d.toISOString();
  }
  const events = [
    { name: 'Team retro', start: at(-3, 15, 0) },
    { name: 'Product demo', start: at(0, 17, 30) },
    { name: 'Design review', start: at(1, 10, 0) },
    { name: 'Launch party', start: at(2, 18, 0) },
    { name: 'Quarterly planning', start: at(12, 9, 0) },
  ];

  // Whole calendar days between today and the event, in the reader's time zone
  function daysFromToday(date) {
    const a = new Date(); a.setHours(0, 0, 0, 0);
    const b = new Date(date); b.setHours(0, 0, 0, 0);
    return Math.round((b - a) / 86400000);
  }

  function render() {
    const locale = document.getElementById('locale').value || undefined;
    const when = new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' });
    const rel = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });  // "tomorrow", "in 2 days"

    const list = document.getElementById('list');
    list.innerHTML = '';
    for (const ev of events) {
      const d = new Date(ev.start);
      const days = daysFromToday(d);

      const li = document.createElement('li');
      const left = document.createElement('div');
      left.innerHTML = '<div class="name"></div><time></time>';
      left.querySelector('.name').textContent = ev.name;
      const t = left.querySelector('time');
      t.dateTime = d.toISOString();       // for machines: one exact moment
      t.textContent = when.format(d);     // for people: their locale and time zone

      const badge = document.createElement('span');
      badge.className = 'rel' + (days < 0 ? ' past' : '');
      badge.textContent = rel.format(days, 'day');

      li.append(left, badge);
      list.append(li);
    }
    document.getElementById('markup').textContent = list.querySelector('time').outerHTML;
  }

  document.getElementById('locale').addEventListener('input', render);
  render();
</script>
</body>
</html>
Switch the locale. The visible text changes; the datetime attribute stays the same.

The <time> element carries two versions of the date. The text between the tags is for people. The datetime attribute holds a machine-readable value, such as a toISOString() result, for search engines, scripts and assistive tools.

<time datetime="2026-09-28T16:00:00.000Z">Sep 28, 2026, 6:00 PM</time>

To count down to one of these events instead, see the HTML countdown timer. To refresh a relative label on a schedule, setTimeout and setInterval covers the timers.

When it does not work

What you see Cause Fix
The date is one day early A date-only string was parsed as UTC Add T00:00, use new Date(y, m, d), or format with timeZone: 'UTC'
The month is one too low, or 0 in January getMonth() counts from 0 Add 1 when displaying; subtract 1 when building
Invalid Date A non-ISO string such as 26/09/2026 Parse ISO strings only, or split the parts
RangeError from toISOString The Date is invalid Check isNaN(d) before formatting
TypeError about options dateStyle mixed with weekday or month, or timeStyle in toLocaleDateString Use one kind of option; use Intl.DateTimeFormat for date and time
Text differs on another device No locale or timeZone given, so the device settings apply Pass both when output must be fixed; do not hard-code expected strings in tests
The time is hours off after saving Local time was stored without its offset Store toISOString() and format on display

Date formatting depends on where it runs, so a screenshot from your machine shows only your locale and time zone. A live page shows each person their own.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so each person who opens it sees dates in their own language and time zone. If you change the code later, the same link shows the new version.

Questions people ask

How do I format a date as yyyy-mm-dd in JavaScript?

Take getFullYear(), getMonth() + 1 and getDate(), and pad the month and day to two digits with String(n).padStart(2, '0'). That gives the local date. toISOString().slice(0, 10) also looks like yyyy-mm-dd, but it is the UTC date and can be a day off.

Why is my JavaScript date one day off?

Usually because a date-only string such as "2026-09-26" was parsed. The specification reads that form as midnight UTC, and anywhere west of UTC that moment falls on the previous local day. Parse "2026-09-26T00:00" or use new Date(2026, 8, 26) for local midnight, or format with timeZone: 'UTC'.

What is the difference between toLocaleDateString and Intl.DateTimeFormat?

They take the same locale and options arguments and produce the same text. Intl.DateTimeFormat builds a formatter object you can reuse for many dates, and it also offers formatToParts and formatRange. toLocaleDateString is a one-off call on a single date.

Why does the same code print different text on another computer?

Without a locale argument the browser uses its own language setting, and without timeZone it uses the device's time zone. The locale data also comes from the browser, so spacing and punctuation can differ between browsers. Pass a locale and timeZone when the output must be fixed.

How do I show "3 days ago" in JavaScript?

Work out the difference yourself, then pass it to Intl.RelativeTimeFormat: new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(-3, 'day') returns "3 days ago". With numeric: 'auto', -1 and 1 become "yesterday" and "tomorrow".

Keep reading