Flashcard Learning App with HTML, CSS & JavaScript
20 DAYS 20 PROJECT CHALLENGE
Day #9
Project Overview
A simple Flashcard Learning App that allows users to create, flip, and study flashcards interactively. Each card stores a question on the front and an answer on the back, using a smooth 3D flip animation. The app supports adding, editing, deleting, importing, and exporting cards — all stored in the browser using LocalStorage. This project demonstrates DOM manipulation, CSS 3D transforms, event handling, and LocalStorage persistence, making it ideal for learning frontend fundamentals.
Key Features
- Add, edit, delete flashcards with clean UI controls.
- 3D flip animation on click (front ↔ back).
- Preserve spaces and new lines exactly as typed.
- LocalStorage support to save cards permanently.
- Import & Export flashcards as JSON files.
- “Reveal” button for instant answer view without flipping manually.
- Responsive card grid layout for all screen sizes.
- Starter sample cards added automatically on first load.
- Supports multiple lines and formatted text using
white-space: pre-wrap.
HTML Code
<!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
: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
// --- 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().