Skip to main content
Aonghus Storey

Creating a Calendar with the HTML <details> Element

All blog posts

I was recently setting up a calendar on the Irish Left Archive website and in the absence of a handy Javascript content-switcher or similar in the existing codebase, I made a simple one using the <details> element. I hadn't experimented a great deal with <details> before, but it's another useful tool for removing some of that old Jquery we all still have floating around.

It's always an interesting exercise to see how far a dynamic UI element can get using CSS alone, and the calendar has worked quite well, with reliable support in modern browsers. You can see the live version on the Irish Left Archive website, but here's a simplified demo to get an idea:

Demo of CSS- and HTML-only calendar UI element.

Code

The HTML consists of twelve <details> elements, containing an <ol> list of days of each month and the month name in the <summary>. The default state of details can be styled to a grid of squares for each month, and details[open] to display the calendar for that month.

<div class="calendar">
	<details>
		<summary><p>January</p></summary>
		<ol>
			<li>01</li>
		</ol>
	</details>
</div>

Using this structure, the initial, unopened view can be styled into a grid with something like this:

.calendar {
	display: flex;
	min-height: 35em;
	align-content: start;
	flex-wrap: wrap;
	position: relative;
	overflow: hidden;
}

.calendar details {
	display: block;
	width: 25%;
	padding: 0.5em;
}

.calendar summary {
	display: block;
	padding-top: 2em;
	font-size: 1.2em;
	font-weight: 700;
	cursor: pointer;
	height: 6em;
	text-align: center;
	border: 1px solid #ccc;
	border-radius: 3px;
}

.calendar summary:hover {
	background-color: #eee;
}

.calendar summary::-webkit-details-marker {
	display: none;
}

For the sake of the example, I've assumed box-sizing: border-box is set globally for simplicity, and set four items to a row. Depending on the responsive breakpoints it's being integrated with, this can of course be set higher – for example, the live version sets .calendar details to 16.6% for rows of six on full width devices.

By default, <summary> is displayed as list-inline with an arrow marker. Changing display removes that in most cases, but according to MDN, Safari needs to be reminded with the custom marker selector… It also doesn't default to using a link-style cursor, so this is added explicitly, along with a hover colour as a further visual cue.

The reason for position: relative and the large min-height on the calendar is to allow using position: absolute to overlay the open state without it covering any succeeding content. This means align-content: start is also needed to stop the rows being widely spaced.

Perhaps a neater solution would be to apply the forced height only to .calendar:has(details[open]), and then applying the above in a @support query, for example:

.calendar:has(details[open]) {
	min-height: 35em;
}

@supports not selector(:has(details[open])) {
	.calendar {
		min-height: 35em;
		align-content: start;
	}
}

Unfortunately :has() isn't supported in Firefox yet. With better support, it could also be used for example to make the open calendar a modal using body:has(.calendar details[open]), but for now the open view uses absolute positioning in the container, with a forced container height.

.calendar details[open] {
	position: absolute;
	z-index: 100;
	width: 100%;
	left: 0;
	top: 0;
	bottom: 0;
}

.calendar details[open] summary {
	display: flex;
	align-items: center;
	height: 3em;
	margin: 0;
	padding-top: 0;
	text-align: left;
	border: none;
}

.calendar details[open] summary:hover {
	background-color: inherit;
}

.calendar details[open] summary p {
	width: 100%;
}

.calendar details[open] summary::after {
	display: block;
	content: "Close ✕";
	white-space: nowrap;
}

Overlaying the clickable months with the open month helps to avoid the fact that there's nothing preventing multiple <details> from being open at the same time, by hiding the other months until the open one is closed again.

The <summary> element is restyled to act as a header for the month view. (The flex and alignment are there to push the "Close" button to the right. That's also why there's a <p> in there, though I'm sure there's a tidier solution.)

Finally, the content of the calendar itself is set out as a grid of seven-item rows with the following:

.calendar ol {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	flex: 1;
	border: 1px solid #ccc;
	border-width: 1px 0 0 1px;
}

.calendar ol li {
	flex: 1 0 14.2857%;
	max-width: 14.2857%;
	border: 1px solid #ccc;
	border-width: 0 1px 1px 0;
	height: 6em;
	padding: 0.5em;
}

(Note the top and left borders are set on the <ol>, and the right and bottom on the <li> elements to avoid doubling.)

To make it a little less boring, I added an SVG background to the closed summary box (it could just use details:not([open]), but we already have selectors targeting both states to slot the extra rules in to):

.calendar details summary {
	background-image: url("data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDE5MCAxOTAiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPgogIDxsaW5lIHgxPSI1IiB4Mj0iMTg1IiB5MT0iNjUiIHkyPSI2NSIgc3Ryb2tlPSIjZWVlIiAvPgogIDxsaW5lIHgxPSI1IiB4Mj0iMTg1IiB5MT0iMTI1IiB5Mj0iMTI1IiBzdHJva2U9IiNlZWUiIC8+ICAKICA8bGluZSB4MT0iNjUiIHgyPSI2NSIgeTE9IjUiIHkyPSIxODUiIHN0cm9rZT0iI2VlZSIgLz4KICA8bGluZSB4MT0iMTI1IiB4Mj0iMTI1IiB5MT0iNSIgeTI9IjE4NSIgc3Ryb2tlPSIjZWVlIiAvPgo8L3N2Zz4K");
	background-size: 80% 80%;
	background-repeat: no-repeat;
	background-position: center;
}

.calendar details[open] summary {
	background-image: none;
}

The base64 encoded SVG is just a simple grid of lines to represent the calendar table. Note it uses the preserveAspectRatio attribute, which is useful in this case since the position and scale of the background is more important than the aspect ratio.

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 190 190" preserveAspectRatio="none">
  <line x1="5" x2="185" y1="65" y2="65" stroke="#eee" />
  <line x1="5" x2="185" y1="125" y2="125" stroke="#eee" />  
  <line x1="65" x2="65" y1="5" y2="185" stroke="#eee" />
  <line x1="125" x2="125" y1="5" y2="185" stroke="#eee" />
</svg>

If you're reading this page with dark mode enabled, you'll notice the SVG background clashes. Because the Left Archive site has a fixed theme, I haven't adjusted it for dark mode, and because the image is external, it can't inherit colours. This could be solved by using background-mask instead (though this still needs prefixing for Chrome support). That's the approach this website takes for icons, for example (see e.g. the topics on this post).

Finally, it's good to have a visual indication of the month opening, so I've added a short sliding-in animation. Using a transition is also an option, but the shift from the small <summary> box to the position: absolute open view makes that a bit messy.

.calendar details[open] {
	animation: 0.2s 1 reveal;
}

@keyframes reveal {
	from {
		top: -20em;
	}
	to {
		top: 0;
	}
}

Put all that together and you have the example above. A useful version would include content on the calendar of course – in the live version on the Irish Left Archive, it's used to show the type of material we have for the "On This Day" page for each day of the year, and to link through to that page when there's something to see.

Obviously there are still some improvements that could be made with a little bit of Javascript. One person went for the back button to close the open calendar, for example, so there could be a case for adding to the history (which could also give direct links to each open month state). That said, I'd be careful of the knock-on effects of that – a cumulative history change of open and closed states would be more trouble. Personally, muscle memory keeps making me expect the Esc key to be bound to close the calendar as well.

A bit of height adjustment with Javascript to remove that space below it would be good in the case of the demo above, where it's clearly an issue, but I don't think it's a problem in the live use-case I had, where there isn't content below the calendar. Aside from those relatively small UX issues, it's a useful exercise in how much can be achieved in modern CSS without resorting to Javascript.