Build JLPT N3 Kanji Flashcards
Build an interactive kanji flashcard app with CSS 3D flip animations.
Introduction
30 Second Summary
Learning kanji is one of the biggest challenges in studying Japanese. Having a flashcard app built exactly the way you want makes all the difference.
In this project, you will build an interactive JLPT N3 kanji flashcard app using HTML, CSS, and JavaScript. Your app flips cards with a smooth 3D animation to reveal readings and meanings, tracks your progress, and saves your study session automatically.
What You'll Build
A browser-based kanji study tool that loads card data from a JSON file and renders an interactive flashcard interface with flip animations, navigation controls, and persistent progress tracking.
By the end of this project, you'll have:
- A working flashcard interface that displays JLPT N3 kanji characters and reveals on'yomi readings, kun'yomi readings, and English meanings with a 3D flip animation.
- Progress tracking that saves your Know and Still Learning status to localStorage so you can pick up where you left off.
- Keyboard navigation for efficient studying with arrow keys and spacebar controls.
- Secret Mission: Add category filtering so you can study kanji grouped by theme (e.g., emotions, actions, nature).
Are there any prerequisites?
None. This guide walks you through setting up everything from scratch. You just need a web browser and a code editor, both of which are covered in Step 1.
Before We Start
Before We Start...
Set Up Your Project
Every web project starts with a clean folder and the right files in place. Your kanji flashcard app needs four files working together: HTML for structure, CSS for styling, JavaScript for interactivity, and JSON for your kanji data.
In this step, you'll create the project folder, set up all four files, and add the starter HTML that connects everything together.
In this step, get ready to:
- Create a project folder on your Desktop.
- Create the four project files inside it.
- Add HTML boilerplate that links to your CSS and JavaScript files.
Create your project folder
- In VS Code, open a new terminal by clicking Terminal in the top menu bar, then clicking New Terminal.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir kanji-flashcards
cd kanji-flashcards
- Open this folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the kanji-flashcards folder, then click Open.
Seeing a trust prompt?
VS Code may ask "Do you trust the authors of the files in this folder?" when opening a new folder. Click Yes, I trust the authors to continue.
VS Code's Explorer sidebar now shows the kanji-flashcards folder name at the top.
Create the four project files
Your project needs four files. You'll create them all at once from the terminal.
- Open a new terminal in this folder by clicking Terminal in the top menu bar, then clicking New Terminal.
- Create all four files by running this command:
macOS
touch index.html style.css script.js cards.json
Windows
New-Item -ItemType File -Name index.html, style.css, script.js, cards.json
- Confirm the files exist by running:
ls
You should see index.html, style.css, script.js, and cards.json in the output.
Don't see all four files?
Make sure your terminal is in the kanji-flashcards folder. Check that the terminal prompt shows the folder name. If not, run cd ~/Desktop/kanji-flashcards and try the file creation command again.
Help me create my project files in VS Code's terminal.
Add the HTML boilerplate
The HTML file acts as the entry point for your app. It links to the CSS and JavaScript files so the browser knows where to find your styles and logic.
- Click on index.html in the Explorer sidebar to open it in the editor.
- Paste the following code into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JLPT N3 Kanji Flashcards</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="script.js"></script>
</body>
</html>
What does this code do?
This is standard HTML boilerplate that every web page needs. The <link> tag connects your stylesheet so CSS can style the page. The <script> tag at the bottom loads your JavaScript after the page content is ready.
Placing the <script> tag just before </body> ensures the HTML is fully loaded before JavaScript tries to interact with it.
- Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Open the file in your browser by right-clicking index.html in the Explorer sidebar and selecting Reveal in Finder (macOS) or Reveal in File Explorer (Windows).
- Double-click index.html in the file browser to open it in your default web browser.
You should see a blank white page. Check the browser tab title. It should read JLPT N3 Kanji Flashcards.
Seeing a file not found error?
Make sure you saved index.html before opening it. A white dot next to the filename in VS Code's tab bar means unsaved changes.
Also confirm you double-clicked index.html (not one of the other files). The browser URL bar should end with index.html.
Help me open my index.html file in the browser.
✔️ Awesome, I've got everything!
Great. Double check you've saved index.html and that it loads in your browser with the correct tab title.
ⓧ I'd like to double check the full code
Here is the complete index.html at this point:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JLPT N3 Kanji Flashcards</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="script.js"></script>
</body>
</html>
The other three files (style.css, script.js, cards.json) should exist but remain empty for now.
Your project skeleton is ready. Next up, you'll fill cards.json with real JLPT N3 kanji data that your flashcard app will display.
Build the Kanji Data File
Your project environment is ready with all four files in place. Now it's time to give your flashcard app something to study.
Every flashcard app needs a deck of cards. In this step, you'll create a JSON data file containing 20 JLPT N3 kanji characters, each with their readings and English meanings.
In this step, get ready to:
- Create a JSON data structure with 20 JLPT N3 kanji entries.
- Include on'yomi, kun'yomi, and English meaning for each card.
- Verify the file loads correctly in your browser.
Create the kanji data structure
Instead of hardcoding kanji information directly into JavaScript, you'll store it in a separate JSON file. This keeps your data organized and makes it easy to add more kanji later.
- Open cards.json in VS Code and paste the following code:
{
"title": "JLPT N3 Kanji",
"cards": [
{ "id": 1, "kanji": "政", "onyomi": "セイ, ショウ", "kunyomi": "まつりごと", "meaning": "government, politics", "category": "daily life" },
{ "id": 2, "kanji": "届", "onyomi": "カイ", "kunyomi": "とど(く)", "meaning": "deliver, reach", "category": "actions" },
{ "id": 3, "kanji": "sincerity", "onyomi": "セイ", "kunyomi": "まこと", "meaning": "sincerity", "category": "emotions" },
{ "id": 4, "kanji": "怒", "onyomi": "ド, ヌ", "kunyomi": "いか(る), おこ(る)", "meaning": "anger", "category": "emotions" },
{ "id": 5, "kanji": "悲", "onyomi": "ヒ", "kunyomi": "かな(しい)", "meaning": "sadness, grief", "category": "emotions" },
{ "id": 6, "kanji": "感", "onyomi": "カン", "kunyomi": "—", "meaning": "feeling, emotion", "category": "emotions" },
{ "id": 7, "kanji": "愛", "onyomi": "アイ", "kunyomi": "—", "meaning": "love, affection", "category": "emotions" },
{ "id": 8, "kanji": "草", "onyomi": "ソウ", "kunyomi": "くさ", "meaning": "grass", "category": "nature" },
{ "id": 9, "kanji": "葉", "onyomi": "ヨウ", "kunyomi": "は", "meaning": "leaf", "category": "nature" },
{ "id": 10, "kanji": "雪", "onyomi": "セツ", "kunyomi": "ゆき", "meaning": "snow", "category": "nature" },
{ "id": 11, "kanji": "港", "onyomi": "コウ", "kunyomi": "みなと", "meaning": "harbor, port", "category": "nature" },
{ "id": 12, "kanji": "深", "onyomi": "シン", "kunyomi": "ふか(い)", "meaning": "deep", "category": "nature" },
{ "id": 13, "kanji": "走", "onyomi": "ソウ", "kunyomi": "はし(る)", "meaning": "run", "category": "actions" },
{ "id": 14, "kanji": "投", "onyomi": "トウ", "kunyomi": "な(げる)", "meaning": "throw", "category": "actions" },
{ "id": 15, "kanji": "届", "onyomi": "トドケ", "kunyomi": "とど(ける)", "meaning": "deliver, reach", "category": "actions" },
{ "id": 16, "kanji": "返", "onyomi": "ヘン", "kunyomi": "かえ(す)", "meaning": "return, give back", "category": "actions" },
{ "id": 17, "kanji": "洗", "onyomi": "セン", "kunyomi": "あら(う)", "meaning": "wash", "category": "daily life" },
{ "id": 18, "kanji": "届", "onyomi": "キョ, コ", "kunyomi": "い(る)", "meaning": "reside, be present", "category": "daily life" },
{ "id": 19, "kanji": "届", "onyomi": "シュク", "kunyomi": "やど", "meaning": "lodging, inn", "category": "daily life" },
{ "id": 20, "kanji": "届", "onyomi": "ショク", "kunyomi": "—", "meaning": "employment, job", "category": "daily life" }
]
}
What does this structure do?
The "title" field names the deck for display purposes. The "cards" array holds 20 objects, each representing one flashcard.
Each card object has six fields: id (a unique number), kanji (the character itself), onyomi (the Chinese-derived reading in katakana), kunyomi (the native Japanese reading in hiragana), meaning (English translation), and category (a theme grouping like "emotions" or "nature").
The unique id on each card is important. Later, your app will use these IDs to track which cards you know and which you're still learning, even if the cards get filtered or reordered.
- Save cards.json.
Seeing red squiggly lines in VS Code?
If VS Code highlights errors in your JSON, check for missing commas between objects in the array. Each object (the curly-braced entries) needs a comma after it except the very last one.
Also make sure all quotes are straight double quotes ("), not curly "smart quotes" that word processors sometimes insert.
Why is my JSON showing errors in VS Code?
Verify your JSON in the browser
Browsers have a built-in JSON parser. If your file has a syntax error (a missing comma, an extra bracket), the browser will show you an error message instead of your data.
- In your browser's address bar, type the full file path to cards.json. You can do this by dragging the file from VS Code's Explorer sidebar directly into a browser tab.
- You should see the raw JSON text displayed in the browser. The data will appear as formatted text with all 20 kanji entries visible.
What should I see?
A wall of text starting with {"title":"JLPT N3 Kanji","cards":[... is a success. Some browsers (like Chrome) will even pretty-print it with collapsible sections.
If you see an error message mentioning "unexpected token" or "JSON parse error", there's a typo in your file. Head back to VS Code and double-check commas and brackets.
Browser showing an error instead of data?
Make sure you saved cards.json before dragging it into the browser. Unsaved changes won't be reflected.
Check that the very last entry in the array does NOT have a trailing comma after its closing brace. JSON does not allow trailing commas.
Help me fix my JSON syntax error.
✔️ Awesome, I've got everything!
Great. Double check you've saved cards.json and the browser displayed your data without errors.
ⓧ I'd like to double check the full code
Here's the complete cards.json file. Compare it against yours character by character:
{
"title": "JLPT N3 Kanji",
"cards": [
{ "id": 1, "kanji": "政", "onyomi": "セイ, ショウ", "kunyomi": "まつりごと", "meaning": "government, politics", "category": "daily life" },
{ "id": 2, "kanji": "届", "onyomi": "カイ", "kunyomi": "とど(く)", "meaning": "deliver, reach", "category": "actions" },
{ "id": 3, "kanji": "sincerity", "onyomi": "セイ", "kunyomi": "まこと", "meaning": "sincerity", "category": "emotions" },
{ "id": 4, "kanji": "怒", "onyomi": "ド, ヌ", "kunyomi": "いか(る), おこ(る)", "meaning": "anger", "category": "emotions" },
{ "id": 5, "kanji": "悲", "onyomi": "ヒ", "kunyomi": "かな(しい)", "meaning": "sadness, grief", "category": "emotions" },
{ "id": 6, "kanji": "感", "onyomi": "カン", "kunyomi": "—", "meaning": "feeling, emotion", "category": "emotions" },
{ "id": 7, "kanji": "愛", "onyomi": "アイ", "kunyomi": "—", "meaning": "love, affection", "category": "emotions" },
{ "id": 8, "kanji": "草", "onyomi": "ソウ", "kunyomi": "くさ", "meaning": "grass", "category": "nature" },
{ "id": 9, "kanji": "葉", "onyomi": "ヨウ", "kunyomi": "は", "meaning": "leaf", "category": "nature" },
{ "id": 10, "kanji": "雪", "onyomi": "セツ", "kunyomi": "ゆき", "meaning": "snow", "category": "nature" },
{ "id": 11, "kanji": "港", "onyomi": "コウ", "kunyomi": "みなと", "meaning": "harbor, port", "category": "nature" },
{ "id": 12, "kanji": "深", "onyomi": "シン", "kunyomi": "ふか(い)", "meaning": "deep", "category": "nature" },
{ "id": 13, "kanji": "走", "onyomi": "ソウ", "kunyomi": "はし(る)", "meaning": "run", "category": "actions" },
{ "id": 14, "kanji": "投", "onyomi": "トウ", "kunyomi": "な(げる)", "meaning": "throw", "category": "actions" },
{ "id": 15, "kanji": "届", "onyomi": "トドケ", "kunyomi": "とど(ける)", "meaning": "deliver, reach", "category": "actions" },
{ "id": 16, "kanji": "返", "onyomi": "ヘン", "kunyomi": "かえ(す)", "meaning": "return, give back", "category": "actions" },
{ "id": 17, "kanji": "洗", "onyomi": "セン", "kunyomi": "あら(う)", "meaning": "wash", "category": "daily life" },
{ "id": 18, "kanji": "届", "onyomi": "キョ, コ", "kunyomi": "い(る)", "meaning": "reside, be present", "category": "daily life" },
{ "id": 19, "kanji": "届", "onyomi": "シュク", "kunyomi": "やど", "meaning": "lodging, inn", "category": "daily life" },
{ "id": 20, "kanji": "届", "onyomi": "ショク", "kunyomi": "—", "meaning": "employment, job", "category": "daily life" }
]
}
Your kanji deck is ready to go. Next up, you'll build the HTML interface that displays these cards with a flip animation.
Build the HTML Interface
Your kanji data is ready in cards.json. Now you need an interface for the learner to interact with.
A flashcard app needs several UI sections working together: a display area for the card itself, navigation controls to move between cards, and a way to track progress. In this step, you will build all of that structure in HTML.
In this step, get ready to:
- Add the header with the app title and card counter.
- Build the flashcard container with a front face and back face.
- Add navigation buttons, status buttons, and a progress bar.
Add the header with the app title and card counter
Every app needs a clear title and some context for the user. The header will show the app name in Japanese and a counter that displays which card the learner is currently viewing.
- In VS Code, switch back to index.html.
- Find the empty space between the opening <body> tag and the <script src="script.js"></script> line.
- Add the container wrapper and header by pasting this code in that empty space:
<!-- Main wrapper that centers all content -->
<div class="container">
<header>
<!-- App title in Japanese -->
<h1>JLPT N3 漢字</h1>
<!-- Card position tracker, updated by JavaScript later -->
<p id="meta">Card 1 of 20</p>
</header>
What does this code do?
The .container div wraps the entire app. Later, CSS will use this to center everything on the page.
The <p id="meta"> element shows "Card 1 of 20" as a default. JavaScript will update this text dynamically as the learner navigates between cards.
- Save index.html.
- Refresh your browser. You should see "JLPT N3 漢字" as a heading and "Card 1 of 20" as plain text below it.
Don't see any text in the browser?
Make sure you pasted the code between <body> and <script>, not inside the <head> section. Content inside <head> is invisible to the browser.
Also check that you saved the file. Unsaved changes will not appear in the browser.
Still stuck? Help me figure out why my HTML content isn't showing in the browser.
Add the flashcard container with front and back faces
A physical flashcard has two sides. Your digital version needs the same structure. The front shows the kanji character, and the back reveals the readings and meaning.
- In index.html, place your cursor directly after the closing </header> tag.
- Add the flashcard structure by pasting this code below the header:
<!-- Flashcard with two faces that flip via CSS -->
<div class="flashcard" id="flashcard">
<div class="flashcard-inner" id="flashcard-inner">
<!-- Front face: shows the kanji character -->
<div class="flashcard-front">
<span id="kanji-display" class="kanji-char">政</span>
</div>
<!-- Back face: shows readings and meaning -->
<div class="flashcard-back">
<p class="reading-label">音読み (On'yomi)</p>
<p id="onyomi-display" class="reading">セイ, ショウ</p>
<p class="reading-label">訓読み (Kun'yomi)</p>
<p id="kunyomi-display" class="reading">まつりごと</p>
<p class="reading-label">Meaning</p>
<p id="meaning-display" class="meaning">government, politics</p>
</div>
</div>
</div>
What does this code do?
The flashcard uses three nested layers. The outer .flashcard div sets up 3D perspective. The .flashcard-inner div is the piece that actually rotates.
Inside that, .flashcard-front and .flashcard-back sit on top of each other. CSS will later hide whichever face is turned away from the viewer. JavaScript will swap the text content when navigating between cards.
The default content (政, セイ, ショウ, etc.) matches the first card in your cards.json file so the page looks correct before JavaScript loads.
- Save index.html and refresh your browser.
- You should see the kanji 政, the readings (セイ, ショウ and まつりごと), and the meaning "government, politics" all displayed as plain unstyled text below the header.
Seeing garbled characters instead of Japanese?
Make sure the <meta charset="UTF-8"> line is still present in your <head> section. Without it, the browser may not decode Japanese characters correctly.
Also confirm that your file is saved with UTF-8 encoding. In VS Code, check the bottom-right status bar for "UTF-8".
Need more help? Help me fix garbled Japanese characters in my HTML page.
Add the control buttons and progress bar
The learner needs buttons to navigate between cards, flip the card, mark their knowledge status, and see how far they have progressed.
- In index.html, place your cursor after the closing </div> of the flashcard section (the one that closes <div class="flashcard">).
- Add the controls, status buttons, and progress bar by pasting this code:
<!-- Navigation buttons -->
<div class="controls">
<button id="prevBtn" class="nav-btn" disabled>← Previous</button>
<button id="flipBtn" class="flip-btn">Flip</button>
<button id="nextBtn" class="nav-btn">Next →</button>
</div>
<!-- Know / Still Learning status buttons -->
<div class="status-controls">
<button id="knowBtn" class="status-btn know">✓ Know</button>
<button id="learnBtn" class="status-btn learn">✗ Still Learning</button>
</div>
<!-- Progress tracking section -->
<div class="progress-section">
<div class="progress-bar">
<div id="bar" class="progress-fill"></div>
</div>
<p id="progressText">Know: 0 • Still learning: 0 • Reviewed: 0/20 (0%)</p>
</div>
</div>
What does this code do?
- The .controls div holds three navigation buttons. The Previous button starts with the disabled attribute because you cannot go backwards from the first card.
- The .status-controls div holds the Know and Still Learning buttons that let you mark your confidence on each card.
- The .progress-section contains a visual progress bar (#bar fills with color as you review cards) and a text summary.
- The final </div> closes the .container wrapper you opened at the top.
- Save index.html and refresh your browser.
- You should see five buttons (Previous, Flip, Next, Know, Still Learning) and the text "Know: 0 • Still learning: 0 • Reviewed: 0/20 (0%)" below the kanji content.
Buttons not showing up?
Check that you pasted the code after the flashcard's closing </div>, not inside it. If the buttons appear above the kanji, they are in the wrong position.
Also make sure the very last </div> in your pasted code is present. This closes the .container wrapper. A missing closing tag can break the layout entirely.
Still stuck? Help me debug my HTML structure for this flashcard app.
✔️ Awesome, I've got everything!
Great. Double check you have saved index.html before moving on.
ⓧ I'd like to double check the full code
Here is the complete index.html file at this point. Compare yours against it:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JLPT N3 Kanji Flashcards</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>JLPT N3 漢字</h1>
<p id="meta">Card 1 of 20</p>
</header>
<div class="flashcard" id="flashcard">
<div class="flashcard-inner" id="flashcard-inner">
<div class="flashcard-front">
<span id="kanji-display" class="kanji-char">政</span>
</div>
<div class="flashcard-back">
<p class="reading-label">音読み (On'yomi)</p>
<p id="onyomi-display" class="reading">セイ, ショウ</p>
<p class="reading-label">訓読み (Kun'yomi)</p>
<p id="kunyomi-display" class="reading">まつりごと</p>
<p class="reading-label">Meaning</p>
<p id="meaning-display" class="meaning">government, politics</p>
</div>
</div>
</div>
<div class="controls">
<button id="prevBtn" class="nav-btn" disabled>← Previous</button>
<button id="flipBtn" class="flip-btn">Flip</button>
<button id="nextBtn" class="nav-btn">Next →</button>
</div>
<div class="status-controls">
<button id="knowBtn" class="status-btn know">✓ Know</button>
<button id="learnBtn" class="status-btn learn">✗ Still Learning</button>
</div>
<div class="progress-section">
<div class="progress-bar">
<div id="bar" class="progress-fill"></div>
</div>
<p id="progressText">Know: 0 • Still learning: 0 • Reviewed: 0/20 (0%)</p>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
Your interface structure is complete. Everything is on the page, but it looks like a jumble of unstyled text and default buttons. Next up, you will transform this raw structure into a polished, dark-themed flashcard with a smooth 3D flip animation using CSS.
Style the Flashcard with CSS
Your HTML is structured and your JSON is ready, but the app looks like a plain list of text. CSS will transform it into a polished, dark-themed flashcard interface.
In this step, you will write styles that center the layout, create a 3D card flip animation, and give the buttons a clean, modern look.
In this step, get ready to:
- Style the page layout with a dark background and centered content.
- Create the 3D card flip effect using CSS transforms.
- Style the buttons and progress bar.
Style the overall layout
Every element on the page currently has default browser spacing and styling. A CSS reset removes those defaults so you start from a clean slate.
- Switch back to style.css in VS Code and paste the following base layout styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1a1a2e;
color: #eee;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
text-align: center;
max-width: 500px;
width: 90%;
padding: 20px;
}
header h1 {
font-size: 1.8rem;
margin-bottom: 5px;
color: #e94560;
}
header p {
color: #aaa;
margin-bottom: 20px;
}
What does this code do?
- The * selector resets all margins, padding, and uses border-box sizing so elements include padding in their width.
- The body uses flexbox to center the entire app both horizontally and vertically on a dark navy background (#1a1a2e).
- The .container caps the content width at 500px so the flashcard doesn't stretch across wide screens.
- The header title uses a vibrant red-pink accent color (#e94560) that will appear throughout the design.
- Save style.css and refresh your browser.
- You should see a dark navy page with the title in red-pink, and all your content centered in the middle.
Still seeing a white background?
Make sure your <link rel="stylesheet" href="style.css"> is in the <head> of index.html. The filename must match exactly, including the lowercase.
Also confirm that style.css is in the same folder as index.html.
Help me debug my CSS not loading.
Create the 3D card flip effect
The flip animation is the signature interaction of this app. It uses three CSS properties working together to simulate a physical card turning over in 3D space.
- Add the flashcard structure and 3D transform styles below your header styles in style.css:
/* Flashcard */
.flashcard {
width: 300px;
height: 350px;
margin: 0 auto 20px;
perspective: 1000px;
cursor: pointer;
}
.flashcard-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.6s;
transform-style: preserve-3d;
}
.flashcard.flipped .flashcard-inner {
transform: rotateY(180deg);
}
.flashcard-front,
.flashcard-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 16px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
}
.flashcard-front {
background: linear-gradient(135deg, #16213e, #0f3460);
border: 2px solid #e94560;
}
.flashcard-back {
background: linear-gradient(135deg, #0f3460, #1a1a2e);
border: 2px solid #e94560;
transform: rotateY(180deg);
}
How does the 3D flip work?
Three properties create the illusion of a card rotating in space:
- perspective: 1000px on the parent gives the scene depth. Lower values make the rotation look more dramatic.
- transform-style: preserve-3d on .flashcard-inner tells the browser that child elements exist in 3D space, not flattened.
- backface-visibility: hidden hides each face when it rotates away from you. Without this, you would see a mirrored version of the back through the front.
- The back face starts pre-rotated 180 degrees (rotateY(180deg)) so it only becomes visible when the whole card flips.
- Now add the text styling for the kanji and reading displays below your flashcard-back rule:
.kanji-char {
font-size: 8rem;
color: #fff;
}
.reading-label {
font-size: 0.75rem;
color: #e94560;
text-transform: uppercase;
margin-top: 10px;
letter-spacing: 1px;
}
.reading {
font-size: 1.4rem;
color: #fff;
margin-bottom: 5px;
}
.meaning {
font-size: 1.2rem;
color: #aaa;
font-style: italic;
}
What does this code do?
- The kanji character displays at 8rem so it dominates the card front as the focal point.
- Reading labels use the accent color and uppercase styling to clearly distinguish them from the actual readings.
- The meaning appears in italics with a muted gray to visually separate it from the Japanese text.
- Save style.css and refresh your browser.
- You should see a dark card with a large kanji character (政) centered on it, and all the back-face content hidden behind it.
Test the flip manually
Open DevTools (F12), find the .flashcard element, and add the class flipped to it. The card should rotate to reveal the readings on the back.
Seeing both sides at once?
If the front and back overlap instead of flipping, check that backface-visibility: hidden is on both .flashcard-front and .flashcard-back. Also confirm transform-style: preserve-3d is on .flashcard-inner, not on .flashcard itself.
Help me fix my 3D card flip.
Style the buttons and progress bar
The buttons and progress bar need clear visual hierarchy. The flip button should stand out as the primary action, while the navigation buttons stay subtle.
- Add the button control styles below your .meaning rule in style.css:
/* Controls */
.controls {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 15px;
}
.nav-btn, .flip-btn, .status-btn {
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
transition: background 0.2s;
}
.nav-btn {
background: #16213e;
color: #eee;
border: 1px solid #333;
}
.nav-btn:hover:not(:disabled) {
background: #0f3460;
}
.nav-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.flip-btn {
background: #e94560;
color: #fff;
}
.flip-btn:hover {
background: #c73e54;
}
What does this code do?
- The shared rule removes default button borders, adds padding, and sets up a smooth transition: background 0.2s for hover effects.
- Navigation buttons use the same dark blue as the card, making them feel secondary to the flip button.
- The :hover:not(:disabled) selector ensures disabled buttons do not react to hover.
- The flip button uses the same accent red (#e94560) as the header and card border, drawing the eye to the primary action.
- Add the status buttons and progress bar styles below your .flip-btn:hover rule:
/* Status buttons */
.status-controls {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
}
.status-btn.know {
background: #2ecc71;
color: #fff;
}
.status-btn.know:hover {
background: #27ae60;
}
.status-btn.learn {
background: #3498db;
color: #fff;
}
.status-btn.learn:hover {
background: #2980b9;
}
/* Progress */
.progress-section {
margin-top: 10px;
}
.progress-bar {
width: 100%;
height: 8px;
background: #333;
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.progress-fill {
height: 100%;
background: #2ecc71;
width: 0%;
transition: width 0.3s;
}
#progressText {
font-size: 0.8rem;
color: #aaa;
}
What does this code do?
- The Know button uses green (#2ecc71) and the Still Learning button uses blue (#3498db). These colors provide instant visual feedback on your study progress.
- The progress bar starts at width: 0% and uses transition: width 0.3s so it animates smoothly when JavaScript updates the width later.
- The overflow: hidden on .progress-bar clips the fill to the rounded container shape.
- Save style.css and refresh your browser.
- You should see a fully styled app with a red Flip button in the center, subtle Previous/Next buttons on either side, green and blue status buttons below, and a thin progress bar at the bottom.
Buttons look misaligned or oversized?
Check that the * { box-sizing: border-box; } reset is at the very top of your file. Without it, padding adds to element width and throws off the flex layout.
Also confirm that .controls has display: flex and gap: 10px to space the buttons evenly.
Help me fix my button layout.
✔️ Awesome, I've got everything!
Great. Double check you have saved style.css before moving on.
ⓧ I'd like to double check the full code
Here is the complete style.css file. Compare it against yours:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1a1a2e;
color: #eee;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
text-align: center;
max-width: 500px;
width: 90%;
padding: 20px;
}
header h1 {
font-size: 1.8rem;
margin-bottom: 5px;
color: #e94560;
}
header p {
color: #aaa;
margin-bottom: 20px;
}
/* Flashcard */
.flashcard {
width: 300px;
height: 350px;
margin: 0 auto 20px;
perspective: 1000px;
cursor: pointer;
}
.flashcard-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.6s;
transform-style: preserve-3d;
}
.flashcard.flipped .flashcard-inner {
transform: rotateY(180deg);
}
.flashcard-front,
.flashcard-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 16px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
}
.flashcard-front {
background: linear-gradient(135deg, #16213e, #0f3460);
border: 2px solid #e94560;
}
.flashcard-back {
background: linear-gradient(135deg, #0f3460, #1a1a2e);
border: 2px solid #e94560;
transform: rotateY(180deg);
}
.kanji-char {
font-size: 8rem;
color: #fff;
}
.reading-label {
font-size: 0.75rem;
color: #e94560;
text-transform: uppercase;
margin-top: 10px;
letter-spacing: 1px;
}
.reading {
font-size: 1.4rem;
color: #fff;
margin-bottom: 5px;
}
.meaning {
font-size: 1.2rem;
color: #aaa;
font-style: italic;
}
/* Controls */
.controls {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 15px;
}
.nav-btn, .flip-btn, .status-btn {
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
transition: background 0.2s;
}
.nav-btn {
background: #16213e;
color: #eee;
border: 1px solid #333;
}
.nav-btn:hover:not(:disabled) {
background: #0f3460;
}
.nav-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.flip-btn {
background: #e94560;
color: #fff;
}
.flip-btn:hover {
background: #c73e54;
}
/* Status buttons */
.status-controls {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
}
.status-btn.know {
background: #2ecc71;
color: #fff;
}
.status-btn.know:hover {
background: #27ae60;
}
.status-btn.learn {
background: #3498db;
color: #fff;
}
.status-btn.learn:hover {
background: #2980b9;
}
/* Progress */
.progress-section {
margin-top: 10px;
}
.progress-bar {
width: 100%;
height: 8px;
background: #333;
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.progress-fill {
height: 100%;
background: #2ecc71;
width: 0%;
transition: width 0.3s;
}
#progressText {
font-size: 0.8rem;
color: #aaa;
}
Your flashcard app now looks polished and professional. Next up, you will bring it to life with JavaScript to load the kanji data, flip cards, navigate between them, and track your study progress.
Add JavaScript Interactivity
Your flashcard app looks great, but it doesn't actually do anything yet. Clicking the buttons, flipping the card, and tracking progress all require JavaScript to bring the interface to life.
In this step, you'll write the logic that loads kanji data from your JSON file, renders it onto the card, handles flip animations and navigation, and saves your study progress to the browser's localStorage.
In this step, get ready to:
- Fetch cards.json and render the first card's kanji on the front face.
- Implement flip toggling, Previous/Next navigation, and keyboard support.
- Implement Know/Still Learning status tracking with localStorage persistence and update the progress bar.
Fetch cards.json and render the first card
Before your app can display kanji, it needs references to every HTML element it will update. Then it needs to fetch the card data and render the first card.
- In VS Code, open script.js from the Explorer sidebar.
- Add the DOM element references, module-level variables, and the data loading and rendering functions by pasting this code:
const metaEl = document.getElementById("meta");
const flashcard = document.getElementById("flashcard");
const flashcardInner = document.getElementById("flashcard-inner");
const kanjiDisplay = document.getElementById("kanji-display");
const onyomiDisplay = document.getElementById("onyomi-display");
const kunyomiDisplay = document.getElementById("kunyomi-display");
const meaningDisplay = document.getElementById("meaning-display");
const prevBtn = document.getElementById("prevBtn");
const nextBtn = document.getElementById("nextBtn");
const flipBtn = document.getElementById("flipBtn");
const knowBtn = document.getElementById("knowBtn");
const learnBtn = document.getElementById("learnBtn");
const barEl = document.getElementById("bar");
const progressTextEl = document.getElementById("progressText");
let data = null;
let index = 0;
let showBack = false;
let status = loadStatus();
init();
async function init() {
// Fetch the kanji data from our JSON file
const res = await fetch("./cards.json", { cache: "no-store" });
data = await res.json();
hookEvents();
render();
}
function render() {
// Update the card counter text and re-render all sections
const total = data.cards.length;
metaEl.textContent = `Card ${index + 1} of ${total}`;
renderCard();
renderFlip();
updateButtons();
updateProgress();
}
function renderCard() {
// Populate the front and back faces with the current card's data
const card = data.cards[index];
kanjiDisplay.textContent = card.kanji;
onyomiDisplay.textContent = card.onyomi;
kunyomiDisplay.textContent = card.kunyomi;
meaningDisplay.textContent = card.meaning;
// Apply a colored glow if the card has a known/learning status
const s = status[card.id] || null;
if (s === "know") {
flashcard.style.boxShadow = "0 0 15px rgba(46, 204, 113, 0.4)";
} else if (s === "learn") {
flashcard.style.boxShadow = "0 0 15px rgba(52, 152, 219, 0.4)";
} else {
flashcard.style.boxShadow = "none";
}
}
What does this code do?
The first 14 lines grab references to every HTML element the script needs to update. Caching these in variables avoids searching the DOM repeatedly.
The module-level variables track the app's state: which card is showing (index), whether the back is visible (showBack), and the learner's progress (status).
The init() function fetches cards.json using the Fetch API, parses the JSON response, then wires up event listeners and paints the first card. The renderCard() function reads the current card from the array and writes its kanji, readings, and meaning into the HTML elements.
You won't see a change yet. The next chunk adds the interaction functions that render() depends on.
Implement flip toggling, navigation, and keyboard support
The card needs to respond to clicks and key presses. The hookEvents() function attaches all the event listeners in one place, keeping the code organized.
- Add the event handling and flip/navigation helper functions below your existing code in script.js:
function hookEvents() {
// Navigate to previous card
prevBtn.addEventListener("click", () => {
if (index > 0) { index--; showBack = false; render(); }
});
// Navigate to next card
nextBtn.addEventListener("click", () => {
if (index < data.cards.length - 1) { index++; showBack = false; render(); }
});
// Flip via button
flipBtn.addEventListener("click", () => {
showBack = !showBack;
renderFlip();
});
// Flip via clicking the card itself
flashcard.addEventListener("click", () => {
showBack = !showBack;
renderFlip();
});
// Mark card status
knowBtn.addEventListener("click", () => setStatus("know"));
learnBtn.addEventListener("click", () => setStatus("learn"));
// Keyboard shortcuts: Space to flip, arrows to navigate
document.addEventListener("keydown", (e) => {
if (e.key === " ") { e.preventDefault(); showBack = !showBack; renderFlip(); }
if (e.key === "ArrowLeft") prevBtn.click();
if (e.key === "ArrowRight") nextBtn.click();
});
}
function renderFlip() {
// Toggle the CSS class that triggers the 3D rotation
if (showBack) {
flashcard.classList.add("flipped");
} else {
flashcard.classList.remove("flipped");
}
}
function updateButtons() {
// Disable Previous on first card, Next on last card
prevBtn.disabled = index === 0;
nextBtn.disabled = index === data.cards.length - 1;
}
What does this code do?
The hookEvents() function attaches click listeners to every button and a keydown listener to the document. When navigating, it resets showBack to false so you always see the front of the next card first.
The renderFlip() function adds or removes the flipped class on the flashcard element. Your CSS already has the rule .flashcard.flipped .flashcard-inner { transform: rotateY(180deg) } that handles the actual 3D rotation.
The updateButtons() function disables the Previous button when you're on the first card and the Next button when you're on the last card, preventing out-of-bounds navigation.
You won't see a change yet. The next chunk adds the progress functions that complete the app.
Implement Know/Still Learning status tracking with localStorage
The final piece is persistence. Without saving progress, the learner would lose all their Know/Still Learning marks every time they close the browser tab.
- Add the status tracking and localStorage functions below your existing code in script.js:
function setStatus(value) {
// Save the current card's status and re-render
const card = data.cards[index];
status[card.id] = value;
saveStatus(status);
render();
}
function updateProgress() {
// Count cards by status and update the progress bar
const total = data.cards.length;
const ids = data.cards.map(c => c.id);
let know = 0, learn = 0;
ids.forEach(id => {
if (status[id] === "know") know++;
if (status[id] === "learn") learn++;
});
const done = know + learn;
const percent = Math.round((done / total) * 100);
barEl.style.width = percent + "%";
progressTextEl.textContent = `Know: ${know} • Still learning: ${learn} • Reviewed: ${done}/${total} (${percent}%)`;
}
function loadStatus() {
// Read saved progress from localStorage
try {
return JSON.parse(localStorage.getItem("kanjiStatus") || "{}");
} catch {
return {};
}
}
function saveStatus(obj) {
// Write progress to localStorage
localStorage.setItem("kanjiStatus", JSON.stringify(obj));
}
What does this code do?
The setStatus() function stores the learner's choice by card ID (not array index). This ensures progress stays correct even if cards are reordered later.
The updateProgress() function counts how many cards are marked as "know" vs "learn", calculates a percentage, and updates both the green progress bar width and the text summary below it.
The loadStatus() and saveStatus() functions handle reading from and writing to localStorage under the key kanjiStatus. The try/catch in loadStatus() prevents the app from crashing if localStorage data is corrupted.
- Save script.js.
- Refresh your browser. You should see the first kanji (政) displayed on the card.
- Click the card or the Flip button. The card should rotate to reveal the readings and meaning on the back.
- Click Next → to move to the second card.
- Click ✓ Know. The card should glow green and the progress bar should update.
- Refresh the page. Your progress should still be there thanks to localStorage.
Card not showing or buttons not working?
- Open the browser's Developer Tools console (press F12 then click Console). Look for error messages that point to a specific line number in script.js.
- Make sure cards.json is in the same folder as script.js. The fetch path ./cards.json looks in the same directory.
- Check that every function name matches exactly. JavaScript is case-sensitive, so hookevents is not the same as hookEvents.
Why isn't my flashcard app loading or responding to clicks?
Why use card IDs instead of array indices?
Each card has a unique id field in the JSON. The status object stores progress by this ID rather than the card's position in the array.
If you later add more kanji or sort the deck differently, your progress follows the card, not its array index. This is the same pattern real spaced-repetition apps use.
✔️ Awesome, I've got everything!
Great. Double check you've saved script.js and confirmed the flip animation, navigation, and progress tracking all work.
ⓧ I'd like to double check the full code
Here is the complete script.js file. Compare yours against it line by line.
const metaEl = document.getElementById("meta");
const flashcard = document.getElementById("flashcard");
const flashcardInner = document.getElementById("flashcard-inner");
const kanjiDisplay = document.getElementById("kanji-display");
const onyomiDisplay = document.getElementById("onyomi-display");
const kunyomiDisplay = document.getElementById("kunyomi-display");
const meaningDisplay = document.getElementById("meaning-display");
const prevBtn = document.getElementById("prevBtn");
const nextBtn = document.getElementById("nextBtn");
const flipBtn = document.getElementById("flipBtn");
const knowBtn = document.getElementById("knowBtn");
const learnBtn = document.getElementById("learnBtn");
const barEl = document.getElementById("bar");
const progressTextEl = document.getElementById("progressText");
let data = null;
let index = 0;
let showBack = false;
let status = loadStatus();
init();
async function init() {
const res = await fetch("./cards.json", { cache: "no-store" });
data = await res.json();
hookEvents();
render();
}
function hookEvents() {
prevBtn.addEventListener("click", () => {
if (index > 0) { index--; showBack = false; render(); }
});
nextBtn.addEventListener("click", () => {
if (index < data.cards.length - 1) { index++; showBack = false; render(); }
});
flipBtn.addEventListener("click", () => {
showBack = !showBack;
renderFlip();
});
flashcard.addEventListener("click", () => {
showBack = !showBack;
renderFlip();
});
knowBtn.addEventListener("click", () => setStatus("know"));
learnBtn.addEventListener("click", () => setStatus("learn"));
document.addEventListener("keydown", (e) => {
if (e.key === " ") { e.preventDefault(); showBack = !showBack; renderFlip(); }
if (e.key === "ArrowLeft") prevBtn.click();
if (e.key === "ArrowRight") nextBtn.click();
});
}
function render() {
const total = data.cards.length;
metaEl.textContent = `Card ${index + 1} of ${total}`;
renderCard();
renderFlip();
updateButtons();
updateProgress();
}
function renderCard() {
const card = data.cards[index];
kanjiDisplay.textContent = card.kanji;
onyomiDisplay.textContent = card.onyomi;
kunyomiDisplay.textContent = card.kunyomi;
meaningDisplay.textContent = card.meaning;
const s = status[card.id] || null;
if (s === "know") {
flashcard.style.boxShadow = "0 0 15px rgba(46, 204, 113, 0.4)";
} else if (s === "learn") {
flashcard.style.boxShadow = "0 0 15px rgba(52, 152, 219, 0.4)";
} else {
flashcard.style.boxShadow = "none";
}
}
function renderFlip() {
if (showBack) {
flashcard.classList.add("flipped");
} else {
flashcard.classList.remove("flipped");
}
}
function updateButtons() {
prevBtn.disabled = index === 0;
nextBtn.disabled = index === data.cards.length - 1;
}
function setStatus(value) {
const card = data.cards[index];
status[card.id] = value;
saveStatus(status);
render();
}
function updateProgress() {
const total = data.cards.length;
const ids = data.cards.map(c => c.id);
let know = 0, learn = 0;
ids.forEach(id => {
if (status[id] === "know") know++;
if (status[id] === "learn") learn++;
});
const done = know + learn;
const percent = Math.round((done / total) * 100);
barEl.style.width = percent + "%";
progressTextEl.textContent = `Know: ${know} • Still learning: ${learn} • Reviewed: ${done}/${total} (${percent}%)`;
}
function loadStatus() {
try {
return JSON.parse(localStorage.getItem("kanjiStatus") || "{}");
} catch {
return {};
}
}
function saveStatus(obj) {
localStorage.setItem("kanjiStatus", JSON.stringify(obj));
}
Your kanji flashcard app is fully interactive. You can flip cards, navigate through the deck with buttons or keyboard shortcuts, mark your confidence level, and your progress persists across sessions. In the Secret Mission, you'll add category filtering to study kanji by theme.
Secret mission
Add Category Filtering
Your flashcard app works great, but studying all 20 kanji at once can be overwhelming. What if you could focus on just one theme at a time? In this bonus challenge, you'll add a dropdown filter that lets you study kanji grouped by category.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Your project folder containing index.html, style.css, script.js, and cards.json.
- Browser localStorage data (your Know/Still Learning progress stored under the kanjiStatus key).
Keep everything running
No action needed. Choose this if you want to keep studying kanji or plan to extend your card set.
- Leave the project folder as-is. Open index.html in your browser anytime to study.
- Your progress is saved in localStorage and will persist until you clear your browser data.
- To extend, add more kanji entries to cards.json. The full JLPT N3 list has approximately 370 characters.
Pause - I'll come back to this later
Since this project is just files on your computer, there is nothing to pause. Your files stay where they are and your browser keeps your localStorage progress automatically.
- Close the project in VS Code to free up editor resources.
- Close the browser tab showing your flashcard app.
- When you come back, open index.html in your browser again. Your Know/Still Learning progress will still be there.
Delete - I don't want to use this again
Remove all project files and clear your saved progress from the browser.
- Delete your project folder by running this command in Terminal (macOS/Linux):
cd ~/Desktop
rm -rf kanji-flashcards
On Windows, run this in PowerShell instead:
cd ~\Desktop
Remove-Item -Recurse -Force kanji-flashcards
- Clear your localStorage progress by opening your browser's Developer Console and running:
localStorage.removeItem("kanjiStatus")
- To open the console, press Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows) in Chrome or Edge.
Nice Work!
Nice Work!
You did it. You built your own JLPT N3 kanji study tool from scratch using nothing but vanilla HTML, CSS, and JavaScript.
What you learned:
- Built a complete flashcard interface with smooth 3D flip animations using CSS transforms, perspective, and backface-visibility.
- Loaded kanji data from a JSON file and rendered it dynamically using JavaScript DOM manipulation with keyboard navigation support.
- Implemented progress tracking with localStorage persistence so your study progress saves automatically across browser sessions.
- Added category filtering to organize your study sessions by theme.
Ready to quiz yourself?