TUMBLR KEYBOARD NAVIGATION

J/K

Written by ritabuuk and dubiousdisc
Posted on June 1st 2023
Tumblr Keyboard Navigation cheat sheet

We have made a Javascript script that implements keyboard navigation on custom Tumblr themes. Just like on the dashboard, it lets you navigate from post to post using the J and K keys, return to the top of the page using the . key, and navigate from page to page using the ← left and → right arrow keys. You can feel free to take this code to add keyboard navigation to your own Tumblr theme.

See a working example on Rosy's artblog.

Prerequisites: General knowledge of HTML and Tumblr theme making.

Step 1: Theme preparation

Before everything, you have to add the hooks for your theme to be able to work with the Javascript.

  1. You will need a main content wrapper for your entire theme. This can be something like a <div> wrapping your entire theme.
  2. Add the class keynav-mainelement to your main content wrapper.
  3. Add the class keynav-post to each of your posts (in Tumblr theme language, {block:Posts}).
  4. Add id="{PostID}" to each of your posts.

At the most bare-bones level, a working structure would look something like:

<div class="keynav-mainelement">
    {block:Posts}
        <div class="keynav-post" id="{PostID}">
            [all the stuff that goes into posts…]
        </div>
    {/block:Posts}
</div>

Step 2: Add the Javascript

Paste this code at the bottom of your theme code, before </body>:

<script type="text/javascript">
/*
 * This implements Tumblr-style keyboard navigation
 * - the j key moves to the older post further down the page
 *   (or the bottom of the page, if already at the bottom post).
 * - the k key moves to the newer post back up the page
 *   (or the top of the page, if already at the top post).
 * - the period (.) key moves to the top of the page.
 * - the right arrow key moves to the next page of older posts.
 * - the left arrow key moves to the previous page of newer posts.
 * Feel free to take and reuse and edit for your purposes.
 */

const mainElement = document.querySelector('.keynav-mainelement');
const posts = mainElement.getElementsByClassName('keynav-post');

function findHowFarScrolledDown()
{
    // How far down the page (in pixels) has the visitor scrolled?
    // This refers to the top of the viewport.

    // Reference: https://awik.io/find-far-user-scrolled-javascript/
    return (window.pageYOffset !== undefined) ? window.pageYOffset :
        (   document.documentElement
         || document.body.parentNode
         || document.body
        ).scrollTop;
}

function findHowFarScrolledDownMiddleOfScreen()
{
    // How far down the page (in pixels) has the visitor scrolled?
    // This refers to the center of the viewport.

    return findHowFarScrolledDown()
        + (document.documentElement.clientHeight / 2.0 );
}

function getTopOfPost( post )
{
    // How far down the page (in pixels) is the top of the post?
    return post.offsetTop;
}

function getBottomOfPost( post )
{
    // How far down the page (in pixels) is the bottom of the post?
    return post.offsetTop + post.offsetHeight;
}

function whichElementIsOnScreen()
{
    // Which post is closest to the middle of the visitor's screen?

    // How far down has the visitor scrolled?
    var scrolledFromTop = findHowFarScrolledDownMiddleOfScreen();
    
    // Check each post (starting from the top)
    // until we find the one that the visitor has
    // in the middle of their screen.
    
    for( i = 0; i < posts.length; i++ )
    {
        var post = posts[i];
        var topOfPost = getTopOfPost( post );
        var bottomOfPost = getBottomOfPost( post );
        
        if( topOfPost <= scrolledFromTop
            && bottomOfPost >= scrolledFromTop )
        {
            // The visitor has scrolled to somewhere
            // in the middle of a long post
            return i;
        }
        else if( scrolledFromTop <= topOfPost )
        {
            // The visitor's scrolling is above this post.
            return i;
        }
    }
    // If we're here, the visitor doesn't have a post
    // at the middle of the screen.  Return index of the last post.
    return posts.length - 1;
}

function getIDofPost( cursorIndex )
{
    var post = posts[cursorIndex];
    return post.id;
}

function moveCursorPrev( postCursor )
{
    // Ensures it will not return a number less than zero.
    return Math.max( 0, postCursor-1 );
}

function moveCursorNext( postCursor )
{
    // Ensures it will not return a number greater than the last index.
    return Math.min( posts.length-1, postCursor+1 );
}

function isPostTallerThanViewport( post )
{
    var postHeight = post.offsetHeight;
    var viewportHeight = document.documentElement.clientHeight;
    
    if( postHeight > viewportHeight )
    {
        return true;
    }
    return false;
}

function scrollPostIntoViewNicely( targetID )
{
    var post = document.getElementById( targetID );
    
    if( isPostTallerThanViewport( post ) )
    {
        /* Scroll tall posts so that the top of the post
         * is at the top of the screen.
         * Using 'center' on a tall post would make us
         * scroll past the the beginning of the post.
         */
    
        post.scrollIntoView({
            block: 'start',
            behavior: 'smooth'
        });
    }
    else
    {
        /* Scrolling to 'center' looks really nice with
         * shorter posts; they get centered on the screen.
         */
    
        post.scrollIntoView({
            block: 'center',
            behavior: 'smooth'
        });
    }
    return;
}

function scrollToTopOfPage()
{
    window.scrollTo({
        top: 0,
        behavior: 'smooth'
    });
    return;
}

function scrollToBottomOfPage()
{
    window.scrollTo({
        top: document.body.scrollHeight,
        behavior: 'smooth'
    });
    return;
}

function jknavigation( key )
{
    /* Based on which post we are looking at,
     * moves us to the next/previous point
     * (top/bottom of page or next/previous post)
     */

    // The postCursor starts on the post that is on screen.
    var postCursor = whichElementIsOnScreen();
    
    if( postCursor == 0 && key == 'k' )
    {
        // Nowhere else to go before the first post
        // Scroll to the top of the page instead
        scrollToTopOfPage();
    }
    else if( postCursor == posts.length-1 && key == 'j' )
    {
        // Nowhere else to go after the last post
        // Scroll to the bottom of the page instead
        scrollToBottomOfPage();
    }
    else
    {
        if( key == 'j' )
        {
            postCursor = moveCursorNext( postCursor );
        }
        else if( key == 'k')
        {
            postCursor = moveCursorPrev( postCursor );
        }
    
        var targetID = getIDofPost( postCursor );
        scrollPostIntoViewNicely( targetID );
    }
    return postCursor;
}

function goToPage(input_url)
{
    /* If provided a URL, go to that URL.
     * This is useful in going to the next/previous page
     */

    if(input_url != "")
    {
        window.location = input_url;
    }
}

function isLightboxActive()
{
    var lightboxElement = document.getElementsByClassName("tmblr-lightbox")[0];
    if( lightboxElement == undefined )
    {
        return false;
    }
    return true;
    
}

function arrowKeyNavigation( arrowDirection )
{
    if( isLightboxActive() )
    {
        /* Don't do anything.
        * Tumblr will use the arrow keys to switch
        * the image viewed inside the lightbox
        */

        return;
    }

    if( arrowDirection == 'left' )
    {
        goToPage(prevpage);
    }
    else if( arrowDirection == 'right' )
    {
        goToPage(nextpage);
    }

    return;
}

function KeyCheck(e)
{
    var KeyID = (window.event) ? event.keyCode : e.keyCode;

    switch(KeyID)
    {
        case 37: /* left arrow */
            arrowKeyNavigation( 'left' );
            break;

        case 39: /* right arrow */
            arrowKeyNavigation( 'right' );
            break;

        case 190: /* period . */
            scrollToTopOfPage();
            break;

        case 74: /* J */
            jknavigation( 'j' );
            break;

        case 75: /* K */
            jknavigation( 'k' );
            break;
    }
}


/* PreviousPage and NextPage are only available when
 * inside block:PreviousPage or block:NextPage.
 * We declare the variables outside the blocks to
 * ensure they always exist, then set them inside the blocks.
 */

var prevpage = "";
var nextpage = "";

{block:PreviousPage}
prevpage = "{PreviousPage}";
{/block:PreviousPage}

{block:NextPage}
nextpage = "{NextPage}";
{/block:NextPage}

document.onkeydown = KeyCheck;   

</script>

Save your theme, fully refresh your page, and if everything worked, now you have keyboard navigation. Happy theming!