The Rustling of the Leaves

Relative date filter in Eleventy

For this new blog, I wanted to display the time since the post was published, e.g. “two days ago”. My CMS appends a published and updated date to each post automatically, indicating when it was originally published and when it was last updated. These are formatted like this: 2026-02-18T21:58:50.158Z.

I wanted to use the templating language support built into Eleventy to turn a hard-to-read date string into a relative date, such as “5 days ago”. In Eleventy, you can set up custom filters that allow you to easily transform data, which is exactly what I'm trying to do here. For example, let's say you are editing a page with a title property of “About Me". If you wanted to render this in uppercase, you would write {{ title | upcase }} (in Liquid). Let's introduce a relative_date filter.

The first step is working out the logic that translates the time difference between Date.now() and the timestamp attached to each post. I went with the following implementation:

const relativeDate = (input) => {
  const diff = new Date(input) - Date.now();
  const rel = new Intl.RelativeTimeFormat("en", {
    style: "long",
    numeric: "auto", // Gives us "yesterday" instead of "1 day ago", which I prefer
  });

  const units = [
    { unit: "year", ms: 1000 * 60 * 60 * 24 * 365 },
    { unit: "month", ms: 1000 * 60 * 60 * 24 * 30 }, // Choosing to ignore that months have different durations in days
    { unit: "week", ms: 1000 * 60 * 60 * 24 * 7 },
    { unit: "day", ms: 1000 * 60 * 60 * 24 },
    { unit: "hour", ms: 1000 * 60 * 60 },
    { unit: "minute", ms: 1000 * 60 },
  ];

  // Find the largest time unit that exceeds the corresponding ms thresholds
  const { unit, ms } =
    units.find(({ ms }) => Math.abs(diff) >= ms) ?? units.at(-1);
  return rel.format(Math.round(diff / ms), unit);
};

This relies on the native JavaScript Intl.RelativeTimeFormat API, which provides all this logic out of the box. It's amazing to have such a powerful API natively supported in the language, rather than having to use a library. It's good separation of concerns to have this code as a module that we can invoke in a few places (as will become clear later).

Second, we want to ensure that this code is accessible within the templates of my blog.

import { relativeDate } from "./scripts/relativeDate.js";

eleventyConfig.addFilter("relative_date", (input) => {
    return `<span class="relative-date" data-date="${input}">${relativeDate(
      input,
    )}</span>`;
  });

This code creates a filter relative_date which wraps the input (the data) in a span element with a relevant class. I'm also baking the publication date into a data-* attribute, so it's easier to use in the next step.

Combining these two steps, we now have what we need. Let's say we have a Markdown page with the following frontmatter.

---
title: Relative date filter in Eleventy
tags:
  - eleventy
  - javascript
published: '2026-02-23T16:16:35.098Z'
updated: '2026-02-23T16:28:24.036Z'
---

We can now easily use our filter {{ published | relative_date }} to get:

Finally, Eleventy generates static sites and the above code is only generated at build time. If we built the site a week ago, then the relative time difference will be a week out of date! Therefore, we grab all elements with the class relative-date and update their contents at run time. Because we baked the published date in the span element itself, we can recalculate the relative time difference.

const updateRelativeDate = () => {
  const elements = document.querySelectorAll(".relative-date");
  elements.forEach((el) => {
    el.textContent = relativeDate(el.dataset.date);
  });
};

Finally, in our base template, we add the following code:

<script type="module">
  import { updateRelativeDate } from './scripts/relativeDate.js';

  updateRelativeDate();
  setInterval(updateRelativeDate, 60_000);
</script>

This ensures that our relative time difference is always accurate to within one minute!

4 weeks ago
(last updated: 4 weeks ago)