DEV Community

Cover image for Creating the CSS Christmas Calendar
Johnny Fekete
Johnny Fekete

Posted on

Creating the CSS Christmas Calendar

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.

Explanation of the CSS grid structure

<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>
Enter fullscreen mode Exit fullscreen mode

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";
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 is auto., 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.

The calendar on an iPhone

.calendar-grid {
  grid-template-columns: repeat(3, 1fr);

  grid-template-areas:
    "title  title  title"
    "day22  day3   day8"
    "day9   day18  day11"
    ...
}
Enter fullscreen mode Exit fullscreen mode

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:

The title

@import url("https://fonts.googleapis.com/css2?family=Mountains+of+Christmas:wght@700&display=swap");
Enter fullscreen mode Exit fullscreen mode

I broke up the whole title into 3 spans, 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>
Enter fullscreen mode Exit fullscreen mode
.title {
  grid-area: title;
  display: flex;
  align-items: center;
  justify-content: center;
}

.title-1 {
  color: #9c163f;
  display: block;
  transform: rotate(-10deg);
}
Enter fullscreen mode Exit fullscreen mode

As with the layout, I also used a slightly different style for the title for mobile devices: smaller fonts, and the 3 spans 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.

Opening/closing the calendar doors

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>
Enter fullscreen mode Exit fullscreen mode

To hide the checkbox, simply add:

.calendar-grid input {
  display: none;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

(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);
}
Enter fullscreen mode Exit fullscreen mode

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:

Transform origin with opening door

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>
Enter fullscreen mode Exit fullscreen mode
.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);
}
Enter fullscreen mode Exit fullscreen mode

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:

The z-index ordering

.calendar-grid .day-1 {
  z-index: 16;
}
.calendar-grid .day-2 {
  z-index: 9;
}
...
.calendar-grid .day-24 {
  z-index: 14;
}
Enter fullscreen mode Exit fullscreen mode

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.

Title of a CSS polar bear

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;

  ...
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (2)

Collapse
khangnd profile image
Khang

Wow, this looks stunning. Amazing John :)

Collapse
johnnyfekete profile image
Johnny Fekete Author

Thanks a lot 😊