Building a notification thingamajig with Eleventy data

This post assumes familiarity with Eleventy and Nunjucks.

I recently built and rolled out a small site that provides information about an upcoming event. The site needed to launch before every single detail could be locked down, so there are a few Check back soon! placeholders sprinkled throughout.

To help visitors spot when new information is available and guide them toward those updates, I built a little floating notification doodad that hangs out in the upper right corner of the page like this:

Page with white bell icon and a red notification badge, all enclosed by a black circle

The real site isn't publicly available, so I built a small demo site you can browse to see the whatsit in action.

Once a visitor browses the page that contains the latest update, the dingus disappears—until the next time a notification rolls out. Here's how I put the pieces together.

Eleventy Data

Behind the scenes, some simple Eleventy data defines notifications as an array of objects with just two properties:

[
    {
        "id": "001-apple",
        "url": "/apple/"
    },
    {
        "id": "002-banana",
        "url": "/banana/"
    },
    {
        "id": "003-cranberry",
        "url": "/cranberry/"
    }
]

This is a small, simple site and I want the notification thingamabob to appear on every page, so I’m able to define front-end logic directly in the page’s Nunjuck’s template without causing any undue clutter or concern that it’s being declared but never used.

As a result, I can use Nunjucks's built-in last filter to grab the most recent object out of the notifications array and then plop it directly into my script as an object literal using the dump filter:

    </footer>
</div>

<script>
    let notification = {
        lastDisplayed: null,
        latestNotification: {{ notifications | last | dump | safe }},

        // ...

When built, the resulting script block would look something like this:

    </footer>
</div>

<script>
    let notification = {

        lastDisplayed: null,
        latestNotification: {"id": "003-cranberry", "url": "/cranberry"},

        // ...

With notification data surfaced on the front-end, I can then build out some basic rules to be run on each page load that determine whether to show the whatchamacallit:

        // ...
    
        init() {
            // If the current URL matches latestNotification.url, the visitor is
            // browsing the page with the update. Write latestNotification.id to
            // localStorage for future reference.
    
            let url = new URL( window.location.href );
            if( url.pathname === this.latestNotification.url )
            {
                localStorage.setItem( "lastDisplayed", this.latestNotification.id );
            }
    
            this.lastDisplayed = localStorage.getItem( "lastDisplayed" );
    
            // If the latestNotification.id doesn't match what we have in
            // localStorage, the user hasn't seen the page with the update yet.
            // Set data-notification="visible" on the body element for use by
            // our CSS.

            if( this.lastDisplayed !== this.latestNotification.id )
            {
                document.querySelector( "body" ).dataset.notification = "visible";
            }
        },
    }
    
    notification.init();
</script>

Finally, I can use a data-notification="visible" selector in my CSS to toggle the visibility on and off:

.notification {
    // ...
    display: none;
}

[data-notification="visible"] .notification {
    display: block;
}

A classic mashup of Eleventy data file and Nunjucks templating, a smattering of JavaScript, and a pinch of CSS are all it takes to implement a basic notification widget.

Accessibility and animation

Leaving the notification thingy to pop into view unceremoniously would have been fine, but it's usually at this point I ask myself "what would Dan* do?" This seemed like a good opportunity to add a little attention-grabbing pizzazz and try my hand at a bit of CSS animation.

I spent an inordinate amount of time tweaking the timing on all three elements that comprise the notification. Most of that journey is an unexciting montage of me flipping back and forth between 0.5s and 0.75s delays, with one notable exception: I made it a point to adopt the no-motion-first principle of animations I learned from Tatiana Mac.

By using an opt-in model for animation, the notification gizmo only animates into view if the user has has no preference for reduced motion and their browser supports the prefers-reduced-motion media query:

/* Animation */
@media (prefers-reduced-motion: no-preference) {
    .notification {
        transition:
            opacity 0.75s 0.25s,
            transform 0.5s 0.5s;
    }

    .notification:after {
        transition: all 0.875s 1.5s;
    }

    .notification-icon {
        transition:
            opacity 1s 0.5s,
            transform 1.25s 0.5s;
    }
}

This ensures that the animation isn't shown to anyone who has set their system's reduced motion preference, or to any visitors whose operating systems or browsers don't provide that option.

*Dan Messing, my friend and coworker who is extremely good at adding delightful animations to interfaces he builds. (Keep an eye out for his handiwork in Playdate OS!)