Skip to content

A humble attempt to create a visually pleasing and highly functional Anki card template that easily integrates with the Yomitan browser extension

Notifications You must be signed in to change notification settings

99-Knots/PrettyYomitanCards

Repository files navigation

PrettyYomitanCards

About this Project

This repository aims to provide a practical template designed to enhance your Anki flashcards and seemlessly integrate with the Yomitan browser extension through the use of HTML, CSS and JavaScript.

Popup-dictionaries like Yomitan can offer a plethora of information for individual words and expressions. However, incorporating it all at once into a flashcard for study purposes can quickly lead to being overwhelmed by content. By only selectively displaying the information, this template aims for a easily digestible format while also ensuring that all potentially useful information remains accessible.

Card Design

Front of the Anki card with the expression and an example Sentence

Front Template

The front is designed to be as simple as possible by default, showing only the expression in question. By providing as little additional information as possible the card can be stripped of unconcious cues that might help discerning the answer without genuine recollection. But for those who prefer learning their words in context, pressing the Example button reveals the sentence from which the card was generated. The cards also offer the option to set the sentence to always be visible, if so desired.


Front of the Anki card with the expression and an example Sentence

Back Template

On the back side of a card, the expression is presented with furigana as a pronounciation guide, accompanied by the glossaries from Yomitan. This setup allows for a quick verification of the reading and meaning at a glance when reviewing the cards. The different glossary entries are organized initially by their assigned tags in the dictionary and then separated into individual subgroups based on their core meanings, mirroring the original dictionary's structure.

Additionally, if Yomitan was able to source an audiofile of the pronounciation, a play button next to the expressions allows one to listen to it. A direct link to the term's search results in the jisho.org online dictionary is also available at the bottom of the card, offering more information about the its usage and the associated kanji.


Front of the Anki card with the expression and an example Sentence

Further functionality is accessible through buttons on the card itself, allowing to trigger the display of various types of information. For example, if multiple dictionaries are installed, ona can switch between their respective entries via tabs on the side. An installed pitch accent dictionary is installed, it is possible to show a visualisation of an expression's pitch pattern, or patterns if multiple are in use, either through markers on the term's reading or an optional graph above it, if preferred. Like on the front side, one can also enable the example sentence, if given. This time with the addition of a furigana option as well. The Notes field of a card provides space for additional information and, if used, another tab appears below the ones for Pitch and Example, which can be used to trigger the display of the custom notes.

In order to get more information on the different tags, that have been assigned to a card, one can hover their cursor over one (or tab it on mobile devices), revealing a popup with a short description. Note that this is based on the usage of tags in an individual dictionary, and will not be available if the dictionary does not provide the necessary tag data. In a similar vein, hovering over, or tapping, a kanji in the expression will show a popup with its handwritten form, with the individual strokes labeled in the traditional drawing order.


How to set up

Requirements

First, you need to install the necessary programs for the generation of Anki cards:

  • Anki flashcard program - version 2.1.50 or higher

  • Yomitan browser extension
    please note that while these cards should be compatible with it, Yomichan has been discontinued and it is highly recommended for current users to switch to Yomitan instead. Instructions for migrating your settings and dictionaries can be found here.

  • AnkiConnect AddOn for Anki

  • A dictionary for Yomitan
    Due to big differences in their formating, only some dictionaries may be compatible with this card design. The tested ones include:

    • Jitendex - modern format for the J->E JMdict
    • JMdict - dictionary availabe in various languages
    • JMnedict - readings for different kinds of names and proper nouns
    • Kanjium - pitch accents - see here for a direct download link

To add a dictionary to Yomitan, in the Yomitan settings under Dictionaries click the Configure installed and enabled dictionaries… option, then Import and select the .zip file of the dictionary.

Creating the Anki Note Type

You can skip this section by simply importing the demo Deck and using the note type that comes with it. Continue the setup with the Configuring Yomitan section.

In the Anki desktop app, under Tools > Manage Note Types click Add. You will be prompted to select an existing type as a base. Your choice here doesn't matter as we will be doing quite some customization anyway, so picking Add:Basic is fine. Give your new note type a name and click OK.

With your new note type selected click on Fields and add and delete fields till your card type looks like this:

Screenshot of the Anki note editor showing the fields Expression, Furigana, Pitch, Pitch Graph, Meaning, Sentence, Sentence with furigana, Audio, URL and Notes.

Pay extra attention to the spelling and capitalization of each field name. You can ignore the other settings on this page, as they will only affect the appearance of the fields in the card browser.

Save your changes and return to the note types editor, this time selecting Cards instead of Fields. Delete all the existing code in the Front Template and replace it with the following:

Front Template Code
<script>
////////////////////////////////////////////////////////
///////////////////// Variables ////////////////////////
////////////////////////////////////////////////////////
// here you can set different preferences for the presets
// the main color for the card in RGB-format; base for calculating all other colors.
// must be in the format [r, g, b] where r, g, b are numbers between 0 and 255.
var colorRGB = [205, 0, 205];
// set which fields are to be shown by default
// allowed values are true and false
var show_example_sentence_by_default = false;
var expr_font_size = 30; // font size in px for the expression at the very top; default: 30
var content_font_size = 18; // font size in px for the example sentence; default: 18
// !!! If you don't know what you are doing, do not procceed beyond this point !!!
////////////////////////////////////////////////////////
////////////////// End of Variables ////////////////////
////////////////////////////////////////////////////////
</script>
<div class="main" lang="ja">
<div id="expr-field" class="expression-field content bottom">
<span class="expression fill-flex">
{{Expression}}
</span>
<aside class="tab corner-offset">
<div id="ex-tab" class="title">Example</div>
</aside>
</div>
<div class="content-container">
<div id="ex-content" class="content hidden top-border">
<div id="ex-plain" class="padded">
{{Sentence}}
</div>
</div>
</div>
</div>
<script>
// clip rgb values to fit [0, 255] range
for(var i=0; i<colorRGB.length; i++)
colorRGB[i] = (colorRGB[i]>255) ? 255 : ((colorRGB[i]<0) ? 0 : colorRGB[i]);
// set the corresponding variables on the style sheet
var colorHSL = RGBtoHSL(colorRGB[0], colorRGB[1], colorRGB[2]);
var root = document.querySelector(':root');
root.style.setProperty('--h', colorHSL[0] + '');
root.style.setProperty('--s', colorHSL[1] + '%');
root.style.setProperty('--l', colorHSL[2] + '%');
root.style.setProperty('--l-copy', colorHSL[2] + '%');
root.style.setProperty('--expr-font-size', expr_font_size + 'px');
root.style.setProperty('--content-font-size', content_font_size + 'px');
/**
* Converts a given RGB color to HSL format, compatible with CSS' HSL colors
*
* @param {int} r red color value, between 0 and 255
* @param {int} g green color value, between 0 and 255
* @param {int} b blue color value, between 0 and 255
* @returns {number[]} array in the form [h, s, l]
*/
function RGBtoHSL(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
var cMin = Math.min(r, g, b);
var cMax = Math.max(r, g, b);
var h, s, l;
l = (cMin + cMax) / 2;
if (cMin == cMax)
h = s = 0;
else {
var delta = cMax - cMin;
s = delta/(1-Math.abs(2*l - 1));
switch(cMax) {
case r:
h = (g-b)/delta + (g<b ? 6: 0);
break;
case g:
h = (b-r)/delta + 2;
break;
case b:
h = (r-g)/delta + 4;
break;
}
h /= 6;
}
return [h*360, s*100, l*100];
}
// because yomitan/-chan also adds one-word sentences
if("{{Expression}}"=="{{Sentence}}")
{
document.getElementById('ex-tab').classList.add('disabled');
}
/** Switch an Elements state between hidden and unhidden
*
* @param {string} id - id of the element
* @param {boolean} hidden - if the element is currently hidden from the user
* @returns {boolean} new hidden-status of the element
*/
function hideUnhideContext(id, hidden, )
{
var context = document.getElementById(id);
if(hidden){
document.getElementById('expr-field').classList.remove('bottom');
context.classList.remove('hidden');
context.classList.add('shown');
}
else {
document.getElementById('expr-field').classList.add('bottom');
context.classList.remove('shown');
context.classList.add('hidden');
}
return !hidden;
}
/** Init a tab's functionality to influence the visiblity of one or two elements
*
* @param {string} idTab - ID of the tab
* @param {string} idContent - ID of the content that should be hidden or unhidden when the tab is clicked
* @param {string} idSecondContent - if provided, a click on tab switches between the elements associated with this ID and idContent
* @param {boolean} showByDefault - if the content should be visible initially; default false
*/
function setupHideFunctionality(idButton, idContent, idSecondContent=null, showByDefault=false) {
var button = document.getElementById(idButton);
var isHidden = true;
if(!button.classList.contains('disabled')) {
button.addEventListener('click', () => {
if(idSecondContent) hideUnhideContext(idSecondContent, !isHidden);
isHidden = hideUnhideContext(idContent, isHidden);
});
if(showByDefault) { //call the functions once to trigger the variable setup
if(idSecondContent) hideUnhideContext(idSecondContent, !isHidden);
isHidden = hideUnhideContext(idContent, isHidden);
}
}
}
setupHideFunctionality('ex-tab', 'ex-content', null, show_example_sentence_by_default);
</script>

Do the same with the Back Template:

Back Template Code
<script>
////////////////////////////////////////////////////////
///////////////////// Variables ////////////////////////
////////////////////////////////////////////////////////
// here you can set different preferences for the presets
// the main color for the card in RGB-format; base for calculating all other colors.
// must be in the format [r, g, b] where r, g, b are numbers between 0 and 255.
var colorRGB = [205, 0, 205];
// set which fields are to be shown by default
// allowed values are true and false
var show_pitch_by_default = false;
var show_pitch_graph_by_default = false;
var show_example_sentence_by_default = false;
var show_sentence_furigana_by_default = false;
var expr_font_size = 30;
var content_font_size = 18; // font size in px for the example sentence, pitch information, notes and the definitions; default: 18
var tag_popup_font_size = 10; // font size in px for the tag explanations when hovering over them; default: 10
// !!! If you don't know what you are doing, do not procceed beyond this point !!!
////////////////////////////////////////////////////////
////////////////// End of Variables ////////////////////
////////////////////////////////////////////////////////
</script>
<div class="main" lang="ja">
<div id="expr-field" class="expression-field content">
<span class="expression fill-flex">
<span id="expr">{{Furigana}}</span>
<span id="audio">{{Audio}}</span>
</span>
<aside class="tab corner-offset">
<div id="pitch-tab" class="title">Pitch</div>
<div id="ex-tab" class="title">Example</div>
<div id="notes-tab" class="title">Notes</div>
</aside>
</div>
<div class="content-container">
<span id="pitch-content" class="content hidden top-border">
<div class="fill-flex padded">
<div id="pitch">
{{Pitch}}
</div>
<div id="pitch-graph" class="hidden">
{{Pitch Graph}}
</div>
</div>
<aside class="tab">
<div id="graph-tab" class="title">Graph</div>
</aside>
</span>
<div id="ex-content" class="content hidden top-border">
<div class="fill-flex padded">
<div id="ex-plain">
{{Sentence}}
</div>
<div id="ex-furi" class="hidden">
{{Sentence with furigana}}
</div>
</div>
<aside class="tab">
<div id="ex-furi-tab" class="title">Furigana</div>
</aside>
</div>
<span id="notes-content" class="content hidden top-border">
<div id="notes-info">
{{Notes}}
</div>
</span>
</div>
<div class="glossary top-border">
<div class="content">
<span id="meaning" class="fill-flex padded">{{Meaning}}</span>
<aside id=dictionary-tabs class="tab">
</aside>
</div>
<div class="jisho-link">
<a class="jisho-link" href="http://jisho.org/search/{{Expression}}">≪Jisho.org≫</a>
</div>
</div>
</div>
<script>
////////////////////////////////////////////////////////////////////////////
///////////////////formating the provided field contents////////////////////
// Expression on top
var expression = document.getElementById('expr');
formatRuby(expression);
// Pitch and Pitch graph
var pitch = document.getElementById('pitch');
var pitchGraph = document.getElementById('pitch-graph');
formatPitchContent(pitch, pitchGraph);
// Example Sentence with and without Furigana
var sentence = document.getElementById('ex-plain');
var sentenceFurigana = document.getElementById('ex-furi');
// Notes
var notes = document.getElementById('notes-info').textContent.trim();
// Meaning
var meaning = document.getElementById('meaning');
formatMeaning(meaning);
// clip rgb values to fit [0, 255] range
for(var i=0; i<colorRGB.length; i++)
colorRGB[i] = (colorRGB[i]>255) ? 255 : ((colorRGB[i]<0) ? 0 : colorRGB[i]);
// apply the set variables
var root = document.querySelector(':root');
// set the corresponding variables on the style sheet
var colorHSL = RGBtoHSL(colorRGB[0], colorRGB[1], colorRGB[2]);
root.style.setProperty('--h', colorHSL[0] + '');
root.style.setProperty('--s', colorHSL[1] + '%');
root.style.setProperty('--l', colorHSL[2] + '%');
root.style.setProperty('--l-copy', colorHSL[2] + '%');
root.style.setProperty('--expr-font-size', expr_font_size + 'px');
root.style.setProperty('--content-font-size', content_font_size + 'px');
root.style.setProperty('--tag-popup-font-size', tag_popup_font_size + 'px');
/** Converts a given RGB color to HSL format, compatible with CSS' HSL colors
*
* @param {int} r - red color value, between 0 and 255
* @param {int} g - green color value, between 0 and 255
* @param {int} b - blue color value, between 0 and 255
* @returns {number[]} array in the form [h, s, l]
*/
function RGBtoHSL(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
var cMin = Math.min(r, g, b);
var cMax = Math.max(r, g, b);
var h, s, l;
l = (cMin + cMax) / 2;
if (cMin == cMax)
h = s = 0;
else {
var delta = cMax - cMin;
s = delta/(1-Math.abs(2*l - 1));
switch(cMax) {
case r:
h = (g-b)/delta + (g<b ? 6: 0);
break;
case g:
h = (b-r)/delta + 2;
break;
case b:
h = (r-g)/delta + 4;
break;
}
h /= 6;
}
return [h*360, s*100, l*100];
}
////////////////////////// Arrange Pitch Accent Field ///////////////////////////////
/** Unwrap all 'ol' or 'ul' elements into a flat array
*
* @param {HTMLElement} elem - source that the array will be created from
* @returns {HTMLElement[]}
*/
function htmlToArray(elem) {
var returnList = [];
if(elem.tagName === 'OL' || elem.tagName === 'UL')
elem.childNodes.forEach( child => { returnList = returnList.concat(htmlToArray(child)) });
else
if(elem.nodeType !== Node.TEXT_NODE)
returnList.push(elem);
return returnList;
}
/** Format the pitch and the pitch graph.
* Set them up, so the pitch element displays a list with the different marked readings
* and the graph element shows the readings list with the corresponding graphs above
*
* @param {HTMLElement} pitchElem - readings with the pitch markings
* @param {HTMLElement} graphElem - graph visualisations
*/
function formatPitchContent(pitchElem, graphElem) {
if(!pitchElem.firstElementChild) return; // no formating possible if no pitch is given
var pitchArray = htmlToArray(pitchElem.firstElementChild);
var graphArray = htmlToArray(graphElem.firstElementChild);
var pitchOl = document.createElement('ol');
var graphOl = document.createElement('ol');
for(var i=0; i<pitchArray.length; i++) {
var pitchPlain = pitchArray[i];
var graph = document.createElement('span');
graph.classList.add('graph');
// if the word starts with sutegana move the graph over to fit the mora slightly better
if('ゃャゅュょョ'.includes(pitchPlain.innerText[1]))
graph.classList.add('sutegana');
if(pitchPlain.tagName !== 'LI') { // ensure consistent formating
var liPitchElem = document.createElement('li');
liPitchElem.appendChild(pitchPlain);
pitchPlain = liPitchElem;
}
if(graphArray[i].tagName !== 'LI')
graph.appendChild(graphArray[i]);
else
graphArray[i].childNodes.forEach( child => { graph.appendChild(child) });
pitchOl.appendChild(pitchPlain);
graphOl.appendChild(graph);
graphOl.appendChild(pitchPlain.cloneNode(true));
}
pitchElem.textContent = '';
graphElem.textContent = '';
pitchElem.appendChild(pitchOl);
graphElem.appendChild(graphOl);
}
/** Reformat the element by putting each kanji in it's own HTMLElement
* and adding the popup funcionality for the stroke order
*
* @param {HTMLElement} rubyElem - element containing the ruby tags to format
*/
function formatRuby(rubyElem) {
rubyElem.childNodes.forEach( child => {
if (child.tagName === 'RUBY') {
child.childNodes.forEach( c => {
if(c.nodeType === Node.TEXT_NODE) {
var text = c.textContent;
c.textContent = '';
for(var i=0; i<text.length; i++) {
var kanjiElem = document.createElement('span');
var kanjiPopup = document.createElement('span');
kanjiElem.classList.add('popup');
kanjiPopup.classList.add('popup-text', 'stroke-order');
kanjiElem.innerText = kanjiPopup.innerText = text[i];
kanjiElem.appendChild(kanjiPopup);
child.insertBefore(kanjiElem, c);
}
child.removeChild(c);
}
});
}
});
}
/////////////////////////////////////////////////////////////////////////////////////
////////////////////////// format glossary from meaning /////////////////////////////
/** @typedef {Object} content
* @property {HTMLElement[]} tags - the tags associated with the given glossary entries
* @property {HTMLElement[]} entries - array of related glossary entries
*/
/** Extract the tag and glossary information from a dictionary element
*
* @param {HTMLElement} dictElem - dictionary element
* @returns {content} array of glossary content objects in the dictionary
*/
function getDictionaryContent(dictElem) {
var content = [];
var tags = [];
var entries = [];
dictElem.childNodes.forEach( entry => {
switch(entry.dataset?.type) {
case 'tag':
if (!entry.dataset.tagNotes.match(/Sense #/g)) { // filter out JMDicts numbered entry tags
if (entry.dataset.tagNotes === "") // disable the popup feature, if the popup has no content, anyway
entry.classList.remove('popup');
tags.push(entry);
}
break;
case 'glossary-entry':
entries.push(entry);
break;
case 'glossary':
// if there are any existing entry lists, complete those first
if(entries.length > 0)
content.push({tags: tags, entries: entries});
entries = [];
// glossaries are set up to only ever contain 'glossary-entries'
entry.childNodes.forEach( child => { entries.push(child) });
content.push({tags: tags, entries: entries});
entries = [];
tags = [];
break;
}
});
if(entries.length > 0)
content.push({tags: tags, entries: entries});
return content;
}
/**Reformat the elements contained in a dictionary object to a HTMLElement
* @param {Object} dict - dictionary object
* @param {string} dict.dictionary - name of the dictionary
* @param {content[]} dict.content - array of content objects
* @returns {HTMLElement} new element containing the dictionary's information
*/
function formatDictionary(dict) {
var dictElem = document.createElement('div');
dictElem.id = dict.dictionary;
dictElem.classList.add('dictionary');
var tagNames = [];
var oldTagNames = [];
dict.content.forEach( content => {
var tagListElem = document.createElement('span');
var entryElem = document.createElement('ul');
var difTag = false;
content.tags.forEach( tag => {
if(tag.dataset?.tagName) {
tagNames.push(tag.dataset.tagName);
if(!oldTagNames.includes(tag.dataset.tagName))
difTag = true;
}
});
if(difTag) {
content.tags.forEach( t=>{tagListElem.appendChild(t)});
entryElem.appendChild(tagListElem);
}
content.entries.forEach( entry => {entryElem.appendChild(entry)});
dictElem.appendChild(entryElem);
oldTagNames = [...tagNames];
tagNames = [];
});
return dictElem;
}
/** Create individual elements to the MeaningElem for the provided dictionaries and their entries
* and add the tabs to switch between them.
*
* @param {HTMLElement} meaningElem - element containing the contents of the cards 'Meaning' field
*/
function formatMeaning(meaningElem) {
var meaningArray = [];
meaningElem.childNodes.forEach( dict => {
if(dict.dataset?.dictionary) {
var content = getDictionaryContent(dict);
if(dict.dataset.dictionary === meaningArray[meaningArray.length-1]?.dictionary) {
var dictEntry = meaningArray[meaningArray.length-1];
dictEntry.content = dictEntry.content.concat(content);
}
else
meaningArray.push({dictionary: dict.dataset.dictionary, content: content});
}
});
meaningElem.textContent = '';
var tabsElem = document.getElementById('dictionary-tabs');
meaningArray.forEach( dict => {
var tab = document.createElement('div');
tab.id = dict.dictionary + '-tab';
tab.classList.add('title', 'unselected');
tab.textContent = dict.dictionary;
tabsElem.appendChild(tab);
meaningElem.appendChild(formatDictionary(dict));
});
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////// Side Tab Logic ////////////////////////////////////////////////////////////////////
// disable the Sentence Tab. if it doesn't differ from the expression or no sentence has been provided
if('{{Expression}}'==='{{Sentence}}' || sentence.textContent === '')
{
document.getElementById('ex-tab').classList.add('disabled');
}
// disable Pitch Tab, if yomitan/-chan could not provide any data on it anyway
if(pitch.textContent.includes('No pitch accent data') || pitch.textContent === '') {
document.getElementById('pitch-tab').classList.add('disabled');
}
// remove Notes Tab, if no notes have been provided
if(notes === '') {
document.getElementById('notes-tab').classList.add('disabled');
document.getElementById('notes-tab').style.display = 'none';
}
// disable the Furigana Tab for Sentences, that don't contain any
if(sentence.textContent === sentenceFurigana.textContent || sentenceFurigana.textContent === '') {
document.getElementById('ex-furi-tab').classList.add('disabled');
}
// trigger side tabs
/** Switch an Elements state between hidden and unhidden
*
* @param {string} id - id of the element
* @param {boolean} hidden - if the element is currently hidden from the user
* @returns {boolean} new hidden-status of the element
*/
function hideUnhideContent(id, hidden)
{
var content = document.getElementById(id);
if(hidden){
content.classList.remove('hidden');
content.classList.add('shown');
}
else {
content.classList.remove('shown');
content.classList.add('hidden');
}
return !hidden;
}
/** Switch an array of elements to hidden and a selected one to visible
* and mark the selected tab
*
* @param {string[]} ids - ids of all the related elements
* @param {int} selectedIdIndex - index of the ID in ids which is to be switched to visible
*/
function switchHiddenContent(ids, selectedIdIndex) {
for(var i=0; i<ids.length; i++) {
hideUnhideContent(ids[i], i===selectedIdIndex);
document.getElementById(ids[i] + '-tab').classList.replace(i===selectedIdIndex? 'unselected' :'selected', i===selectedIdIndex? 'selected' :'unselected');
}
}
/** Init a tab's functionality to influence the visiblity of one or two elements
*
* @param {string} idTab - ID of the tab
* @param {string} idContent - ID of the content that should be hidden or unhidden when the tab is clicked
* @param {string} idSecondContent - if provided, a click on tab switches between the elements associated with this ID and idContent
* @param {boolean} showByDefault - if the content should be visible initially; default false
*/
function setupHideFunctionality(idTab, idContent, idSecondContent=null, showByDefault=false) {
var button = document.getElementById(idTab);
var isHidden = true;
if(!button.classList.contains('disabled')) {
button.addEventListener('click', () => {
if(idSecondContent)
hideUnhideContent(idSecondContent, !isHidden);
isHidden = hideUnhideContent(idContent, isHidden);
});
if(showByDefault) { //call the functions once to trigger the variable setup
if(idSecondContent)
hideUnhideContent(idSecondContent, !isHidden);
isHidden = hideUnhideContent(idContent, isHidden);
}
}
}
/** Init function for the switching between the different dictionaries
*/
function setupSwitchDicts() {
var ids = [];
for(let k=0; k<meaning.childNodes.length; k++) {
let id = meaning.childNodes[k].id;
ids.push(id);
document.getElementById(id+'-tab').addEventListener('click', () => { switchHiddenContent(ids, k) }); // todo: find a way to pass value not reference
}
switchHiddenContent(ids, 0);
}
setupHideFunctionality('pitch-tab', 'pitch-content', null, show_pitch_by_default);
setupHideFunctionality('ex-tab', 'ex-content', null, show_example_sentence_by_default);
setupHideFunctionality('notes-tab', 'notes-content');
setupHideFunctionality('graph-tab', 'pitch-graph', 'pitch', show_pitch_graph_by_default);
setupHideFunctionality('ex-furi-tab', 'ex-furi', 'ex-plain', show_sentence_furigana_by_default);
setupSwitchDicts();
</script>

and for Styling:

Style Sheet
@font-face {
font-family: StrokeOrder;
src: url('_KanjiStrokeOrders_v4.004.ttf');
}
:root {
--expr-font-size: 30px;
--content-font-size: 18px;
--tab-font-size: 12px;
--tag-popup-font-size: 10px;
--h: 300;
--s: 100%;
--l: 50%;
--l-copy: 50%;
--darken1: -10%;
--darken2: -15%;
--lighten1: 15%;
--lighten2: 30%;
--compl-offset: 240;
--gloss-gradient: linear-gradient(hsl(var(--h), calc(var(--s) * 0.5), 90%), #eeeeee);
--gloss-font-color: #000000;
--popup-background: #eeeeee;
--popup-font-color: #000000;
}
.nightMode {
--l: calc(var(--l-copy) + var(--darken2));
--popup-background: #000000;
--popup-font-color: #f5f5f5;
--gloss-gradient: linear-gradient(#333333, #111111);
--gloss-font-color: #f5f5f5;
}
.card {
font-family: "Empty", "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro", "Osaka", "メイリオ", "Meiryo", "MS Pゴシック", "MS PGothic", "MS ゴシック" , "MS Gothic", "Noto Sans CJK JP", "TakaoPGothic", sans-serif;
text-align: left;
overflow-y: scroll;
--main-color: hsl(var(--h), var(--s), var(--l));
--main-color-darker1: hsl(var(--h), var(--s), calc(var(--l) + var(--darken1)));
--main-color-darker2: hsl(var(--h), var(--s), calc(var(--l) + var(--darken2)));
--main-color-lighter1: hsl(var(--h), var(--s), calc(var(--l) + var(--lighten1)));
--main-color-lighter2: hsl(var(--h), var(--s), calc(var(--l) + var(--lighten2)));
--compl-color: hsl(calc(var(--h) + var(--compl-offset)), var(--s), var(--l));
--compl-color-darker: hsl(calc(var(--h) + var(--compl-offset)), var(--s), calc(var(--l) + var(--darken1)));
--expr-gradient: linear-gradient(var(--main-color), var(--main-color-darker1));
--expr-font-color: hsl(0, 0%, calc((70% - var(--l)) * 1000));
--tab-gradient: linear-gradient(var(--main-color-lighter2), var(--main-color-lighter1));
--tab-font-color: hsl(0, 0%, calc((70% - (var(--l) + var(--lighten1))) * 1000));;
--content-gradient: linear-gradient(var(--main-color-darker1), var(--main-color-darker2));
--content-font-color: hsl(0, 0%, calc((70% - var(--l)) * 1000));
--disa-tab-gradient: linear-gradient(#666666, #999999);
--disa-tab-font-color: #afafaf;
--tag-gradient: linear-gradient(var(--compl-color), var(--compl-color-darker));
--tag-font-color: hsl(0, 0%, calc((70% - var(--l)) * 1000));
--link-font-color: var(--main-color);
}
.main {
display: flex;
flex-direction: column;
position: relative;
margin-right: 1em;
}
.main > :first-child {
border-top-left-radius: 1.5vh;
border-top-right-radius: 1.5vh;
}
.main > :last-child, .bottom {
border-bottom-left-radius: 1.5vh;
border-bottom-right-radius: 1.5vh;
}
.expression-field {
color: var(--expr-font-color);
font-size: var(--expr-font-size);
background: var(--expr-gradient);
}
.expression {
padding: 1.2em 0.8em 0.8em 0.8em;
}
.popup {
position: relative;
}
.popup:hover .popup-text {
visibility: visible;
}
.popup-text {
z-index: 1;
position: absolute;
top: 100%;
left: 0%;
color: var(--popup-font-color);
background-color: var(--popup-background);
border: 1px solid var(--popup-font-color);
visibility: hidden;
padding: 0.5em;
width: max-content;
}
.popup .stroke-order {
font-family: StrokeOrder;
font-size: 50vmin;
padding: 0.1em;
}
.replay-button {
width: 1em;
top: 0.8em;
}
.replay-button svg circle {
fill: none;
stroke: none;
}
.replay-button svg path {
fill: var(--expr-font-color);
}
.corner-offset {
margin-top: 1vh;
}
.tab {
display: flex;
flex-direction: column;
align-self: flex-start;
color: var(--tab-font-color);
font-family: 'times new roman';
font-weight: bold;
font-size: var(--tab-font-size);
text-align: right;
gap: 0.3em;
margin-top: 0.3em;
margin-right: -1em;
}
.tab .title {
background: var(--tab-gradient);
border-radius: 0.3em;
padding: 0.2em 0.2em 0.2em 1em;
user-select: none;
width: 6em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab .title.selected {
margin-right: 0.8em;
}
.tab .title.unselected {
margin-left: 0.8em;
}
.tab .title:first {
margin-top: 1.5vh;
}
.tab .title:hover {
cursor: pointer;
}
.tab .title.disabled {
background: var(--disa-tab-gradient);
color: var(--disa-tab-font-color);
}
.tab .title.disabled:hover {
cursor: default;
}
.top-border {
border-top: calc(var(--content-font-size)*0.1) var(--content-font-color) solid;
}
.fill-flex {
flex: 1;
}
.padded {
padding: 1em;
}
.content-container {
display: flex;
flex-direction: column;
color: var(--content-font-color);
font-size: var(--content-font-size);
background: var(--content-gradient);
}
.content {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: baseline;
max-height: 9000px;
overflow: visible;
}
.hidden {
max-height: 0px;
border: 0;
overflow: hidden;
}
.graph {
font-size: calc(var(--content-font-size) * 1.4);
}
.graph.sutegana {
margin-left: 0.55em;
}
.tag {
color: var(--tag-font-color);
background: var(--tag-gradient);
margin: 0.2em;
padding: 0.1em 0.5em;
border-radius: 0.2em;
font-size: 60%;
}
.tag .popup-text {
font-size: var(--tag-popup-font-size);
}
.glossary {
color: var(--gloss-font-color);
background: var(--gloss-gradient);
padding-bottom: 0.8em;
}
.glossary ul:not(:last-child) {
padding-bottom: 1em;
}
.jisho-link {
color: var(--link-font-color);
text-align: center;
font-style: italic;
font-size: 90%;
}
#pitch-content li {
margin-bottom: 0.8em;
line-height: 1.5em;
}
#pitch-content li:last-child{
margin-bottom: 0px;
}
#ex-content ol{
line-height: 2em;
}
#audio {
position: relative;
}
audio {
width: 145px;
position: absolute;
bottom: 0%;
left: 1em;
}
audio::-webkit-media-controls-timeline,
video::-webkit-media-controls-timeline {
display: none;
}
audio::-webkit-media-controls-current-time-display,
audio::-webkit-media-controls-time-remaining-display {
display: none;
}
ol, ul {
margin: 0px;
padding: 0em 0em 0em 1.5em;
}
Front of the Anki card with the expression and an example Sentence

Stroke Order Popup

To have the popup when hovering over a kanji display their stroke order, it is necessary to install this Japanese Stroke Order Font. Instructions for doing so can be found in the Anki Manual.

This step is optional, if one does not intend to use this information. Without the font installed the popup will simply show a bigger version of the kanji in the same font.

Configuring Yomitan

Open Yomitan's settings page in your browser and navigate to the Anki section. Make sure your connection with AnkiConnect is enabled and working properly before clicking on Configure Anki card format....

At the top right choose a deck you want your generated cards to be added to and below that on the Model field select the new node type you created (or PrettyYomitan if you are using the demo deck). Yomitan will then show you a list of the selected node type's fields. Set their values according to the following image:

Screenshot of the Anki card configuration in Yomitan

Yomitan will probably fill out most of this automatically but make sure to verify the correct values. Also note that the {py-glossary} entry for the Meaning field does not appear in the expandable list. This is normal, you will have to enter it by hand.

Close the popup window when you are done and enable the Advanced mode switch at the bottom left of the settings page for the next step. Without doing so the Configure Anki card templates... option will not appear. Click it and without changing anything insert the following at the bottom of the code window (below the line {{~> (lookup . "marker") ~}}):

Yomitan Template Code
{{!~ bring the glossary entry in a coherent HTML-list format ~}}
{{#*inline "py-format-structured"}}
{{~#scope~}}
<ol data-type="glossary">
{{~#if (op ">=" length 1)~}}
{{~#each .~}}
<li data-type="glossary-entry">{{content}}</li>
{{~/each~}}
{{~else~}}
<li data-type="glossary-entry">{{content}}</li>
{{~/if~}}
</ol>
{{~/scope~}}
{{/inline}}
{{!~ create html elements from the given structure ~}}
{{#*inline "py-structured-to-html"}}
{{~#if tag~}}
<{{tag}}>{{>py-format-html content~}}
{{~else~}}
{{.}}
{{~/if~}}
{{~#if tag~}}
</{{tag}}>
{{~/if~}}
{{/inline}}
{{!~ unwrap list objects ~}}
{{#*inline "py-format-html"}}
{{~#if (op "&&"(op "==" (typeof .) "object") (op ">" length 1))~}}
{{~#each .~}}
{{~>py-structured-to-html .}}
{{~/each~}}
{{~else~}}
{{~>py-structured-to-html .}}
{{~/if~}}
{{/inline}}
{{!~ recursively search in the glossary for the tags and the entry marked as "glossary", containing the definitions ~}}
{{#*inline "py-unwrap-structured"}}
{{~#scope~}}
{{~#set "is_glossary"~}}
{{~#regexMatch "glossary" "g"~}}{{data.content}}{{~/regexMatch~}} {{!~ if data.content contains the word "glossary", content contains the entries ~}}
{{~/set~}}
{{~#set "is_ex_sentence"~}}
{{~#regexMatch "sentence" "g"~}}{{data.content}}{{~/regexMatch~}}
{{~/set~}}
{{~#set "is_note"~}}
{{~#regexMatch "note" "g"~}}{{data.content}}{{~/regexMatch~}}
{{~/set~}}
{{~#if (get "is_glossary")~}}
{{~>py-format-structured content~}}
{{~else if (get "is_ex_sentence")~}}
<li data-type="example-sentence">{{>py-format-html content}}</li>
{{~else if (get "is_note")~}}
<li data-type="notes">{{>py-format-html content}}</li>
{{~else if data.code~}} {{!~ only tags seem to have a data.code element ~}}
<i class="tag popup" data-type="tag" data-tag-name="{{~data.code~}}" data-tag-notes="{{~content.title~}}" data-tag-category="">{{~data.code~}}<span class="popup-text">{{~content.title~}}</span></i>
{{~else if (op ">=" length 1)~}}
{{~#each .~}}
{{~>py-unwrap-structured .~}}
{{/each}}
{{~else if content~}}
{{~>py-unwrap-structured content~}}
{{~/if~}}
{{~/scope~}}
{{/inline}}
{{!~ extract the glossary entries with their tags ~}}
{{#*inline "py-glossary"}}
{{~#each definition.definitions~}}
<div data-dictionary="{{~dictionary~}}">
{{~#each definitionTags~}}
<i class="tag popup" data-type="tag" data-tag-name="{{~name~}}" data-tag-notes="{{~notes~}}" data-tag-category="{{~category~}}">{{~name~}}<span class="popup-text">{{~notes~}}</span></i>
{{~/each~}}
{{~#each glossary~}}
{{~#if (op "==" type "structured-content")~}}
{{~>py-unwrap-structured .~}}
{{~else~}}
<li data-type="glossary-entry">{{.}}</li>
{{~/if~}}
{{~/each~}}
</div>
{{~/each~}}
{{/inline}}

You can verify this step by typing {py-glossary} into the Card Field and pressing Test. If it says "The partial py-glossary could not be found", check your spelling, reset the template with the button at the bottom and try again.


You should now be all set up to generate your flashcards from words you find around the web and have them show up in Anki with some nice design and added functionality. Try clicking the green plus at the top right of the demo in the Yomitan settings to create your first card.

※ To enable Yomitan for local files as well, go to your browser's extension settings page (chrome://extensions when using Chrome for example), find Yomitan there and enable 'allow access to file URLs' for it.

Further customization

You can easily change the cards' font sizing, which fields should be expanded by default and even the color yourself if you want to.

In Anki open the card editor and search for var colorRGB, it should be located at the beginning of the template files. Within the brackets you can enter the R, G and B values for a new main color of your choosing. All other colors in the template and a darker night mode version will be calculated based on those values. Try for example the combination 120, 150, 100 for a nice sage green.

Below the color you will find variables with a show_XXX_by_default naming pattern. Changing those from false to true will automatically expand the respective field when the card appears in your reviews. Slightly further down you will also find variables for the font sizes used on the card. Only enter numbers there, the unit (px) will later be added in the code.

For a consistent appearance those changes will have to be applied both to the front and the back template.

Other Resources

Anki-Addon for more audio options for Yomichan

Japanese Font selection taken from here