Creating the CSS Christmas Calendar items was a lot of fun, but before I could've even started on them, I actually had to build the calendar itself - only using HTML and CSS.
It was a lot easier than I expected, with CSS techniques such as CSS grid, flexbox or 3D transforms.
The Layout
The layout can be achieved with CSS grid, which allows us to position the calendar doors to their desired position.
<div class="calendar-grid">
<div class="title">...</div>
<div class="day day-1">...</div>
<div class="day day-2">...</div>
...
<div class="day day-24">...</div>
</div>
This simple HTML can be turned into the given layout by just these few CSS rules:
.calendar-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: auto;
grid-gap: 1rem;
grid-template-areas:
"title title title day5 day17 day15"
"title title title day11 day20 day16"
"title title title day1 day18 day12"
"day6 day22 day14 day24 day24 day4"
"day10 day21 day2 day24 day24 day8"
"day3 day9 day7 day13 day23 day19";
}
and then assigning each calendar door DIV
to the corresponding template area:
.title {
grid-area: title;
}
.day-1 {
grid-area: day1;
}
...
.day-24 {
grid-area: day24;
}
Once the calendar-grid's display
property is set to grid
, I could define the grid layout:
- the
grid-template-columns
create 6 columns with equal width (1 fraction) and the number of rows isauto
., so there can be as many as necessary. - the
grid-gap
defines the space between the calendar doors (and title). - the
grid-template-areas
shows, where each item should fit inside the grid layout. (these names must match the ones assigned to the individual items, as you could see).
Notice, that the .title
element takes up a 3x3 square. That is not a problem with CSS grid, it allows grid areas that take up more than one template areas.
What About Mobile?
There's no way 6 columns could comfortably fit on mobile devices, but they don't need to.
.calendar-grid {
grid-template-columns: repeat(3, 1fr);
grid-template-areas:
"title title title"
"day22 day3 day8"
"day9 day18 day11"
...
}
With a media-query
I targeted smaller resolution devices and changed the grid template column count, and the area allocation.
This time the title takes up the top 3 areas.
The Title
I wanted to create the title in the Christmassy spirit, so I used a Google font Mountains of Christmas:
@import url("https://fonts.googleapis.com/css2?family=Mountains+of+Christmas:wght@700&display=swap");
I broke up the whole title into 3 span
s, applied different colors on them and rotated them individually:
<div class="title">
<h1>
<span class="title-1">CSS</span>
<span class="title-2">Christmas</span>
<span class="title-3">Calendar</span>
</h1>
</div>
.title {
grid-area: title;
display: flex;
align-items: center;
justify-content: center;
}
.title-1 {
color: #9c163f;
display: block;
transform: rotate(-10deg);
}
As with the layout, I also used a slightly different style for the title for mobile devices: smaller fonts, and the 3 span
s with inline-block
display, so they fit next to each other.
Creating the Doors
The doors can open, close, and show the day's surprise CSS art and its title.
To detect the opened/closed state without using JavaScript, I used a well-known trick: added an invisible checkbox, and a related label (that contains the CSS art). Clicking on the label will trigger the checkbox, and in CSS I can target it with the input:checked
selector.
<div class="day day-1">
<label>
<input type="checkbox"/>
<div class="door">
<div class="front">1</div>
<div class="back"></div>
</div>
<div class="inside">
... CSS art of the day ...
</div>
<div class="title-container">
... title of the CSS art ...
</div>
</label>
</div>
To hide the checkbox, simply add:
.calendar-grid input {
display: none;
}
The label
has multiple functions:
- gives a height to the door/item
- changes the mouse cursor to pointer
- creates a perspective to the door (so there's a 3D effect when it's opening or closing)
.calendar-grid label {
perspective: 1000px;
transform-style: preserve-3d;
cursor: pointer;
display: flex;
min-height: 100%;
width: 100%;
height: 136px;
position: relative;
}
(there are some small changes in the height for mobile layout, where the height is calculated from the viewport's width: height: calc(85vw / 3);
)
The .door
div has the animation configured:
.calendar-grid .door {
transform-style: preserve-3d;
transition: all 300ms;
transform-origin: 0% 50%;
... further styles
}
.calendar-grid input:checked + .door {
transform: rotateY(-180deg);
}
First I set the transform-style, so the door's child elements will also preserve the 3D effect.
I set that any change should be animated in 0.3 seconds.
Finally, defined that transform animation should start from the middle of the left side. This is needed for the flip effect, so the door opens in the 3D space:
When the input is checked, I transform the rotation of the door, and the combination of this and the previously discussed styles end up with a nice opening/closing animation.
However, the door has two sides: the outside with the number, and a gray back.
To achieve this, I used the backface-visibility: hidden
style. This means that if an element is rotated in the 3D space, its back is not showing up:
<div class="door">
<div class="front">1</div>
<div class="back"></div>
</div>
.calendar-grid .door div {
position: absolute;
height: 100%;
width: 100%;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
... further styles ...
}
.calendar-grid .door .back {
background: linear-gradient(to right, #384044, #2e454f);
transform: rotateY(-180deg);
}
By applying transform: rotateY(-180deg)
on the .back
div, I could flip it (the same way how the opening animation flips, but this time staying in the same place, to show up as the back of the door).
Ordering the Doors
I realized that if I don't set the order of the doors, they can cover each other in a weird way, or the title of an open door can get covered by another door that is in the next row.
To avoid these glitches, I manually set the z-index
on every door, starting from the bottom left corner:
.calendar-grid .day-1 {
z-index: 16;
}
.calendar-grid .day-2 {
z-index: 9;
}
...
.calendar-grid .day-24 {
z-index: 14;
}
It's important to mention, that the z-index
ordering differs in the mobile layout, as the doors are in different locations. Therefore they need to be actualized by targeting the days with media queries.
The Titles
Each CSS art has a title, that's a link for the individual piece's CodePen.
To make it easily noticeable that it's a link, it plays an animation to the striped background, once the mouse is over it.
The titles are inside the label, next to the door:
<div class="day day-6">
<label>
<input type="checkbox"/>
<div class="door"> ... </div>
<div class="inside"> ... </div>
<div class="title-container">
<a href="https://codepen.io/johnnyfekete/pen/qBaRZXV" target="_blank" title="Link to source code">
Polar bear
</a>
</div>
</label>
</div>
To trigger the entering/leaving animations, I added these styles:
.calendar-grid .title-container {
opacity: 0;
transform: translateY(-1rem);
pointer-events: none;
transition: all 400ms ease-in-out;
... other styles for positioning and layout ...
}
.calendar-grid input:checked ~ .title-container {
opacity: 1;
transform: translateY(0);
pointer-events: all;
}
With these styles, the title is initially hidden (opacity: 0
) and moved up (transform: translateY(-1rem)
). It doesn't detect any mouse events.
Once the checkbox is checked, it animates the opacity and the position in 0.4 seconds.
Striped Background of the Titles
The red-white striped border is not really a border, but the background of the title, positioned behind the white background.
.calendar-grid .title-container a {
/* this is the actual title with white background */
position: relative;
border-radius: 0.25rem;
background-color: #1d3557;
... additional styles for the text ...
}
.calendar-grid .title-container a::before {
/* pseudo class behind the title,
with a negative offset in each direction */
content: "";
display: block;
position: absolute;
top: -0.5rem;
right: -0.5rem;
bottom: -0.5rem;
left: -0.5rem;
z-index: -1;
border-radius: 0.75rem;
/* the striped background uses repeating linear gradient */
background: repeating-linear-gradient(
-45deg,
#f1faee 0,
#f1faee 0.5rem,
#e63946 0.5rem,
#e63946 1rem
);
background-size: 1.44rem 1.44rem;
...
}
Finally, for the animation on hover
I defined a keyframe-animation
to move the background's position:
@keyframes calendar-item-link {
0% {
background-position: 0 0;
}
100% {
background-position: 1.44rem 0;
}
}
This moves the background to the left with 1.44rem (exactly the same width as the width was for the stripe pattern (fun fact, I used the Pythagorean theorem to calculate this number, as the stripes are each 0.5rem wide (1rem the red + white in total).
I applied this keyframe-animation
on the striped background pseudo-element:
.calendar-grid .title-container a::before {
... rest of the styles ...
animation: calendar-item-link 0.6s infinite linear;
animation-play-state: paused;
}
so it's repeating forever, without any easing (and each cycle takes 0.6 seconds).
I also paused the animation by default, and added a hover state that plays it once the mouse is over:
.calendar-grid .title-container a:hover::before {
animation-play-state: running;
}
And there you have it ✨
Creating the calendar was quite challenging, but I also had a lot of fun with it!
I learned the lesson that of course JavaScript is useful in many situations, but it is always easy for a developer to overuse it.
This project demonstrated how much can be achieved by using CSS and HTML only.
The full project is available on Github at https://github.com/johnnyfekete/CSSChristmasCalendar/.
The code in this article focused on the main features, and in the real calendar, I used more verbose CSS with vendor prefixes where necessary.
Go ahead and give it a try!
Top comments (2)
Wow, this looks stunning. Amazing John :)
Thanks a lot 😊