{"id":376950,"date":"2023-03-13T06:23:52","date_gmt":"2023-03-13T13:23:52","guid":{"rendered":"https:\/\/css-tricks.com\/?p=376950"},"modified":"2023-03-13T06:24:00","modified_gmt":"2023-03-13T13:24:00","slug":"making-calendars-with-accessibility-and-internationalization-in-mind","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/making-calendars-with-accessibility-and-internationalization-in-mind\/","title":{"rendered":"Making Calendars With Accessibility and Internationalization in Mind"},"content":{"rendered":"\n

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<\/a>. Some attempt to bring actual data into the mix<\/a>. Some rely on a framework<\/a> to help with state management.<\/p>\n\n\n\n

There are many considerations when building a calendar component \u2014 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\u2026 and that\u2019s before we even get into accessibility and additional layout considerations depending on where the calendar is displayed and whatnot.<\/p>\n\n\n\n

Many developers fear the Date()<\/code> object<\/a> and stick with older libraries like moment.js<\/code><\/a>. But while there are many \u201cgotchas\u201d when it comes to dates and formatting, JavaScript has a lot of cool APIs and stuff to help out!<\/p>\n\n\n\n\n\n\n\n

\"January<\/figure>\n\n\n\n

I don\u2019t want to re-create the wheel here, but I will show you how we can get a dang good calendar with vanilla JavaScript. We\u2019ll look into accessibility<\/strong>, using semantic markup and screenreader-friendly <time><\/code> -tags \u2014 as well as internationalization<\/strong> and formatting<\/strong>, using the Intl.Locale<\/code>, Intl.DateTimeFormat<\/code> and Intl.NumberFormat<\/code>-APIs.<\/p>\n\n\n\n

In other words, we\u2019re making a calendar\u2026 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\u2019ll 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\u2019m putting something like this together.<\/p>\n\n\n

First off, naming<\/h3>\n\n\n

What should we call our calendar component? In my native language, it would be called \u201ckalender element\u201d, so let\u2019s use that and shorten that to \u201cKal-El\u201d \u2014 also known as Superman\u2019s name on the planet Krypton<\/a>.<\/p>\n\n\n\n

Let\u2019s create a function to get things going:<\/p>\n\n\n\n

function kalEl(settings = {}) { ... }<\/code><\/pre>\n\n\n\n

This method will render a single month<\/strong>. Later we\u2019ll call this method from [...Array(12).keys()]<\/code> to render an entire year.<\/p>\n\n\n

Initial data and internationalization<\/h3>\n\n\n

One of the common things a typical online calendar does is highlight the current date. So let\u2019s create a reference for that:<\/p>\n\n\n\n

const today = new Date();<\/code><\/pre>\n\n\n\n

Next, we\u2019ll create a \u201cconfiguration object\u201d that we\u2019ll merge with the optional settings<\/code> object of the primary method:<\/p>\n\n\n\n

const config = Object.assign(\n  {\n    locale: (document.documentElement.getAttribute('lang') || 'en-US'), \n    today: { \n      day: today.getDate(),\n      month: today.getMonth(),\n      year: today.getFullYear() \n    } \n  }, settings\n);<\/code><\/pre>\n\n\n\n

We check, if the root element (<html><\/code>) contains a lang<\/code>-attribute with locale<\/strong> info; otherwise, we\u2019ll fallback to using en-US<\/code>. This is the first step toward internationalizing the calendar<\/a>.<\/p>\n\n\n\n

We also need to determine which month to initially display when the calendar is rendered. That’s why we extended the config<\/code> object with the primary date<\/code>. This way, if no date is provided in the settings<\/code> object, we\u2019ll use the today<\/code> reference instead:<\/p>\n\n\n\n

const date = config.date ? new Date(config.date) : today;<\/code><\/pre>\n\n\n\n

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\u2019ll update it using the Intl.Locale<\/code> API<\/a>. The API has a weekInfo<\/code> object<\/a> that returns a firstDay<\/code> property that gives us exactly what we\u2019re looking for without any hassle. We can also get which days of the week are assigned to the weekend<\/code>:<\/p>\n\n\n\n

if (!config.info) config.info = new Intl.Locale(config.locale).weekInfo || { \n  firstDay: 7,\n  weekend: [6, 7] \n};<\/code><\/pre>\n\n\n\n

Again, we create fallbacks. The \u201cfirst day\u201d of the week for en-US<\/code> is Sunday, so it defaults to a value of 7<\/code>. This is a little confusing, as the getDay<\/code> method<\/a> in JavaScript returns the days as [0-6]<\/code>, where 0<\/code> is Sunday\u2026 don\u2019t ask me why. The weekends are Saturday and Sunday, hence [6, 7]<\/code>.<\/p>\n\n\n\n

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

\/\/ en-GB\n{\n  firstDay: 1,\n  weekend: [6, 7],\n  minimalDays: 4\n}<\/code><\/pre>\n\n\n\n

In a country like Brunei (ms-BN<\/code>), the weekend is Friday and Sunday:<\/p>\n\n\n\n

\/\/ ms-BN\n{\n  firstDay: 7,\n  weekend: [5, 7],\n  minimalDays: 1\n}<\/code><\/pre>\n\n\n\n

You might wonder what that minimalDays<\/code> property is. That\u2019s the fewest days required in the first week of a month to be counted as a full week<\/a>. In some regions, it might be just one day. For others, it might be a full seven days.<\/p>\n\n\n\n

Next, we\u2019ll create a render<\/code> method within our kalEl<\/code>-method:<\/p>\n\n\n\n

const render = (date, locale) => { ... }<\/code><\/pre>\n\n\n\n

We still need some more data to work with before we render anything:<\/p>\n\n\n\n

const month = date.getMonth();\nconst year = date.getFullYear();\nconst numOfDays = new Date(year, month + 1, 0).getDate();\nconst renderToday = (year === config.today.year) && (month === config.today.month);<\/code><\/pre>\n\n\n\n

The last one is a Boolean<\/code> that checks whether today<\/code> exists in the month we\u2019re about to render.<\/p>\n\n\n

Semantic markup<\/h3>\n\n\n

We\u2019re 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.<\/p>\n\n\n

Calendar wrapper<\/h4>\n\n\n

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

Month names<\/h4>\n\n\n

The <time><\/code> 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\u2019s how we can convey \u201cJanuary 2023\u201d in our markup:<\/p>\n\n\n\n

<time datetime=\"2023-01\">January <i>2023<\/i><\/time><\/code><\/pre>\n\n\n

Day names<\/h4>\n\n\n

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

<ol>\n  <li><abbr title=\"Sunday\">Sun<\/abbr><\/li>\n  <li><abbr title=\"Monday\">Mon<\/abbr><\/li>\n  <!-- etc. -->\n<\/ol><\/code><\/pre>\n\n\n\n

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

<ol>\n  <li>\n    <abbr title=\"S\">Sunday<\/abbr>\n  <\/li>\n<\/ol><\/code><\/pre>\n\n\n\n

\u2026we get the full names by default. We can then \u201chide\u201d the full name when space runs out and display the title<\/code> attribute instead:<\/p>\n\n\n\n

@media all and (max-width: 800px) {\n  li abbr::after {\n    content: attr(title);\n  }\n}<\/code><\/pre>\n\n\n\n

But, we\u2019re not going that way because the Intl.DateTimeFormat<\/code> API can help here as well. We\u2019ll get to that in the next section when we cover rendering.<\/p>\n\n\n

Day numbers<\/h4>\n\n\n

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

<li>\n  <time datetime=\"2023-01-01\">1<\/time>\n<\/li><\/code><\/pre>\n\n\n\n

And while I\u2019m not planning to do any styling just yet, I know I will want some way to style the date numbers. That\u2019s possible as-is, but I also want to be able to style weekday numbers differently than weekend numbers if I need to. So, I\u2019m going to include data-*<\/code> attributes<\/a> specifically for that: data-weekend<\/code> and data-today<\/code>.<\/p>\n\n\n

Week numbers<\/h4>\n\n\n

There are 52 weeks in a year, sometimes 53. While it\u2019s 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\u2019ll totally use it in this tutorial.<\/p>\n\n\n\n

We\u2019ll use a data-weeknumber<\/code> attribute as a styling hook and include it in the markup for each date that is the week’s first date.<\/p>\n\n\n\n

<li data-day=\"7\" data-weeknumber=\"1\" data-weekend=\"\">\n  <time datetime=\"2023-01-08\">8<\/time>\n<\/li><\/code><\/pre>\n\n\n

Rendering<\/h3>\n\n\n

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

<kal-el data-firstday=\"${ config.info.firstDay }\"><\/code><\/pre>\n\n\n\n

We\u2019ll be using template literals<\/a> to render the markup. To format the dates for an international audience, we\u2019ll use the Intl.DateTimeFormat<\/code> API, again using the locale<\/code> we specified earlier.<\/p>\n\n\n

The month and year<\/h4>\n\n\n

When we call the month<\/code>, we can set whether we want to use the long<\/code> name (e.g. February) or the short<\/code> name (e.g. Feb.). Let\u2019s use the long<\/code> name since it\u2019s the title above the calendar:<\/p>\n\n\n\n

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

Weekday names<\/h4>\n\n\n

For weekdays displayed above the grid of dates, we need both the long<\/code> (e.g. \u201cSunday\u201d) and short<\/code> (abbreviated, ie. \u201cSun\u201d) names. This way, we can use the \u201cshort\u201d name when the calendar is short on space:<\/p>\n\n\n\n

Intl.DateTimeFormat([locale], { weekday: 'long' })\nIntl.DateTimeFormat([locale], { weekday: 'short' })<\/code><\/pre>\n\n\n\n

Let\u2019s make a small helper method that makes it a little easier to call each one:<\/p>\n\n\n\n

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

Here\u2019s how we invoke that in the template:<\/p>\n\n\n\n

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

Day numbers<\/h4>\n\n\n

And finally, the days, wrapped in an <ol><\/code> element:<\/p>\n\n\n\n

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

Let\u2019s break that down:<\/p>\n\n\n\n

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

    To \u201cpad\u201d the numbers<\/a> in the datetime<\/code> attribute, we use a little helper method:<\/p>\n\n\n\n

    const pad = (val) => (val + 1).toString().padStart(2, '0');<\/code><\/pre>\n\n\n

    Week number<\/h4>\n\n\n

    Again, the \u201cweek number\u201d is where a week falls in a 52-week calendar. We use a little helper method for that as well:<\/p>\n\n\n\n

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

    I didn\u2019t write this getWeek<\/code>-method. It\u2019s a cleaned up version of this script<\/a>.<\/p>\n\n\n\n

    And that\u2019s it! Thanks to the Intl.Locale<\/code><\/a>, Intl.DateTimeFormat<\/code><\/a> and Intl.NumberFormat<\/code><\/a> APIs, we can now simply change the lang<\/code>-attribute of the <html><\/code> element to change the context of the calendar based on the current region:<\/p>\n\n\n\n

    \n
    \n
    \"January
    de-DE<\/code><\/figcaption><\/figure>\n<\/div>\n\n\n\n
    \n
    \"January
    fa-IR<\/code><\/figcaption><\/figure>\n<\/div>\n\n\n\n
    \n
    \"January
    zh-Hans-CN-u-nu-hanidec<\/code><\/figcaption><\/figure>\n<\/div>\n<\/div>\n\n\n

    Styling the calendar<\/h3>\n\n\n

    You might recall how all the days are just one <ol><\/code> 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<\/a>, but updated a smidge with the :is()<\/code> relational pseudo to optimize the code.<\/p>\n\n\n\n

    Notice that I\u2019m defining configurable CSS variables along the way (and prefixing them with ---kalel-<\/code> to avoid conflicts).<\/p>\n\n\n\n

    kal-el :is(ol, ul) {\n  display: grid;\n  font-size: var(--kalel-fz, small);\n  grid-row-gap: var(--kalel-row-gap, .33em);\n  grid-template-columns: var(--kalel-gtc, repeat(7, 1fr));\n  list-style: none;\n  margin: unset;\n  padding: unset;\n  position: relative;\n}<\/code><\/pre>\n\n\n\n
    \"Seven-column<\/figure>\n\n\n\n

    Let\u2019s draw borders around the date numbers to help separate them visually:<\/p>\n\n\n\n

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

    The seven-column grid works fine when the first day of the month is also<\/em> the first day of the week for the selected locale). But that\u2019s the exception rather than the rule. Most times, we\u2019ll need to shift the first day of the month to a different weekday.<\/p>\n\n\n\n

    \"Showing<\/figure>\n\n\n\n

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

    [data-firstday=\"1\"] [data-day=\"3\"]:first-child {\n  --kalel-li-gc: 1 \/ 4;\n}<\/code><\/pre>\n\n\n\n

    In this case, we\u2019re spanning from the first grid column to the fourth grid column \u2014 which will automatically \u201cpush\u201d the next item (Day 2) to the fifth grid column, and so forth.<\/p>\n\n\n\n

    Let\u2019s add a little style to the \u201ccurrent\u201d date, so it stands out. These are just my styles. You can totally do what you\u2019d like here.<\/p>\n\n\n\n

    [data-today] {\n  --kalel-day-bdrs: 50%;\n  --kalel-day-bg: hsl(0, 86%, 40%);\n  --kalel-day-hover-bgc: hsl(0, 86%, 70%);\n  --kalel-day-c: #fff;\n}<\/code><\/pre>\n\n\n\n

    I like the idea of styling the date numbers for weekends differently than weekdays. I\u2019m going to use a reddish color to style those. Note that we can reach for the :not()<\/code> pseudo-class to select them while leaving the current date alone:<\/p>\n\n\n\n

    [data-weekend]:not([data-today]) { \n  --kalel-day-c: var(--kalel-weekend-c, hsl(0, 86%, 46%));\n}<\/code><\/pre>\n\n\n\n

    Oh, and let\u2019s not forget the week numbers that go before the first date number of each week. We used a data-weeknumber<\/code> attribute in the markup for that, but the numbers won\u2019t actually display unless we reveal them with CSS, which we can do on the ::before<\/code> pseudo-element:<\/p>\n\n\n\n

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

    We\u2019re 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!<\/p>\n\n\n\n

    But let\u2019s take this one more step<\/em>\u2026<\/p>\n\n\n

    Rendering an entire year<\/h3>\n\n\n

    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.<\/p>\n\n\n\n

    Well, the nice thing about the approach we\u2019re using is that we can call the render<\/code> method as many times as we want and merely change the integer that identifies the month on each instance. Let\u2019s call it 12 times based on the current year.<\/p>\n\n\n\n

    as simple as calling the render<\/code>-method 12 times, and just change the integer for month<\/code> \u2014 i<\/code>:<\/p>\n\n\n\n

    [...Array(12).keys()].map(i =>\n  render(\n    new Date(date.getFullYear(),\n    i,\n    date.getDate()),\n    config.locale,\n    date.getMonth()\n  )\n).join('')<\/code><\/pre>\n\n\n\n

    It\u2019s probably a good idea to create a new parent wrapper for the rendered year. Each calendar grid is a <kal-el><\/code> element. Let\u2019s call the new parent wrapper <jor-el><\/code>, where Jor-El is the name of Kal-El\u2019s father<\/a>.<\/p>\n\n\n\n

    <jor-el id=\"app\" data-year=\"true\">\n  <kal-el data-firstday=\"7\">\n    <!-- etc. -->\n  <\/kal-el>\n\n  <!-- other months -->\n<\/jor-el><\/code><\/pre>\n\n\n\n

    We can use <jor-el><\/code> to create a grid for our grids. So meta!<\/p>\n\n\n\n

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

    Final demo<\/h3>\n\n\n