Alert Email External Link Info Letterboxd 0.5/5 stars 1/5 stars 1.5/5 stars 2/5 stars 2.5/5 stars 3/5 stars 3.5/5 stars 4/5 stars 4.5/5 stars 5/5 stars RSS Source Topic Twitter
I’m redesigning this site in public! Follow the process step by step at

Robtober 2021 Design Notes

The making of a custom-designed blog post

Robtober is what I call the horror movie binge I do every October. After I redesigned my site in 2017, I started documenting the event each year with a horrifically custom-designed blog post, getting a little more elaborate each time. This post goes behind the scenes of the 2021 edition.

The data

I generate my site with Jekyll, and a custom-designed post like Robtober gets its own unique layout file. To keep things tidy, all the movie data is formatted in YAML and stored in the post’s front matter:

  - date: 2021-10-06
      - title: 'Phantom of the Paradise'
        director: 'Brian DePalma'
        year: '1974'
        country: 'USA'
        description: 'A disfigured composer sells his soul for the woman he loves so that she will perform his music. However, an evil record tycoon betrays him and steals his music to open his rock palace, The Paradise.''
        poster: true
        color: '#FF90BD'
        trailer: ''
        watch: ''
        tickets: ''

The layout file then uses some fairly straightforward Liquid template code to access that data and use it to build out the movie list on the page:

{% for day in page.robtober %}
{% for film in day.films %}
<div class="film-wrap">
  <div class="film" style="--key-color: {% if film.color %}{{ film.color }}{% else %}hsl(0,0%,75%){% endif %}">
    <div class="film__art-wrap">
      <div class="film__art" data-title="{% if film.title_short %}{{ film.title_short }}{% else %}{{ film.title }}{% endif %}">
        <img width="150" height="272" src="/assets/images/{{ | date: '%Y-%m-%d' }}-{{ film.title | slugify }}.jpg" alt="{{ film.title }} poster" loading="lazy">
    <div class="film__description">
      <h2 class="film__date"><time datetime="{{ | date: '%Y-%m-%e' }}">Robtober {{ | date: '%e' }}, {{ | date: '%Y' }}</time></h2>
      <h3 class="film__title">{{ film.title }}</h3>
      <p class="film__meta">{{ film.director }}, {{ }}, {{ film.year }}</p>
      <p class="film__synopsis">{{ film.description }}</p>
      <ul class="film__links">
        {% if film.trailer %}<li><a class="film__link" href="{{ film.trailer }}">Trailer</a></li>{% endif %}
        {% if %}<li><a class="film__link" href="{{ }}">Tickets</a></li>{% endif %}
        {% if %}<li><a class="film__link" href="{{ }}">Where to Watch</a></li>{% endif %}
{% endfor %}
{% endfor %}

The opener

I’m currently working on a small branding project with a local client, and one of the logo concepts I came up with evokes the bitmap text that VCRs display on TV screens. When that concept was scrapped, I decided to repurpose it for Robtober, complete with animated scan lines. The blue video screen with VHS artifacts in the background is a looping video, but since I wanted the text itself—ROBTOBER 2021, SLP, and 00:00:00—to appear in three corners of the screen and work with any viewport aspect ratio, each of the three pieces of text had to be a separate element. I drew them in SVG, and each row of pixels is a distinct path element which can be animated individually for the scan line effect. A little Sass keyframe function I cooked up awhile back helped me fine-tune the timing. The SVG elements themselves have an additional subtle jitter animation applied, and blur() and drop-shadow() filters were added to complete the low-res analog look. For a clearer visual sense of what’s happening, here’s the animation in slow motion:

This scan line animation, shown here 10 times slower than its actual speed, consists of SVG path elements translateX’d back and forth via CSS keyframes. No JavaScript necessary!

I’m still behind the times on ES6 and the Intersection Observer API, so the opener fades out on scroll via a little JavaScript snippet from David Walsh, which I’ve previously used for the gallery on Plus Equals. It’s the only bit of code in this design that I don’t fully understand, and it’s not entirely appropriate for this use case, but it gets the job done well enough. I learned shortly after launch that it wasn’t obvious to everyone to scroll down, so I added a blinking down-arrow prompt.

The CSS that displays the video, animation, and scroll fade are all activated by a prefers-reduced-motion: no-preference media query, so users who would rather not have incidental movement on the page won’t see them.

The typography

The display face is Fruktur, by Viktoriya Grabowska and Eben Sorkin. The gothic feel at the heart of its blackletter type is the only part of this year’s design that overtly evokes horror, which is undercut just the right amount by its playful curves. The typeface used for text, metadata, and buttons is the eminently functional Poppins, by Jonny Pinhorn, reused from Robtober 2020.

For sizing, I’ve wanted to try out the fluid, responsive technique introduced by James Gilyead and Trys Mudford in their Utopia project ever since it was introduced in early 2020, and now that I finally have, I absolutely love it. My version is slightly different from theirs, in that it uses clamp() but keeps the font-size interpolation calculations in the CSS:

--w-min: 300;
--w-current: 100vw;
--w-max: 2400;
--scale0-min: 16;
--scale0-max: 24;
--scale0: clamp(
    (var(--scale0-min) / 16) * 1rem),
      (var(--scale0-min) * 1px) + (
        ((var(--w-current) - (var(--w-min) * 1px))) * (
          ((var(--scale0-max) - var(--scale0-min))) / ((var(--w-max) - var(--w-min)))
    calc((var(--scale0-max) / 16) * 1rem)

Incorporating a heavily modified version of my Sass typographic scale generator made it easy for me to generate a full typographic scale and tweak the settings until they were just right:

--w-min: 300;
--w-current: 100vw;
--w-max: 2400;

$scale: (
  -1: (min:  14, max:  20),
   0: (min:  16, max:  24),
   1: (min:  42, max:  96)

@each $step, $size in $scale {
  --scale#{$step}-min: #{map-get($size, min)};
  --scale#{$step}-max: #{map-get($size, max)};
  --scale#{$step}: clamp(
    calc((var(--scale#{$step}-min) / 16) * 1rem),
      (var(--scale#{$step}-min) * 1px) + (
        ((var(--w-current) - (var(--w-min) * 1px))) * (
          ((var(--scale#{$step}-max) - var(--scale#{$step}-min))) / ((var(--w-max) - var(--w-min)))
    calc((var(--scale#{$step}-max) / 16) * 1rem)

The VHS boxes

To accompany the opener’s VHS homage, it was only natural to represent each movie as a VHS box (even though many of them have never been released on VHS). The key art was all sourced from TMDb and I had to do a little Photoshop magic in some cases to make it fit the tall, skinny format.

Fake VHS box courtesy of CSS 3D transforms

The spines are ::after pseudo-elements, and their background colors are individually customized, manually eyedropper’d from the key art and dropped into the markup as a CSS custom property in an inline style like this: style="--key-color: #FF90BD". To make them pop off the TV static on the page background, I used Work With Color’s HSL Color Picker to make sure each color had at least 75% luminance. (Note that that’s the tool’s “Lum” value, different from the “L” lightness value. There are less clunky tools available for this than Work With Color’s, but I just happened to have that link handy when I was working on this design.) That luminance level put most of the spines in the pastel realm, which was a happy accident that gave them a 1980s feel to go with the VHS theme.

Making these VHS boxes was also a great opportunity to finally get my head around how CSS 3D transforms work, and after getting frustrated by documentation and trying in vain to reverse-engineer the output of some 3D CSS generators, I found David DeSandro’s outstanding Intro to CSS 3D transforms, which was an absolute godsend. My boxes only include the front and one side, and I arranged the perspective to hide the other missing surfaces.

The scroll snapping

This is my first time using CSS Scroll Snap. It’s probably more often used for horizontally scrolling carousels, but I used it vertically: scroll-snap-type: y mandatory. I’m kind of playing with fire using the mandatory parameter with variable height content that could potentially get cut off, but when I tried using proximity instead, I wasn’t satisfied with how the browser behaved. So some people looking at the page on tiny phones held sideways might have some content cut off. Sorry, folks! I also had to use an annoying workaround to get it to play nice with iOS Safari’s flawed interpretation of 100vh. All told, though, I think the effect works quite nicely! Like the videos and animations, I put this one behind a prefers-reduced-motion: no-preference media query.

This year my headshot in the footer is dressed up for Halloween in the “Pretty Lady” mask Leatherface wears at the end of The Texas Chain Saw Massacre. In the age of touchscreens, :hover is something of a lost art, but I’m still having fun with it!