[Userscript] WaniKani Review Clock

:warning: This is a third-party script/app and is not created by the WaniKani team. By using this, you understand that it can stop working at any time or be discontinued indefinitely.

EDIT 1: Should now work after WaniKani 2023 update.

I couldn’t find a userscript that had the time tracking features that I wanted and was visually appealing, so I made one. Took me a couple days, but I am happy with the result!

Main Features

  1. Display the duration of the current review session
  2. Display the rate of completing reviews (reviews / hour)
  3. Display the estimated time to complete a review session (based on current rate)


To install the script, you will need a userscript manager such as Tampermonkey. Once you have it, you can install the script by clicking the link below.

Install here: WaniKani Review Clock

If you want to adjust settings, you will also need to install WaniKani Open Framework. The extension will work without it as well, but you will be stuck with default settings.

Other Features

  • Hide statistics in settings

  • Only display review rate and time estimate after a time delay from the beginning of a session (off by default):
    Screenshot 2021-08-02 165611
    This is useful because the statistics can be quite inaccurate at the beginning of a session.

  • Calculate an average review rate based on review sessions that are at least 3 minutes long (duration adjustable in settings)
    and display the statistics of the previous session (duration and rate) below the summary page:
    REMOVED: summary page removed in wanikani 2023 update

  • Display a time estimate for the review queue based on average review rate (can be disabled):
    Screenshot 2021-08-02 164501


The settings menu is implemented using WaniKani Open Framework, so install it if you want to adjust settings.

The settings menu can be accessed via the scripts settings menu from the top left in a review session:
Screenshot 2021-08-02 172720

The settings are mostly useful for hiding information that you might not want to be displayed. There are also some additional settings for other features.

Review page settings:

Summary page settings:

Bugs & Suggestions

This script has been tested on Firefox with tampermonkey.

If you encounter any bugs or other annoyances, please let me know on this thread or on GitHub. Also if you have any suggestions, please let me know and I’ll see if I’ll implement them!

Source Code

The source code is hosted on my GitHub. If you are tech-savvy enough, you can take a look and even contribute improvements if you like. Also leave a star if you fancy :slight_smile:


  • 1.1, Aug 3, 2021
    • Added review rate units to settings
    • Added statistics positions to settings
  • 1.2, Oct 6, 2021
    • Fixed icons
  • 1.3, Apr 4, 2023
    • Fixed functionality after WK 2023 update
    • Removed summary page functionality since the page was removed from WK
  • 1.4, Apr 23, 2023
    • Fixed timer elements not updating due to other userscripts
  • 1.5, Jul 17, 2023
    • Enable for Extra Study page. Thanks @rubergly!

I tried it and this is really very nice!

A few opinionated thoughts (feel free to ignore):

  1. I really want to set the update interval to 5 minutes, so it would be nice if there wasn’t a limit on how high these values can be set.

  2. It would be nice if it had the option to display the UI during reviews “out of the way” at the bottom of the page in a gray font similar to what the WaniKani Review Timer script does. (See where it says “Time Elapsed: 6m 2s” in that screenshot. You can ignore where it says “(4s)” in the black bar as that is from a different script.)

  3. I know that in past threads, when people have shared and compared their review times, they have sometimes used units of minutes per 100 reviews or reviews per minute. It would be nice if we could pick the units the script uses so that it is easier to compare.

  4. It would be nice if it counted radicals as 0.5 items since you only answer 1 question for radicals instead of 2.

  5. Sometimes, I will go to start a review but then something else will come up in the middle of the session, but I won’t end the session and will instead resume it later. Also, sometimes I start a review session but then do something else before actually reviewing anything. This means that my stats would show a much lower rate than my actual rate. I think this could be remedied by waiting to start the timer until the first question is answered and, after each question, if no questions are answered for several minutes, not counting the time gap between the last answer and the next answer.

Edit: removed suggestion that was already present in script

1 Like

Thank you for giving it a try! I also really appreciate the detailed suggestions and feedback.

As for the your points:

  1. I knew there would be someone who would like to have values for the settings that I didn’t even consider :D. I will probably remove the limits since it is mostly arbitrary anyways.
  2. I see your point. I would like this script to be as configurable as possible so that everyone can tweak it to their preference, but I’m too lazy to work on this for now. I will keep it in mind though if I have free time in the future.
  3. Yeah different units could be a setting. I will put this in the backlog.
  4. I agree, but due to how I made this technically, the effort to implement this would not be proportional to the gained benefit.
  5. This is also true, and I tried to keep it simple for now. Right now it ignores sessions with zero completed items and ones that are shorter than the set period (3 min by default).
    I could look into this a bit more in the future, but it will probably complicate things technically.

Once again thanks for the feedback! I will probably implement some of the suggestions. I will tag you again if I update the script :slight_smile:

1 Like

Below is a patch implementing #2 and #3 (ability to choose location and units). Note that it is a reverse diff where the minuses are the changes and the pluses are the original. In other words, what is says as “new version” is actually the old version. Most patch utilities have a flag that you can use to tell it the patch is reversed.

--- Current Version
+++ New Version
@@ -3,9 +3,9 @@
 // @namespace   wkreviewclock
 // @description Adds a clock to WaniKani review session statistics and estimates the remaining time.
 // @include     http://www.wanikani.com/review*
 // @include     https://www.wanikani.com/review*
-// @version     1.1
+// @version     1.0
 // @author      Markus Tuominen
 // @grant       none
 // @license     GPL version 3 or later: http://www.gnu.org/copyleft/gpl.html
 // @source      https://github.com/Markus98/wk-review-clock
@@ -21,10 +21,8 @@
 const averageStatsKey = 'reviewRateAverageStats';
 const scriptId = 'WKReviewClock'
 const defaultSettings = {
-    units: 'rph',
-    location: 'toprightright',
     showTimer: true,
     showRate: true,
     showRemaining: true,
     updateInterval: 1.0,
@@ -76,10 +74,10 @@
     const reviewsDoneNumber = parseInt(document.getElementById('completed-count').textContent);
     const reviewRate = time !== 0 ? reviewsDoneNumber/time : 0; // reviews/sec
     if (showRate) {
-        const formattedRate = formatRate(reviewRate, 'short');
-        statHtmlElems.rate.span.textContent = (hideRateRemaining ? '—' : formattedRate) + '';
+        const formattedRate = (reviewRate*3600).toFixed(1); // reviews/hour
+        statHtmlElems.rate.span.textContent = (hideRateRemaining ? '—' : formattedRate) + ' r/h';
     const reviewsAvailableNumber = parseInt(document.getElementById('available-count').textContent);
     const timeRemaining = reviewsAvailableNumber / reviewRate; // seconds
@@ -148,23 +146,9 @@
     // append statsDiv to header
-    let parent;
-    const header = document.createElement('span');
-    const location = wkof.settings[scriptId].location;
-    if (location == 'toprightright') {
-        parent = document.getElementById('stats');
-        parent.append(header);
-    } else if (location == 'toprightleft') {
-        parent = document.getElementById('stats');
-        parent.prepend(header);
-        header.style.cssText = 'margin-right: 2em';
-    } else if (location == 'bottom') {
-        parent = document.getElementById('reviews');
-        parent.append(header);
-        header.classList.add('wkrc_bottom');
-    }
+    const header = document.getElementById('stats');
@@ -253,9 +237,9 @@
     // Saved time and rate
     const lastTime = parseFloat(localStorage.getItem(timerTimeKey));
     const lastTimeStr = isNaN(lastTime) ? '—' : getTimeString(splitToHourMinSec(lastTime));
     const lastRate = parseFloat(localStorage.getItem(timerRateKey));
-    const lastRateStr = formatRate(lastRate);
+    const lastRateStr = isNaN(lastRate) ? '—' : (lastRate*3600).toFixed(1);
     // Average rate
     const avgStats = getAverageStats();
     if (!avgStats.mostRecentAdded && lastTime > ignoreInterval && lastRate > 0) {
@@ -264,18 +248,18 @@
         avgStats.mostRecentAdded = true;
     const avgRate = avgStats.rateSum / avgStats.reviews; // reviews/second
-    const avgRateStr = formatRate(avgRate, 'short');
+    const avgRateStr = isNaN(avgRate) ? '—' : (parseFloat(avgRate)*3600).toFixed(1);
     // Estimate time for current reviews
     const numOfReviews = parseInt(reviewCountSpan.textContent);
     const estimatedTime = numOfReviews / avgRate;
     const estimatedTimeStr = getTimeString(splitToHourMinSec(estimatedTime), false);
     // Set stats text content
     timeSpan.textContent = `Duration: ${lastTimeStr}`;
-    rateSpan.textContent = `Review rate: ${lastRateStr} (avg. ${avgRateStr}) (${avgStats.reviews} sessions)`;
+    rateSpan.textContent = `Review rate: ${lastRateStr} reviews per hour (avg. ${avgRateStr} r/h) (${avgStats.reviews} sessions)`;
     estimatedTimeDiv.textContent = 
         !showEstimatedSessionTime || isNaN(estimatedTime) || numOfReviews === 0 ? 
         '' : `~${estimatedTimeStr}`;
@@ -285,32 +269,8 @@
-let shortUnitNames = {'rph': 'r/h', 'rpm': 'r/m', 'mp100r': 'm/100r'}
-let unitNames = {'rph': 'reviews/hr', 'rpm': 'reviews/min', 'mp100r': 'min/100 reviews'}
-function formatRate(rps, format) {
-    if (isNaN(rps) || rps < 0.00001) {
-        return '—';
-    }
-    rps = parseFloat(rps);
-    const units = wkof.settings[scriptId].units;
-    let res;
-    if (units == 'rph') {
-        res = rps*3600;
-    } else if (units == 'rpm') {
-        res = rps*60;
-    } else if (units == 'mp100r') {
-        res = 1/rps/60*100;
-    }
-    if (format == 'short') {
-        return res.toFixed(1) + ' ' + shortUnitNames[units];
-    } else {
-        return res.toFixed(1) + ' ' + unitNames[units];
-    }
 function openSettings() {
     var config = {
         script_id: scriptId,
         title: 'Review Clock Settings',
@@ -319,40 +279,12 @@
             rateShowDelay = parseFloat(wkof.settings[scriptId].rateShowDelay)*60;
         content: {
-            general: {
-                type: 'page',
-                label: 'General',
-                content: {
-                    units: {
-                        type: 'dropdown',
-                        label: 'Units for Speed',
-                        default: defaultSettings.units,
-                        hover_tip: 'What units the review rate of completion should be displayed in.',
-                        content: {
-                            rph: 'reviews/hr',
-                            rpm: 'reviews/min',
-                            mp100r: 'min/100 reviews',
-                        }
-                    },
-                }
-            },
             reviewPage: {
                 type: 'page',
                 label: 'Review Page',
                 content: {
-                    location: {
-                        type: 'dropdown',
-                        label: 'Display Location',
-                        default: defaultSettings.location,
-                        hover_tip: 'Where to show the below items (if checked) during reviews.',
-                        content: {
-                            toprightright: 'top right (right of other stats)',
-                            toprightleft: 'top right (left of other stats)',
-                            bottom: 'bottom in gray font',
-                        }
-                    },
                     showTimer: {
                         type: 'checkbox',
                         label: 'Show elapsed time',
                         default: defaultSettings.showTimer,
@@ -452,14 +384,8 @@
         console.warn('WaniKani Review Clock: Wanikani Open FrameWork required for adjusting settings. '
             + 'Installation instructions can be found here: https://community.wanikani.com/t/installing-wanikani-open-framework/28549');
-    const style = document.createElement('style');
-    style.textContent = '.wkrc_bottom i { margin-right: 0.5em; margin-left: 0.8em; }' +
-        '.wkrc_bottom span { margin-right: 0.5em; }' +
-        '.wkrc_bottom { color:#BBB; letter-spacing: initial; display: block; text-align: center; }';
-    document.head.append(style);
     if(/session$/.exec(window.location.href)) { // review page
         await generateStatHtmlElems();
     } else { // review summary page
1 Like

It would also be nice if it updated the review numbers right when you finish the reviews in addition to the scheduled updates so that the statistics it computes include all your reviews instead of just the ones done prior to the final scheduled update.

Edit: Another thing is that refreshing the page resets the the timer / count. It would be nice if the behavior were changed so that on page load, it it checks to see if the timer is already running and resumes the timer in that case, rather than starting it over.

Wow! I am flattered. Could you post the whole script without the diff formatting? I can easily see the diff in vscode. This way I won’t have to do awkward copy-pasting and erasing diff formatting. I will take a closer look a bit later when I have time.

You could also fork the repository on GitHub and make a pull request (if you are familiar with working with git and github). This way you would be listed as a contributor on the repository. If you make a PR, allow edits from maintainers, so that I can edit it before merging if necessary.

Not sure I understand what you mean by this. What are you referring to when you say “the review numbers”?

This might be problematic to implement since it resets the completed items counter on refresh, which is used in conjecture with the timer to calculate the rate.

1 Like

You don’t have to do copy-pasting. Just use patch.

1 Like

Didn’t know about this tool, thanks!


Updated the script with your contributions, thank you very much!

I only had to make a small fix to make sure that it worked without WKOF, but otherwise didn’t really have to do anything! You can check out the changes I made on the PR: V1.1 by Markus98 · Pull Request #1 · Markus98/wk-review-clock · GitHub.

I also removed the max limit on the update interval.


Sorry, I mean that if in settings, you set it to update the statistics only once every (say) 5 minutes and do a review session that is 9 minutes long, the statistics that are displayed at the end and used to update your cumulative statistics are the statistics from 5 minutes in to the review session, not the full statistics from the entire review session. I would imagine that in most cases, the user would hope for the full information to be included.

I think the following way of implementing the above two changes might not require too many changes to the existing code:

  1. Use document.addEventListener('visibilitychange', callback) to get notified right before the user leaves the review page. This works for all reasons the browser leaves the page including navigating away, refreshes, etc. The only thing it doesn’t handle is if the browser crashes, but I don’t think it’s necessary to worry about something unlikely like that.
  2. In the callback above, call setStatsAndUpdateTime() to update the stored statistics. I believe this would immediately fix the first point mentioned earlier. And it will also be useful in the fix for the second point as well. (Also, you may need to check the url so it only runs on the .../review/session page.)
  3. Add a localStorage variable that stores whether the timer should be reset. Set this variable to true whenever the review summary page is visited.
  4. Modify the code in main() that runs on the .../review/session page to check the value of the localStorage variable that stores whether the timer should be reset. If it is true, reset the timer. If it is false, resume the timer. You could resume the timer by subtracting the stored elapsed time from startTime. That way, it won’t include time you were away from the page.
1 Like

I noticed that this userscript doesn’t work anymore after the 2023 update. Does anyone know if this might be updated? I’ve found it really useful and it would be a great loss if it doesn’t work anymore! Thank you.

1 Like

I’ve since stopped using WaniKani after leveling to 60. Knowing that people are still using this script and finding it useful makes me happy though.

I spent some time updating this script and fixing the stuff that broke in the 2023 update. Unfortunately the summary page apparently was removed, so I had to remove the summary page time estimation functionality as well. The version of the script should now be 1.3.

Please let me know if you find something that is still broken.


Thank you so much for your help! I’ve just reinstalled it and it shows up now. I’ll let you know if I encounter any bugs. But seriously I really enjoy this user script​.

1 Like

@Makemaan I tried it just now but the clock wasn’t updating. I was doing my reviews for about 20 minutes but it showed the infinity symbol the whole time. I am using Chrome on my Macbook. Please let me know what other info you might need. Thank you again!

Hi, thanks for letting me know.
If you encounter the bug again, could you send me a screenshot of the developer console so that I can check if there are any errors that would give a clue on what the problem is?

You can access that in chrome by pressing Ctrl+Shift+J or Option+⌘+J on mac.

It should look something like this:

There might be a lot of stuff in the console, but the error would most likely be closer to the bottom of the list in red.

If you can reproduce the bug again and send me a screenshot, I might be able to figure out what the problem is and fix it. I haven’t been able to reproduce it myself.

Also if you are using any custom settings for the extension via wanikani open framework, that would be good to know.

Hello! Thanks for wanting to fix this script despite you being done with Wanikani. I don’t know which error here is related to your script, but here is my console:

It doesn’t show anything for the time, and the estimate is infinite.

1 Like

Hi, thanks for sending me this screenshot. Unfortunately it does not have any errors that relate directly to the script code, so I cannot figure out what the problem is from it. It’s possible that the error is higher up and not included in the screenshot. The red error rows are what would most likely give away the reason as to why it is not working.

I cannot reproduce the issue on either chrome or firefox, but if you want to help me diagnose it, I have a few questions that could help me find out what’s wrong.

  1. The timer gets stuck at 1s, correct?
  2. What browser and userscript extension are you using? Chrome and tampermonkey?
  3. Are there any other red errors in the console that are not present in the screenshot you sent? Clicking the expansion arrow on the left side of those entries gives more helpful information as well.
  4. Have you tried changing the statistic update interval in the settings page? (requires wanikani open framework). Changing it to one value, saving it, then setting it to another value again could help.
  5. Have you tried running the userscript only by itself? So turn off any other chrome extension besides tampermonkey and all of the other user scripts in tampermonkey as well. If it works like that, then there is likely a conflict of some sort with another extension/userscript. I can try to fix that if I know what extension / user script the conflict happens with.
  6. If none of the things above get it to work, you could try running it in an incognito window and see if that makes a difference. That essentially runs the script with fresh settings, so that old things left in the browser memory do not cause issues. Running on its own like in questions 5 would be optimal.

The main thing is just to figure out where the issue happens. If I could reproduce the issue locally, it would be an easy fix. Thank you if you take the time to do these things. Otherwise I cannot fix the script.

If there is someone who can reproduce the issue and is able to code, they are always welcome to try and fix it themselves and open a PR on the github page.

Hello! Thanks for responding so quickly. Unfortunately, those are all the errors in the console. I have done some debugging myself this morning, and I think I found the issue, and I’ll try my best to explain it.

  1. Yes, correct.
  2. I am using Firefox 112.0.1 with Tampermonkey.
  3. These are the errors with the arrows expanded with the problem-causing extension loaded (more on that later):
  4. That, unfortunately, didn’t help.
  5. Yes, when you run the user script by itself, it works just fine.
  6. Same issue if the problem extension is loaded.

Now, time for what I found. I first tried disabling all other scripts instead of User Clock, and to my surprise, User Clock worked just fine. After that, I tried enabling scripts I used little by little at a time. Then, I encountered the issue again. I found one script that causes this script to break is Reorder Omega. I have zero ideas why Reorder Omega breaks this script, but it seems to, at least with my setup and system.

I hope this helps, and thank you again for wanting to fix this script!

1 Like

Thanks for the answers to the questions! I was able to reproduce the problem when I installed Reorder Omega, so I was able to figure out why it was breaking. I have now fixed the issue and it is working on my end at least. The latest update that I pushed (1.4) should fix the issue.

Please let me know if you encounter any more issues with the script. Enjoy!


I just found this script, and it’s really well done!

I’ve been using the Extra Study page a lot recently to drill recent lessons and recent mistakes over and over as fast as possible, and started looking for a script that could show my speed while doing those — but I was a little bummed when I realized this script only works on the main Reviews page. So I tweaked it a bit to enable it in Extra Study and submitted a pull request: enable for Extra Study reviews by jruberg · Pull Request #2 · Markus98/wk-review-clock · GitHub

1 Like