TUMBLR KEYBOARD NAVIGATION
J/K
Posted on June 1st 2023
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.
- You will need a main content wrapper for your entire theme. This can be something like a
<div>wrapping your entire theme. - Add the class
keynav-mainelementto your main content wrapper. - Add the class
keynav-postto each of your posts (in Tumblr theme language,{block:Posts}). - 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!