Making Calendars With Accessibility and Internationalization in Mind
Tags: article, customelements, i18n, locales
Published: 2023-08-21 21:02:23

---
title: Making Calendars With Accessibility and Internationalization in Mind
published: true
date: 2023-03-13 13:23:52 UTC
tags: Article,customelements,i18n,locales
canonical_url: https://css-tricks.com/making-calendars-with-accessibility-and-internationalization-in-mind/
---

Doing a quick search here on CSS-Tricks shows just how many different ways there are to approach calendars. Some show how [CSS Grid can create the layout efficiently](https://css-tricks.com/a-calendar-in-three-lines-of-css/). Some attempt to [bring actual data into the mix](https://css-tricks.com/how-to-make-a-monthly-calendar-with-real-data/). Some [rely on a framework](https://css-tricks.com/lets-make-a-vue-powered-monthly-calendar/) to help with state management.

There are many considerations when building a calendar component — far more than what is covered in the articles I linked up. If you think about it, calendars are fraught with nuance, from handling timezones and date formats to localization and even making sure dates flow from one month to the next… and that’s before we even get into accessibility and additional layout considerations depending on where the calendar is displayed and whatnot.

Many developers fear the [`Date()` object](https://css-tricks.com/everything-you-need-to-know-about-date-in-javascript/) and stick with older libraries like [`moment.js`](https://momentjs.com). But while there are many “gotchas” when it comes to dates and formatting, JavaScript has a lot of cool APIs and stuff to help out!

![January 2023 calendar grid.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2023/01/s_5F326355ADAED13048ECB603E300F82A04D113F56D11D632592576A458AB8C94_1672583219797_image.png?resize=464%2C251&ssl=1)

I don’t want to re-create the wheel here, but I will show you how we can get a dang good calendar with vanilla JavaScript. We’ll look into **accessibility** , using semantic markup and screenreader-friendly `<time>` -tags — as well as **internationalization** and **formatting** , using the `Intl.Locale`, `Intl.DateTimeFormat` and `Intl.NumberFormat`-APIs.

In other words, we’re making a calendar… only without the extra dependencies you might typically see used in a tutorial like this, and with some of the nuances you might not typically see. And, in the process, I hope you’ll gain a new appreciation for newer things that JavaScript can do while getting an idea of the sorts of things that cross my mind when I’m putting something like this together.

### First off, naming

What should we call our calendar component? In my native language, it would be called “kalender element”, so let’s use that and shorten that to “Kal-El” — also known as [Superman’s name on the planet Krypton](https://the-superman.fandom.com/wiki/Kal-El).

Let’s create a function to get things going:

```
function kalEl(settings = {}) { ... }
```

This method will render **a single month**. Later we’ll call this method from `[...Array(12).keys()]` to render an entire year.

### Initial data and internationalization

One of the common things a typical online calendar does is highlight the current date. So let’s create a reference for that:

```
const today = new Date();
```

Next, we’ll create a “configuration object” that we’ll merge with the optional `settings` object of the primary method:

```
const config = Object.assign(
  {
    locale: (document.documentElement.getAttribute('lang') || 'en-US'), 
    today: { 
      day: today.getDate(),
      month: today.getMonth(),
      year: today.getFullYear() 
    } 
  }, settings
);
```

We check, if the root element (`<html>`) contains a `lang`-attribute with **locale** info; otherwise, we’ll fallback to using `en-US`. This is the first step toward [internationalizing the calendar](https://www.w3.org/International/questions/qa-html-language-declarations).

We also need to determine which month to initially display when the calendar is rendered. That’s why we extended the `config` object with the primary `date`. This way, if no date is provided in the `settings` object, we’ll use the `today` reference instead:

```
const date = config.date ? new Date(config.date) : today;
```

We need a little more info to properly format the calendar based on locale. For example, we might not know whether the first day of the week is Sunday or Monday, depending on the locale. If we have the info, great! But if not, we’ll update it using the [`Intl.Locale` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale). The API has a [`weekInfo` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/weekInfo) that returns a `firstDay` property that gives us exactly what we’re looking for without any hassle. We can also get which days of the week are assigned to the `weekend`:

```
if (!config.info) config.info = new Intl.Locale(config.locale).weekInfo || { 
  firstDay: 7,
  weekend: [6, 7] 
};
```

Again, we create fallbacks. The “first day” of the week for `en-US` is Sunday, so it defaults to a value of `7`. This is a little confusing, as the [`getDay` method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getDay) in JavaScript returns the days as `[0-6]`, where `0` is Sunday… don’t ask me why. The weekends are Saturday and Sunday, hence `[6, 7]`.

Before we had the `Intl.Locale` API and its `weekInfo` method, it was pretty hard to create an international calendar without many \*\*objects and arrays with information about each locale or region. Nowadays, it’s easy-peasy. If we pass in `en-GB`, the method returns:

```
// en-GB
{
  firstDay: 1,
  weekend: [6, 7],
  minimalDays: 4
}
```

In a country like Brunei (`ms-BN`), the weekend is Friday and Sunday:

```
// ms-BN
{
  firstDay: 7,
  weekend: [5, 7],
  minimalDays: 1
}
```

You might wonder what that `minimalDays` property is. That’s the [fewest days required in the first week of a month to be counted as a full week](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Patterns_Week_Elements). In some regions, it might be just one day. For others, it might be a full seven days.

Next, we’ll create a `render` method within our `kalEl`-method:

```
const render = (date, locale) => { ... }
```

We still need some more data to work with before we render anything:

```
const month = date.getMonth();
const year = date.getFullYear();
const numOfDays = new Date(year, month + 1, 0).getDate();
const renderToday = (year === config.today.year) && (month === config.today.month);
```

The last one is a `Boolean` that checks whether `today` exists in the month we’re about to render.

### Semantic markup

We’re going to get deeper in rendering in just a moment. But first, I want to make sure that the details we set up have semantic HTML tags associated with them. Setting that up right out of the box gives us accessibility benefits from the start.

#### Calendar wrapper

First, we have the non-semantic wrapper: `<kal-el>`. That’s fine because there isn’t a semantic `<calendar>` tag or anything like that. If we weren’t making a custom element, `<article>` might be the most appropriate element since the calendar could stand on its own page.

#### Month names

The `<time>` element is going to be a big one for us because it helps translate dates into a format that screenreaders and search engines can parse more accurately and consistently. For example, here’s how we can convey “January 2023” in our markup:

```
<time datetime="2023-01">January <i>2023</i></time>
```

#### Day names

The row above the calendar’s dates containing the names of the days of the week can be tricky. It’s ideal if we can write out the full names for each day — e.g. Sunday, Monday, Tuesday, etc. — but that can take up a lot of space. So, let’s abbreviate the names for now inside of an `<ol>` where each day is a `<li>`:

```
<ol>
  <li><abbr title="Sunday">Sun</abbr></li>
  <li><abbr title="Monday">Mon</abbr></li>
  <!-- etc. -->
</ol>
```

We could get tricky with CSS to get the best of both worlds. For example, if we modified the markup a bit like this:

```
<ol>
  <li>
    <abbr title="S">Sunday</abbr>
  </li>
</ol>
```

…we get the full names by default. We can then “hide” the full name when space runs out and display the `title` attribute instead:

```
@media all and (max-width: 800px) {
  li abbr::after {
    content: attr(title);
  }
}
```

But, we’re not going that way because the `Intl.DateTimeFormat` API can help here as well. We’ll get to that in the next section when we cover rendering.

#### Day numbers

Each date in the calendar grid gets a number. Each number is a list item (`<li>`) in an ordered list (`<ol>`), and the inline `<time>` tag wraps the actual number.

```
<li>
  <time datetime="2023-01-01">1</time>
</li>
```

And while I’m not planning to do any styling just yet, I know I will want some way to style the date numbers. That’s possible as-is, but I also want to be able to style weekday numbers differently than weekend numbers if I need to. So, I’m going to include [`data-*` attributes](https://css-tricks.com/a-complete-guide-to-data-attributes/) specifically for that: `data-weekend` and `data-today`.

#### Week numbers

There are 52 weeks in a year, sometimes 53. While it’s not super common, it can be nice to display the number for a given week in the calendar for additional context. I like having it now, even if I don’t wind up not using it. But we’ll totally use it in this tutorial.

We’ll use a `data-weeknumber` attribute as a styling hook and include it in the markup for each date that is the week’s first date.

```
<li data-day="7" data-weeknumber="1" data-weekend="">
  <time datetime="2023-01-08">8</time>
</li>
```

### Rendering

Let’s get the calendar on a page! We already know that `<kal-el>` is the name of our custom element. First thing we need to configure it is to set the `firstDay` property on it, so the calendar knows whether Sunday or some other day is the first day of the week.

```
<kal-el data-firstday="${ config.info.firstDay }">
```

We’ll be using [template literals](https://css-tricks.com/template-literals/) to render the markup. To format the dates for an international audience, we’ll use the `Intl.DateTimeFormat` API, again using the `locale` we specified earlier.

#### The month and year

When we call the `month`, we can set whether we want to use the `long` name (e.g. February) or the `short` name (e.g. Feb.). Let’s use the `long` name since it’s the title above the calendar:

```
<time datetime="${year}-${(pad(month))}">
  ${new Intl.DateTimeFormat(
    locale,
    { month:'long'}).format(date)} <i>${year}</i>
</time>
```

#### Weekday names

For weekdays displayed above the grid of dates, we need both the `long` (e.g. “Sunday”) and `short` (abbreviated, ie. “Sun”) names. This way, we can use the “short” name when the calendar is short on space:

```
Intl.DateTimeFormat([locale], { weekday: 'long' })
Intl.DateTimeFormat([locale], { weekday: 'short' })
```

Let’s make a small helper method that makes it a little easier to call each one:

```
const weekdays = (firstDay, locale) => {
  const date = new Date(0);
  const arr = [...Array(7).keys()].map(i => {
    date.setDate(5 + i)
    return {
      long: new Intl.DateTimeFormat([locale], { weekday: 'long'}).format(date),
      short: new Intl.DateTimeFormat([locale], { weekday: 'short'}).format(date)
    }
  })
  for (let i = 0; i < 8 - firstDay; i++) arr.splice(0, 0, arr.pop());
  return arr;
}
```

Here’s how we invoke that in the template:

```
<ol>
  ${weekdays(config.info.firstDay,locale).map(name => `
    <li>
      <abbr title="${name.long}">${name.short}</abbr>
    </li>`).join('')
  }
</ol>
```

#### Day numbers

And finally, the days, wrapped in an `<ol>` element:

```
${[...Array(numOfDays).keys()].map(i => {
  const cur = new Date(year, month, i + 1);
  let day = cur.getDay(); if (day === 0) day = 7;
  const today = renderToday && (config.today.day === i + 1) ? ' data-today':'';
  return `
    <li data-day="${day}"${today}${i === 0 || day === config.info.firstDay ? ` data-weeknumber="${new Intl.NumberFormat(locale).format(getWeek(cur))}"`:''}${config.info.weekend.includes(day) ? ` data-weekend`:''}>
      <time datetime="${year}-${(pad(month))}-${pad(i)}" tabindex="0">
        ${new Intl.NumberFormat(locale).format(i + 1)}
      </time>
    </li>`
}).join('')}
```

Let’s break that down:

1. We create a “dummy” array, based on the “number of days” variable, which we’ll use to iterate.
2. We create a `day` variable for the current day in the iteration.
3. We fix the discrepancy between the `Intl.Locale` API and `getDay()`.
4. If the `day` is equal to `today`, we add a `data-*` attribute.
5. Finally, we return the `<li>` element as a string with merged data.
6. `tabindex="0"` makes the element focusable, when using keyboard navigation, after any positive tabindex values (Note: you should **never** add **positive** tabindex-values)

To [“pad” the numbers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart) in the `datetime` attribute, we use a little helper method:

```
const pad = (val) => (val + 1).toString().padStart(2, '0');
```

#### Week number

Again, the “week number” is where a week falls in a 52-week calendar. We use a little helper method for that as well:

```
function getWeek(cur) {
  const date = new Date(cur.getTime());
  date.setHours(0, 0, 0, 0);
  date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
  const week = new Date(date.getFullYear(), 0, 4);
  return 1 + Math.round(((date.getTime() - week.getTime()) / 86400000 - 3 + (week.getDay() + 6) % 7) / 7);
}
```

I didn’t write this `getWeek`-method. It’s a cleaned up version of [this script](https://weeknumber.com/how-to/javascript).

And that’s it! Thanks to the [`Intl.Locale`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale), [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) and [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) APIs, we can now simply change the `lang`-attribute of the `<html>` element to change the context of the calendar based on the current region:

![January 2023 calendar grid.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2023/01/s_5F326355ADAED13048ECB603E300F82A04D113F56D11D632592576A458AB8C94_1672589519354_image.png?resize=456%2C284&ssl=1)
_de-DE_

![January 2023 calendar grid.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2023/01/s_5F326355ADAED13048ECB603E300F82A04D113F56D11D632592576A458AB8C94_1672589309595_image.png?resize=444%2C253&ssl=1)
_fa-IR_

![January 2023 calendar grid.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2023/01/s_5F326355ADAED13048ECB603E300F82A04D113F56D11D632592576A458AB8C94_1672589340340_image.png?resize=451%2C257&ssl=1)
_zh-Hans-CN-u-nu-hanidec_

### Styling the calendar

You might recall how all the days are just one `<ol>` with list items. To style these into a readable calendar, we dive into the wonderful world of CSS Grid. In fact, we can repurpose the same grid from [a starter calendar template right here on CSS-Tricks](https://css-tricks.com/snippets/css/css-grid-starter-layouts/), but updated a smidge with the `:is()` relational pseudo to optimize the code.

Notice that I’m defining configurable CSS variables along the way (and prefixing them with `---kalel-` to avoid conflicts).

```
kal-el :is(ol, ul) {
  display: grid;
  font-size: var(--kalel-fz, small);
  grid-row-gap: var(--kalel-row-gap, .33em);
  grid-template-columns: var(--kalel-gtc, repeat(7, 1fr));
  list-style: none;
  margin: unset;
  padding: unset;
  position: relative;
}
```

![Seven-column calendar grid with grid lines shown.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2023/01/s_5F326355ADAED13048ECB603E300F82A04D113F56D11D632592576A458AB8C94_1672590153826_image.png?resize=397%2C266&ssl=1)

Let’s draw borders around the date numbers to help separate them visually:

```
kal-el :is(ol, ul) li {
  border-color: var(--kalel-li-bdc, hsl(0, 0%, 80%));
  border-style: var(--kalel-li-bds, solid);
  border-width: var(--kalel-li-bdw, 0 0 1px 0);
  grid-column: var(--kalel-li-gc, initial);
  text-align: var(--kalel-li-tal, end); 
}
```

The seven-column grid works fine when the first day of the month is _also_ the first day of the week for the selected locale). But that’s the exception rather than the rule. Most times, we’ll need to shift the first day of the month to a different weekday.

![Showing the first day of the month falling on a Thursday.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2023/01/s_5F326355ADAED13048ECB603E300F82A04D113F56D11D632592576A458AB8C94_1672590297938_image.png?resize=390%2C266&ssl=1)

Remember all the extra `data-*` attributes we defined when writing our markup? We can hook into those to update which grid column (`--kalel-li-gc`) the first date number of the month is placed on:

```
[data-firstday="1"] [data-day="3"]:first-child {
  --kalel-li-gc: 1 / 4;
}
```

In this case, we’re spanning from the first grid column to the fourth grid column — which will automatically “push” the next item (Day 2) to the fifth grid column, and so forth.

Let’s add a little style to the “current” date, so it stands out. These are just my styles. You can totally do what you’d like here.

```
[data-today] {
  --kalel-day-bdrs: 50%;
  --kalel-day-bg: hsl(0, 86%, 40%);
  --kalel-day-hover-bgc: hsl(0, 86%, 70%);
  --kalel-day-c: #fff;
}
```

I like the idea of styling the date numbers for weekends differently than weekdays. I’m going to use a reddish color to style those. Note that we can reach for the `:not()` pseudo-class to select them while leaving the current date alone:

```
[data-weekend]:not([data-today]) { 
  --kalel-day-c: var(--kalel-weekend-c, hsl(0, 86%, 46%));
}
```

Oh, and let’s not forget the week numbers that go before the first date number of each week. We used a `data-weeknumber` attribute in the markup for that, but the numbers won’t actually display unless we reveal them with CSS, which we can do on the `::before` pseudo-element:

```
[data-weeknumber]::before {
  display: var(--kalel-weeknumber-d, inline-block);
  content: attr(data-weeknumber);
  position: absolute;
  inset-inline-start: 0;
  /* additional styles */
}
```

We’re technically done at this point! We can render a calendar grid that shows the dates for the current month, complete with considerations for localizing the data by locale, and ensuring that the calendar uses proper semantics. And all we used was vanilla JavaScript and CSS!

But let’s take this _one more step_…

### Rendering an entire year

Maybe you need to display a full year of dates! So, rather than render the current month, you might want to display all of the month grids for the current year.

Well, the nice thing about the approach we’re using is that we can call the `render` method as many times as we want and merely change the integer that identifies the month on each instance. Let’s call it 12 times based on the current year.

as simple as calling the `render`-method 12 times, and just change the integer for `month` — `i`:

```
[...Array(12).keys()].map(i =>
  render(
    new Date(date.getFullYear(),
    i,
    date.getDate()),
    config.locale,
    date.getMonth()
  )
).join('')
```

It’s probably a good idea to create a new parent wrapper for the rendered year. Each calendar grid is a `<kal-el>` element. Let’s call the new parent wrapper `<jor-el>`, where [Jor-El is the name of Kal-El’s father](https://superman.fandom.com/wiki/Jor-El).

```
<jor-el id="app" data-year="true">
  <kal-el data-firstday="7">
    <!-- etc. -->
  </kal-el>

  <!-- other months -->
</jor-el>
```

We can use `<jor-el>` to create a grid for our grids. So meta!

```
jor-el {
  background: var(--jorel-bg, none);
  display: var(--jorel-d, grid);
  gap: var(--jorel-gap, 2.5rem);
  grid-template-columns: var(--jorel-gtc, repeat(auto-fill, minmax(320px, 1fr)));
  padding: var(--jorel-p, 0);
}
```

### Final demo

<iframe id="cp_embed_vYaKKKJ" src="//codepen.io/anon/embed/vYaKKKJ?height=500&amp;theme-id=1&amp;slug-hash=vYaKKKJ&amp;default-tab=result" height="500" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed vYaKKKJ" title="CodePen Embed vYaKKKJ" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

### Bonus: Confetti Calendar

I read an excellent book called _[Making and Breaking the Grid](https://www.goodreads.com/book/show/35863579-making-and-breaking-the-grid)_ the other day and stumbled on this beautiful “New Year’s poster”:

![](https://i0.wp.com/paper-attachments.dropboxusercontent.com/s_5F326355ADAED13048ECB603E300F82A04D113F56D11D632592576A458AB8C94_1673333960043_IMG_1264.jpeg?ssl=1)
_Source: Making and Breaking the Grid (2nd Edition) by Timothy Samara_

I figured we could do something similar without changing anything in the HTML or JavaScript. I’ve taken the liberty to include full names for months, and numbers instead of day names, to make it more readable. Enjoy!

<iframe id="cp_embed_bGjqqoj" src="//codepen.io/anon/embed/bGjqqoj?height=750&amp;theme-id=1&amp;slug-hash=bGjqqoj&amp;default-tab=result" height="750" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed bGjqqoj" title="CodePen Embed bGjqqoj" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

* * *

<small><a rel="nofollow" href="https://css-tricks.com/making-calendars-with-accessibility-and-internationalization-in-mind/">Making Calendars With Accessibility and Internationalization in Mind</a> originally published on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>, which is part of the <a href="https://try.digitalocean.com/css-tricks/?utm_medium=rss&amp;utm_source=css-tricks.com&amp;utm_campaign=family_&amp;utm_content=">DigitalOcean</a> family. You should <a href="https://css-tricks.com/newsletters/">get the newsletter</a>.</small>