In Sweden, there’s a long tradition of TV and radio advent calendars: shows, often Christmas-themed, that are exactly 24 episodes long. Each of the 24 days in December leading up to Christmas, a new episode drops. Whether this year’s advent calendar shows are better or worse than last year’s is always a hotly debated topic. But, obviously, the best calendar shows were the ones airing when I grew up.
That’s why I was particularly pleased when I won a mint-condition, unopened advent calendar for the 1994 radio show Hertig Hans Slott at an online auction. Instead of keeping this paper calendar unopened, I decided to thoroughly scan it and make it live forever as an online advent calendar. A couple of hours with a friendly AI, and some fidgeting with CSS clip-path
s, and I ended up with
a result I was pretty happy with:
Here are some notes on how it works and an HTML template that you can use to create a Christmas advent calendar yourself.
An online advent calendar template
To create an online advent calendar using the template at the bottom of this post, you need two things:
- An image of the calendar with all the flaps closed and an image with all the flaps removed. These two images need to be perfectly aligned.
- Approximate CSS
clip-path
s that enclose each flap. These are used to mark which parts of the flaps-removed image should be revealed when clicked on.
There are many online tools that can help with creating clip-path
s — just search for “clip-path generator” and you will find plenty of them. However, none of them are great, and creating these paths took quite some time.
What didn’t take a lot of time was creating the HTML, CSS, and JavaScript code for the calendar app. As it’s a one-page HTML app, small enough for an LLM AI to keep in context, it just took 1-2 hours to prompt it into existence. (Generally, I’ve been having a lot of fun lately prompting my way to small one-page HTML apps.) The particular LLM I used here was OpenAI’s o1-preview, a so-called reasoning model, that worked really well for my use case.
Below is a template you can use to create your own online advent calendar. If you want to see a fully realized example of this, check the source code of my Hertig Hans Slott advent calendar.
Expand for advent calendar HTML template
<!DOCTYPE html>
<!--
Template for creating an interactive advent calendar web app.
Adjust or replace bits marked with !!!.
Note: For this to work really well, the calendar images, with and without flaps,
need to be perfectly aligned.
-->
<head>
<meta charset="UTF-8">
<!-- Replace with your calendar title -->
<title>Your Calendar Title Here</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Replace with a description of your calendar -->
<meta name="description" content="Description of your calendar.">
<style>
body, html {
margin: 0;
padding: 0;
overflow: auto;
height: 100%;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
#calendar-container {
position: relative;
width: 100%;
max-width: 100%;
margin: 0 auto;
}
#calendar-image {
width: 100%;
max-width: 1043.5px; /* !!! Adjust max-width as needed */
height: auto;
display: block;
}
/* Flap styles */
.flap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
transition: opacity 0.3s ease;
/* !!! Replace 'YOUR_BOTTOM_LAYER_IMAGE_HERE' with the filename of your bottom layer image */
background-image: url('YOUR_BOTTOM_LAYER_IMAGE_HERE.jpeg');
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: top left;
z-index: 100;
}
.flap.shown {
opacity: 0;
}
/* "What is this?" link styling */
#about-link {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
text-decoration: none;
border-radius: 5px;
font-size: 14px;
z-index: 500; /* Ensure it's above the flaps */
}
/* Overlay background */
#about-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
display: none;
z-index: 1000;
overflow: auto;
}
/* Content box inside the overlay */
#about-content {
position: relative;
background: white;
margin: 40px auto;
padding: 20px;
width: 80%;
max-height: calc(100% - 80px);
overflow-y: auto;
border-radius: 5px;
}
#close-about {
position: absolute;
top: 10px;
right: 15px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
#close-about:hover {
color: #669933;
}
</style>
</head>
<body>
<div id="calendar-container">
<!-- !!! Replace 'YOUR_TOP_LAYER_IMAGE_HERE' with your top layer image -->
<img id="calendar-image" src="YOUR_TOP_LAYER_IMAGE_HERE.jpeg" alt="Your Calendar Alt Text">
<!-- !!! Flap elements -->
<!-- For each flap, create a div with class 'flap' and a unique ID, e.g., 'flap1', 'flap2', etc. -->
<!-- Adjust the 'clip-path' style to define the shape and position of each flap -->
<!-- To create clip-paths, there are plenty of online tools that can help; just search for
"clip-path generator" and you will find many options. -->
<!-- Example flap: -->
<!--
<div class="flap" id="flap1" style="clip-path: circle(6.4% at 12% 9%);"></div>
-->
<!-- Repeat for each flap needed in your calendar -->
<!-- "What is this?" link -->
<!-- Adjust the text as needed -->
<a href="#" id="about-link">What is this?</a>
</div>
<!-- About Overlay -->
<div id="about-overlay">
<div id="about-content">
<span id="close-about">×</span>
<!-- !!! Replace the content below with your own information about the calendar -->
<h2>Your Calendar Title Here</h2>
<p>Provide information about your calendar here.</p>
<!-- Add more content as needed -->
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const flaps = document.querySelectorAll('.flap');
const calendarImage = document.getElementById('calendar-image');
// !!! Create audio objects for open and close sounds
// Replace 'open.mp3' and 'close.mp3' with your own sound files or remove if not needed
const openSound = new Audio('open.mp3');
const closeSound = new Audio('close.mp3');
function adjustFlapSizes() {
const imageRect = calendarImage.getBoundingClientRect();
const imageWidth = imageRect.width;
const imageHeight = imageRect.height;
flaps.forEach((flap) => {
flap.style.width = imageWidth + 'px';
flap.style.height = imageHeight + 'px';
});
}
// Adjust flap sizes on page load
if (calendarImage.complete) {
adjustFlapSizes();
} else {
calendarImage.addEventListener('load', adjustFlapSizes);
}
// Adjust flap sizes when the window is resized
window.addEventListener('resize', adjustFlapSizes);
flaps.forEach((flap) => {
// !!! Replace 'your_calendar_name' with a unique identifier for your calendar
const flapShownId = "your_calendar_name_" + flap.id + "_shown";
if (localStorage.getItem(flapShownId) === 'false') {
flap.classList.remove('shown');
} else {
flap.classList.add('shown');
}
// Add click event to toggle flap and update local storage
flap.addEventListener('click', function() {
if (flap.classList.contains('shown')) {
flap.classList.remove('shown');
localStorage.setItem(flapShownId, 'false');
if (openSound) openSound.play();
} else {
flap.classList.add('shown');
localStorage.setItem(flapShownId, 'true');
if (closeSound) closeSound.play();
}
});
});
// Code for the "What is this?" link and overlay
const aboutLink = document.getElementById('about-link');
const aboutOverlay = document.getElementById('about-overlay');
const closeAbout = document.getElementById('close-about');
// Show the overlay when the link is clicked
aboutLink.addEventListener('click', function(event) {
event.preventDefault(); // Prevent default link behavior
aboutOverlay.style.display = 'block';
});
// Hide the overlay when the close button is clicked
closeAbout.addEventListener('click', function() {
aboutOverlay.style.display = 'none';
});
// Hide the overlay when clicking outside the content box
aboutOverlay.addEventListener('click', function(event) {
if (event.target == aboutOverlay) {
aboutOverlay.style.display = 'none';
}
});
});
</script>
</body>
</html>