Making Calendars With Accessibility and Internationalization in Thoughts | CSS-Tips
[ad_1]
Doing a fast search right here on CSS-Tips exhibits simply what number of other ways there are to method calendars. Some present how CSS Grid can create the layout efficiently. Some try and bring actual data into the mix. Some rely on a framework to assist with state administration.
There are various issues when constructing a calendar part — way over what is roofed within the articles I linked up. If you consider it, calendars are fraught with nuance, from dealing with timezones and date codecs to localization and even ensuring dates stream from one month to the subsequent… and that’s earlier than we even get into accessibility and extra format issues relying on the place the calendar is displayed and whatnot.
Many builders concern the Date()
object and persist with older libraries like moment.js
. However whereas there are various “gotchas” in terms of dates and formatting, JavaScript has a variety of cool APIs and stuff to assist out!

I don’t wish to re-create the wheel right here, however I’ll present you the way we are able to get a dang good calendar with vanilla JavaScript. We’ll look into accessibility, utilizing semantic markup and screenreader-friendly <time>
-tags — in addition to internationalization and formatting, utilizing the Intl.Locale
, Intl.DateTimeFormat
and Intl.NumberFormat
-APIs.
In different phrases, we’re making a calendar… solely with out the additional dependencies you may sometimes see utilized in a tutorial like this, and with a few of the nuances you may not sometimes see. And, within the course of, I hope you’ll achieve a brand new appreciation for newer issues that JavaScript can do whereas getting an concept of the types of issues that cross my thoughts after I’m placing one thing like this collectively.
First off, naming
What ought to we name our calendar part? In my native language, it might be referred to as “kalender factor”, so let’s use that and shorten that to “Kal-El” — often known as Superman’s name on the planet Krypton.
Let’s create a perform to get issues going:
perform kalEl(settings = {}) { ... }
This methodology will render a single month. Later we’ll name this methodology from [...Array(12).keys()]
to render a whole yr.
Preliminary knowledge and internationalization
One of many widespread issues a typical on-line calendar does is spotlight the present date. So let’s create a reference for that:
const right now = new Date();
Subsequent, we’ll create a “configuration object” that we’ll merge with the non-obligatory settings
object of the first methodology:
const config = Object.assign(
{
locale: (doc.documentElement.getAttribute('lang') || 'en-US'),
right now: {
day: right now.getDate(),
month: right now.getMonth(),
yr: right now.getFullYear()
}
}, settings
);
We test, if the basis factor (<html>
) incorporates a lang
-attribute with locale information; in any other case, we’ll fallback to utilizing en-US
. This is step one towards internationalizing the calendar.
We additionally want to find out which month to initially show when the calendar is rendered. That’s why we prolonged the config
object with the first date
. This manner, if no date is supplied within the settings
object, we’ll use the right now
reference as an alternative:
const date = config.date ? new Date(config.date) : right now;
We’d like slightly extra information to correctly format the calendar primarily based on locale. For instance, we’d not know whether or not the primary day of the week is Sunday or Monday, relying on the locale. If we now have the information, nice! But when not, we’ll replace it utilizing the Intl.Locale
API. The API has a weekInfo
object that returns a firstDay
property that offers us precisely what we’re in search of with none problem. We are able to additionally get which days of the week are assigned to the weekend
:
if (!config.information) config.information = new Intl.Locale(config.locale).weekInfo || {
firstDay: 7,
weekend: [6, 7]
};
Once more, we create fallbacks. The “first day” of the week for en-US
is Sunday, so it defaults to a worth of 7
. This can be a little complicated, because the getDay
method in JavaScript returns the times as [0-6]
, the place 0
is Sunday… don’t ask me why. The weekends are Saturday and Sunday, therefore [6, 7]
.
Earlier than we had the Intl.Locale
API and its weekInfo
methodology, it was fairly onerous to create a global calendar with out many **objects and arrays with details about every locale or area. These days, it’s easy-peasy. If we cross in en-GB
, the strategy returns:
// en-GB
{
firstDay: 1,
weekend: [6, 7],
minimalDays: 4
}
In a rustic like Brunei (ms-BN
), the weekend is Friday and Sunday:
// ms-BN
{
firstDay: 7,
weekend: [5, 7],
minimalDays: 1
}
You may marvel 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. In some areas, it is likely to be simply in the future. For others, it is likely to be a full seven days.
Subsequent, we’ll create a render
methodology inside our kalEl
-method:
const render = (date, locale) => { ... }
We nonetheless want some extra knowledge to work with earlier than we render something:
const month = date.getMonth();
const yr = date.getFullYear();
const numOfDays = new Date(yr, month + 1, 0).getDate();
const renderToday = (yr === config.right now.yr) && (month === config.right now.month);
The final one is a Boolean
that checks whether or not right now
exists within the month we’re about to render.
Semantic markup
We’re going to get deeper in rendering in only a second. However first, I wish to be sure that the main points we arrange have semantic HTML tags related to them. Setting that up proper out of the field offers us accessibility advantages from the beginning.
Calendar wrapper
First, we now have the non-semantic wrapper: <kal-el>
. That’s advantageous as a result of there isn’t a semantic <calendar>
tag or something like that. If we weren’t making a customized factor, <article>
is likely to be essentially the most applicable factor for the reason that calendar may stand by itself web page.
Month names
The <time>
factor goes to be an enormous one for us as a result of it helps translate dates right into a format that screenreaders and serps can parse extra precisely and constantly. For instance, right here’s how we are able to 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 times of the week may be difficult. It’s preferrred if we are able to write out the total names for every day — e.g. Sunday, Monday, Tuesday, and so forth. — however that may take up a variety of area. So, let’s abbreviate the names for now inside an <ol>
the place every day is a <li>
:
<ol>
<li><abbr title="Sunday">Solar</abbr></li>
<li><abbr title="Monday">Mon</abbr></li>
<!-- and so forth. -->
</ol>
We may get difficult with CSS to get the very best of each worlds. For instance, if we modified the markup a bit like this:
<ol>
<li>
<abbr title="S">Sunday</abbr>
</li>
</ol>
…we get the total names by default. We are able to then “disguise” the total identify when area runs out and show the title
attribute as an alternative:
@media all and (max-width: 800px) {
li abbr::after {
content material: attr(title);
}
}
However, we’re not going that method as a result of the Intl.DateTimeFormat
API can assist right here as properly. We’ll get to that within the subsequent part after we cowl rendering.
Day numbers
Every date within the calendar grid will get a quantity. Every quantity is a listing merchandise (<li>
) in an ordered record (<ol>
), and the inline <time>
tag wraps the precise quantity.
<li>
<time datetime="2023-01-01">1</time>
</li>
And whereas I’m not planning on doing any styling simply but, I do know I’ll need some strategy to type the date numbers. That’s attainable as-is, however I additionally need to have the ability to type weekday numbers otherwise than weekend numbers if I must. So, I’m going to incorporate data-*
attributes particularly for that: data-weekend
and data-today
.
Week numbers
There are 52 weeks in a yr, generally 53. Whereas it’s not tremendous widespread, it may be good to show the quantity for a given week within the calendar for added context. I like having it now, even when I don’t wind up not utilizing it. However we’ll completely use it on this tutorial.
We’ll use a data-weeknumber
attribute as a styling hook and embrace it within the markup for every date that’s 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 web page! We already know that <kal-el>
is the identify of our customized factor. Very first thing we have to configure it’s to set the firstDay
property on it, so the calendar is aware of whether or not Sunday or another day is the primary day of the week.
<kal-el data-firstday="${ config.information.firstDay }">
We’ll be utilizing template literals to render the markup. To format the dates for a global viewers, we’ll use the Intl.DateTimeFormat
API, once more utilizing the locale
we specified earlier.
The month and yr
Once we name the month
, we are able to set whether or not we wish to use the lengthy
identify (e.g. February) or the brief
identify (e.g. Feb.). Let’s use the lengthy
identify because it’s the title above the calendar:
<time datetime="${yr}-${(pad(month))}">
${new Intl.DateTimeFormat(
locale,
{ month:'lengthy'}).format(date)} <i>${yr}</i>
</time>
Weekday names
For weekdays displayed above the grid of dates, we want each the lengthy
(e.g. “Sunday”) and brief
(abbreviated, ie. “Solar”) names. This manner, we are able to use the “brief” identify when the calendar is brief on area:
Intl.DateTimeFormat([locale], { weekday: 'lengthy' })
Intl.DateTimeFormat([locale], { weekday: 'brief' })
Let’s make a small helper methodology that makes it slightly simpler to name every one:
const weekdays = (firstDay, locale) => {
const date = new Date(0);
const arr = [...Array(7).keys()].map(i => {
date.setDate(5 + i)
return {
lengthy: new Intl.DateTimeFormat([locale], { weekday: 'lengthy'}).format(date),
brief: new Intl.DateTimeFormat([locale], { weekday: 'brief'}).format(date)
}
})
for (let i = 0; i < 8 - firstDay; i++) arr.splice(0, 0, arr.pop());
return arr;
}
Right here’s how we invoke that within the template:
<ol>
${weekdays(config.information.firstDay,locale).map(identify => `
<li>
<abbr title="${identify.lengthy}">${identify.brief}</abbr>
</li>`).be part of('')
}
</ol>
Day numbers
And at last, the times, wrapped in an <ol>
factor:
${[...Array(numOfDays).keys()].map(i => {
const cur = new Date(yr, month, i + 1);
let day = cur.getDay(); if (day === 0) day = 7;
const right now = renderToday && (config.right now.day === i + 1) ? ' data-today':'';
return `
<li data-day="${day}"${right now}${i === 0 || day === config.information.firstDay ? ` data-weeknumber="${new Intl.NumberFormat(locale).format(getWeek(cur))}"`:''}${config.information.weekend.consists of(day) ? ` data-weekend`:''}>
<time datetime="${yr}-${(pad(month))}-${pad(i)}" tabindex="0">
${new Intl.NumberFormat(locale).format(i + 1)}
</time>
</li>`
}).be part of('')}
Let’s break that down:
- We create a “dummy” array, primarily based on the “variety of days” variable, which we’ll use to iterate.
- We create a
day
variable for the present day within the iteration. - We repair the discrepancy between the
Intl.Locale
API andgetDay()
. - If the
day
is the same asright now
, we add adata-*
attribute. - Lastly, we return the
<li>
factor as a string with merged knowledge. tabindex="0"
makes the factor focusable, when utilizing keyboard navigation, after any constructive tabindex values (Notice: you need to by no means add constructive tabindex-values)
To “pad” the numbers within the datetime
attribute, we use slightly helper methodology:
const pad = (val) => (val + 1).toString().padStart(2, '0');
Week quantity
Once more, the “week quantity” is the place per week falls in a 52-week calendar. We use slightly helper methodology for that as properly:
perform 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.spherical(((date.getTime() - week.getTime()) / 86400000 - 3 + (week.getDay() + 6) % 7) / 7);
}
I didn’t write this getWeek
-method. It’s a cleaned up model of this script.
And that’s it! Because of the Intl.Locale
, Intl.DateTimeFormat
and Intl.NumberFormat
APIs, we are able to now merely change the lang
-attribute of the <html>
factor to vary the context of the calendar primarily based on the present area:

de-DE

fa-IR

zh-Hans-CN-u-nu-hanidec
Styling the calendar
You may recall how all the times are only one <ol>
with record gadgets. To type these right into a readable calendar, we dive into the great world of CSS Grid. In truth, we are able to repurpose the identical grid from a starter calendar template right here on CSS-Tricks, however up to date a smidge with the :is()
relational pseudo to optimize the code.
Discover that I’m defining configurable CSS variables alongside the best way (and prefixing them with ---kalel-
to keep away from conflicts).
kal-el :is(ol, ul) {
show: 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;
place: relative;
}

Let’s draw borders across the date numbers to assist 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, stable);
border-width: var(--kalel-li-bdw, 0 0 1px 0);
grid-column: var(--kalel-li-gc, preliminary);
text-align: var(--kalel-li-tal, finish);
}
The seven-column grid works advantageous when the primary day of the month is additionally the primary day of the week for the chosen locale). However that’s the exception moderately than the rule. Most occasions, we’ll must shift the primary day of the month to a special weekday.

Bear in mind all the additional data-*
attributes we outlined when writing our markup? We are able to hook into these to replace which grid column (--kalel-li-gc
) the primary date variety of the month is positioned on:
[data-firstday="1"] [data-day="3"]:first-child {
--kalel-li-gc: 1 / 4;
}
On this case, we’re spanning from the primary grid column to the fourth grid column — which is able to routinely “push” the subsequent merchandise (Day 2) to the fifth grid column, and so forth.
Let’s add slightly type to the “present” date, so it stands out. These are simply my kinds. You’ll be able to completely do what you’d like right 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 thought of styling the date numbers for weekends otherwise than weekdays. I’m going to make use of a reddish colour to type these. Notice that we are able to attain for the :not()
pseudo-class to pick out them whereas leaving the present date alone:
[data-weekend]:not([data-today]) {
--kalel-day-c: var(--kalel-weekend-c, hsl(0, 86%, 46%));
}
Oh, and let’s not overlook the week numbers that go earlier than the primary date variety of every week. We used a data-weeknumber
attribute within the markup for that, however the numbers gained’t truly show except we reveal them with CSS, which we are able to do on the ::earlier than
pseudo-element:
[data-weeknumber]::earlier than {
show: var(--kalel-weeknumber-d, inline-block);
content material: attr(data-weeknumber);
place: absolute;
inset-inline-start: 0;
/* further kinds */
}
We’re technically finished at this level! We are able to render a calendar grid that exhibits the dates for the present month, full with issues for localizing the information by locale, and guaranteeing that the calendar makes use of correct semantics. And all we used was vanilla JavaScript and CSS!
However let’s take this another step…
Rendering a whole yr
Perhaps it’s essential to show a full yr of dates! So, moderately than render the present month, you may wish to show all the month grids for the present yr.
Properly, the good factor concerning the method we’re utilizing is that we are able to name the render
methodology as many occasions as we would like and merely change the integer that identifies the month on every occasion. Let’s name it 12 occasions primarily based on the present yr.
so simple as calling the render
-method 12 occasions, and simply change the integer for month
— i
:
[...Array(12).keys()].map(i =>
render(
new Date(date.getFullYear(),
i,
date.getDate()),
config.locale,
date.getMonth()
)
).be part of('')
It’s most likely a good suggestion to create a brand new mother or father wrapper for the rendered yr. Every calendar grid is a <kal-el>
factor. Let’s name the brand new mother or father wrapper <jor-el>
, the place Jor-El is the name of Kal-El’s father.
<jor-el id="app" data-year="true">
<kal-el data-firstday="7">
<!-- and so forth. -->
</kal-el>
<!-- different months -->
</jor-el>
We are able to use <jor-el>
to create a grid for our grids. So meta!
jor-el {
background: var(--jorel-bg, none);
show: var(--jorel-d, grid);
hole: var(--jorel-gap, 2.5rem);
grid-template-columns: var(--jorel-gtc, repeat(auto-fill, minmax(320px, 1fr)));
padding: var(--jorel-p, 0);
}
Ultimate demo
Bonus: Confetti Calendar
I learn a superb guide referred to as Making and Breaking the Grid the opposite day and chanced on this stunning “New Yr’s poster”:

I figured we may do one thing comparable with out altering something within the HTML or JavaScript. I’ve taken the freedom to incorporate full names for months, and numbers as an alternative of day names, to make it extra readable. Get pleasure from!
[ad_2]