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}
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

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().