Flashcard Learning App with HTML, CSS & JavaScript
20 DAYS 20 PROJECT CHALLENGE
Day #9
Project Overview
The Flashcard Learning App is a beginner-friendly project built with HTML, CSS, and JavaScript. It allows user to create a flashcard with the question on one side and the answer on the other. User can view the answer by clicking on a card and flipping it with a nice 3D animation. Moreover, the user can add, edit, delete and store flashcards in the browser under the local storage tab to save them and have access to them even after refreshing the page.
This project is useful for beginners who want to practice DOM manipulation, event handling, LocalStorage, dynamic element creation, CSS animations, and file handling while building an interactive learning application.
Key Features
- Create Flashcards: Users can create new flashcards by entering a question and its answer. Each saved flashcard is then added to the learning area dynamically.
- Question and Answer: All cards can have a question on one side and an answer on the other. This structure makes the application useful for studying different topics.
- 3D Flip Animation: You can flip the card by clicking on it to see the answer. CSS 3D transforms are used for the flip animation effect.
- Reveal Answer: Each card has a Reveal button to show the answer right away. So, you do not need to flip the card to see the answer.
- Edit Flashcards: You can edit the information of a card if you want to change the question or answer. The current information of the card will be loaded to the form when you edit it. So, the changes you make will be saved.
- Delete Flashcards: You can delete a card by clicking the Delete button. Also, a confirmation prompt is shown before deleting a card. So, you can delete a card permanently.
- LocalStorage: All cards will be saved in your browser with LocalStorage. The saved cards will be available until you close the browser.
- Import Flashcards: It will be possible to import the saved cards to the app from a JSON file. So, you can load the cards you saved earlier.
- Export Flashcards: You can export the cards you saved as a JSON file. So, you can make a backup of your saved cards or transfer them to another browser.
- Starter Cards: Some sample cards can be shown when the app is opened for the first time. So, you can see how the app is used.
- Empty State: An empty state message will be shown if there are no cards. So, the user will know that they can add a new card.
- Responsive: The app uses a responsive grid layout to show the cards. So, the app can be used on different devices and screen sizes.
What You'll Learn
- Learn how to dynamically display multiple items on a webpage.
- Understand how CSS 3D transforms can create a card flip animation.
- Learn how to add and remove CSS classes dynamically.
- Practice storing data in the browser using LocalStorage.
- Understand how JSON.stringify() and JSON.parse() work.
- Learn how to add, edit, and delete items from an array.
- Practice using array methods such as
filter(),find(), andfindIndex(). - Understand how to create and download files using JavaScript.
- Learn how to import and read JSON files using FileReader.
HTML Code
The HTML code creates the main structure of the Flashcard Learning App. The complete application is placed inside the .wrap container, which contains the heading, control buttons, flashcard form, card area, and footer. At the top, buttons such as New Card, Export, Import, and Clear All provide controls for managing flashcards. The #formArea section contains two textarea fields, #question and #answer, where users can enter the content of a new flashcard. The Save Card button saves the entered information, while the Cancel button hides the form.
The #grid element acts as the main area where JavaScript dynamically creates and displays flashcards. The #empty element displays a message when no flashcards are available. Finally, the script.js file is linked at the bottom of the <body> so JavaScript can access the HTML elements and provide the application’s functionality.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Day 9 — Flashcard Learning App</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="wrap">
<header>
<div>
<h1>Day 9 — Flashcard Learning App</h1>
<p>Create and flip flashcards. (Uses LocalStorage + CSS 3D flip)</p>
</div>
<div class="controls">
<button id="addBtn">+ New Card</button>
<button id="exportBtn" class="outline">Export</button>
<button id="importBtn" class="outline">Import</button>
<button id="clearBtn" class="outline">Clear All</button>
</div>
</header>
<section id="formArea" class="form-card" style="display:none">
<label for="question">Question</label>
<textarea id="question" placeholder="Type the question — spaces and new lines are preserved"></textarea>
<label for="answer" style="margin-top:8px">Answer</label>
<textarea id="answer" placeholder="Type the answer — spaces and new lines are preserved"></textarea>
<div class="row" style="margin-top:10px">
<div style="display:flex;gap:8px">
<button id="saveCard">Save Card</button>
<button id="cancelSave" class="ghost">Cancel</button>
</div>
<div style="display:flex;justify-content:flex-end">
<small style="color:var(--muted)">Cards are stored in LocalStorage</small>
</div>
</div>
</section>
<main id="cardsWrap">
<div id="grid" class="grid">
<!-- cards appear here -->
</div>
<div id="empty" class="empty" style="display:none">
No flashcards yet — click "New Card" to add some. Try pressing a card to flip it.
</div>
</main>
<footer>
Tip: Click a card to flip. Use the small action buttons to reveal, edit or delete.
</footer>
</div>
<script src="script.js"></script>
</body>
</html>
CSS Code
The CSS code controls the appearance, layout, and flip animation of the Flashcard Learning App. The .wrap class controls the maximum width and alignment of the main application, header and .controls sections use Flexbox to organize the heading and control buttons, .form-card class styles the area where users enter questions and answers, .grid class uses CSS Grid to display multiple flashcards in a responsive layout. Each .card uses the perspective property, while .card-inner uses transform-style: preserve-3d and a transition effect to create the 3D flip animation. When JavaScript adds the .is-flipped class, the card rotates using rotateY(180deg).
The .front and .back classes represent the two sides of the flashcard. The backface-visibility property hides the reverse side while the card is facing the user. The .q and .a selectors use white-space: pre-wrap so spaces and line breaks entered by the user remain visible.
:root {
--bg: #0f172a;
--card: #0b1220;
--accent: #f59e0b;
--muted: #94a3b8;
--glass: rgba(255, 255, 255, 0.04);
--radius: 14px;
--gap: 16px;
}
* {
box-sizing: border-box
}
html,
body {
height: 100%;
margin: 0;
font-family: Inter, system-ui, Segoe UI, Roboto, 'Helvetica Neue', Arial
}
body {
background: #002252;
color: #e6eef8;
padding: 28px
}
.wrap {
max-width: 900px;
margin: 0 auto
}
header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px
}
header h1 {
font-size: 20px;
margin: 0
}
header p {
color: var(--muted);
margin: 0;
font-size: 13px
}
.controls {
display: flex;
gap: 10px
}
button {
background: var(--accent);
border: none;
padding: 8px 12px;
border-radius: 10px;
color: #08101a;
font-weight: 600;
cursor: pointer
}
.outline {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.06);
color: var(--muted);
padding: 8px 12px
}
/* form area */
.form-card {
background: var(--card);
padding: 14px;
border-radius: 12px;
margin-bottom: 18px;
box-shadow: 0 6px 18px rgba(2, 6, 23, 0.6)
}
.form-row {
display: flex;
gap: 10px
}
textarea {
width: 100%;
min-height: 70px;
padding: 8px;
border-radius: 10px;
background: var(--glass);
border: 1px solid rgba(255, 255, 255, 0.03);
color: inherit;
resize: vertical
}
label {
font-size: 13px;
color: var(--muted);
display: block;
margin-bottom: 6px
}
.row {
display: grid;
grid-template-columns: 1fr 120px;
gap: 10px;
align-items: end
}
/* cards grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 18px
}
/* flip card */
.card {
perspective: 1200px
}
.card-inner {
position: relative;
width: 100%;
height: 180px;
transform-style: preserve-3d;
transition: transform 600ms cubic-bezier(.2, .9, .3, 1)
}
.card.is-flipped .card-inner {
transform: rotateY(180deg)
}
.face {
position: absolute;
inset: 0;
border-radius: 12px;
padding: 14px;
backface-visibility: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
box-shadow: 0 6px 18px rgba(2, 6, 23, 0.6)
}
.front {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
border: 1px solid rgba(255, 255, 255, 0.03)
}
.back {
background: linear-gradient(180deg, rgba(5, 10, 20, 0.9), rgba(2, 6, 12, 0.9));
transform: rotateY(180deg);
border: 1px solid rgba(255, 255, 255, 0.03)
}
.q,
.a {
font-size: 15px;
line-height: 1.4;
color: #e6eef8;
margin: 0
}
/* preserve whitespace and new lines exactly as user types */
.q,
.a {
white-space: pre-wrap
}
.meta {
margin-top: 10px;
display: flex;
gap: 8px;
align-items: center
}
.meta small {
color: var(--muted);
font-size: 12px
}
.card-actions {
display: flex;
gap: 8px;
margin-top: 10px
}
.ghost {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.04);
color: var(--muted);
padding: 6px 8px;
border-radius: 8px;
font-size: 13px
}
.empty {
padding: 40px;
border-radius: 12px;
text-align: center;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
color: var(--muted)
}
footer {
margin-top: 20px;
color: var(--muted);
font-size: 13px;
text-align: center
}
@media (max-width:520px) {
.row {
grid-template-columns: 1fr
}
} Javascript Code
The JavaScript code provides the main functionality of the Flashcard Learning App. First, the LS_KEY variable defines the name used to store flashcard data in LocalStorage, loadCards() function retrieves saved cards from the browser and converts the JSON data back into a JavaScript array. The saveCards() function converts the flashcard array into JSON and stores it in LocalStorage, render() function is responsible for displaying the flashcards on the page. It creates the required HTML elements dynamically for each card, including the question, answer, Reveal button, Edit button, and Delete button. Clicking a card adds or removes the .is-flipped class, which activates the CSS flip animation.
The openFormForNew() function prepares an empty form for creating a new flashcard. The openFormForEdit() function finds an existing card and places its question and answer inside the form for editing. When the Save Card button is clicked, the application either updates an existing card or creates a new one. Export button creates a JSON file containing the saved flashcards. The Import button uses the FileReader API to read a selected JSON file and save its cards to LocalStorage. The Clear All button removes the stored flashcards after confirmation. Finally, render() runs when the page loads so the saved flashcards appear immediately.
// --- Utilities ---
const LS_KEY = 'flashcards_day9_v1'
const qs = sel => document.querySelector(sel)
const qsa = sel => Array.from(document.querySelectorAll(sel))
// sample starter data if none exists
const starter = [
{id: Date.now()+1, q: 'HTML stands for?', a: 'HyperText Markup Language'},
{id: Date.now()+2, q: 'What does CSS control?', a: 'Presentation: layout, colors, fonts\nUse selectors to target elements.'}
]
// load from storage
function loadCards(){
try{
const raw = localStorage.getItem(LS_KEY)
if(!raw) return starter.slice()
const parsed = JSON.parse(raw)
if(!Array.isArray(parsed)) return []
return parsed
}catch(e){console.error('load error', e); return []}
}
function saveCards(cards){
localStorage.setItem(LS_KEY, JSON.stringify(cards))
}
// --- DOM rendering ---
const grid = qs('#grid')
const empty = qs('#empty')
function render(){
const cards = loadCards()
grid.innerHTML = ''
if(cards.length === 0){ empty.style.display = 'block'; return }
empty.style.display = 'none'
cards.forEach(card => {
const wrap = document.createElement('div')
wrap.className = 'card'
const inner = document.createElement('div')
inner.className = 'card-inner'
const front = document.createElement('div')
front.className = 'face front'
const qEl = document.createElement('p')
qEl.className = 'q'
// set as text to preserve spaces and newlines (pre-wrap CSS will show them)
qEl.innerText = card.q
front.appendChild(qEl)
const meta = document.createElement('div')
meta.className = 'meta'
const small = document.createElement('small')
small.innerText = 'Click to flip'
meta.appendChild(small)
const actions = document.createElement('div')
actions.className = 'card-actions'
const reveal = document.createElement('button')
reveal.className = 'ghost'
reveal.innerText = 'Reveal'
reveal.addEventListener('click', e=>{
e.stopPropagation()
wrap.classList.add('is-flipped')
})
const edit = document.createElement('button')
edit.className = 'ghost'
edit.innerText = 'Edit'
edit.addEventListener('click', e=>{
e.stopPropagation()
openFormForEdit(card.id)
})
const del = document.createElement('button')
del.className = 'ghost'
del.innerText = 'Delete'
del.addEventListener('click', e=>{
e.stopPropagation()
if(confirm('Delete this card?')){
const after = loadCards().filter(c=>c.id !== card.id)
saveCards(after)
render()
}
})
actions.appendChild(reveal)
actions.appendChild(edit)
actions.appendChild(del)
front.appendChild(meta)
front.appendChild(actions)
const back = document.createElement('div')
back.className = 'face back'
const aEl = document.createElement('p')
aEl.className = 'a'
// preserve spaces/newlines using innerText
aEl.innerText = card.a
back.appendChild(aEl)
// inner click flips
wrap.addEventListener('click', ()=>{
wrap.classList.toggle('is-flipped')
})
inner.appendChild(front)
inner.appendChild(back)
wrap.appendChild(inner)
grid.appendChild(wrap)
})
}
// --- Form handling ---
const addBtn = qs('#addBtn')
const formArea = qs('#formArea')
const saveBtn = qs('#saveCard')
const cancelBtn = qs('#cancelSave')
const question = qs('#question')
const answer = qs('#answer')
let editingId = null
addBtn.addEventListener('click', ()=>{
openFormForNew()
})
cancelBtn.addEventListener('click', ()=>{
hideForm()
})
function openFormForNew(){
editingId = null
question.value = ''
answer.value = ''
formArea.style.display = 'block'
question.focus()
}
function openFormForEdit(id){
const cards = loadCards()
const found = cards.find(c=>c.id===id)
if(!found) return alert('Card not found')
editingId = id
question.value = found.q
answer.value = found.a
formArea.style.display = 'block'
question.focus()
}
function hideForm(){
formArea.style.display = 'none'
editingId = null
}
saveBtn.addEventListener('click', ()=>{
const qtxt = question.value.trimEnd() // keep internal spaces/newlines, remove trailing blank lines
const atxt = answer.value.trimEnd()
if(!qtxt){ alert('Please type a question'); return }
const cards = loadCards()
if(editingId){
const idx = cards.findIndex(c=>c.id===editingId)
if(idx>-1){ cards[idx].q = qtxt; cards[idx].a=atxt }
} else {
cards.unshift({id: Date.now(), q: qtxt, a: atxt})
}
saveCards(cards)
hideForm()
render()
})
// --- Export / Import / Clear ---
const exportBtn = qs('#exportBtn')
const importBtn = qs('#importBtn')
const clearBtn = qs('#clearBtn')
exportBtn.addEventListener('click', ()=>{
const data = localStorage.getItem(LS_KEY) || '[]'
// download as file
const blob = new Blob([data], {type:'application/json'})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url; a.download = 'flashcards-export.json'
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url)
})
importBtn.addEventListener('click', ()=>{
const input = document.createElement('input')
input.type = 'file'
input.accept = 'application/json'
input.onchange = () => {
const f = input.files[0]
if(!f) return
const reader = new FileReader()
reader.onload = e => {
try{
const parsed = JSON.parse(e.target.result)
if(!Array.isArray(parsed)) throw new Error('invalid')
saveCards(parsed)
render()
alert('Imported ' + parsed.length + ' cards')
}catch(err){alert('Import failed: invalid file')}
}
reader.readAsText(f)
}
input.click()
})
clearBtn.addEventListener('click', ()=>{
if(confirm('Delete all flashcards?')){
localStorage.removeItem(LS_KEY)
render()
}
})
// initial render
render()
// expose for debugging
window._flashcards = {loadCards, saveCards, render}
Related Projects
Day 7 : Expense Tracker
Track income and expenses, and calculate the total balance.
Concepts: LocalStorage, array methods (map, reduce).
Day 11 : Drum Kit
Play drum sounds when clicking buttons or pressing keys.
Concepts: Keyboard events, Audio API.
Day 12 : QR Code Generator
Generates a QR code from text input.
Concepts: Third-party API (QRServer API), fetch().