JavaScript

Data viz & forms

Turn numbers into charts and make forms that actually do something. Below: a bar, line, and donut chart (pure CSS/SVG, no library), animated stat counters, plus real forms — submitting, multi-step, and file upload with a live preview. Everything works right here.

Chart 1

Bar chart (pure CSS)

No library needed — each bar is a div whose height is the value, animated up on load.

40
Mon
75
Tue
55
Wed
90
Thu
65
Fri
HTML
<div class="bar-chart">
  <div class="bar-col"><div class="bar" style="height:75%"></div><span>Tue</span></div>
  <!-- one column per value; height = the value % -->
</div>
CSS
.bar-chart { display: flex; align-items: flex-end; gap: 16px; height: 180px; }
.bar-col   { flex: 1; height: 100%; display: flex; flex-direction: column; justify-content: flex-end; }
.bar { background: linear-gradient(180deg,#2dd4bf,#14b8a6); border-radius: 8px 8px 0 0;
  transform: scaleY(0); transform-origin: bottom; animation: grow .9s ease forwards; }
@keyframes grow { to { transform: scaleY(1); } }   /* bars rise on load */
Chart 2

Line chart (SVG)

A single <polyline> plots the points; a stroke animation draws it left to right.

HTML / SVG
<svg viewBox="0 0 300 140">
  <polyline fill="none" stroke="#14b8a6" stroke-width="3"
    points="0,110 60,70 120,90 180,30 240,55 300,20" />
</svg>
<!-- each "x,y" is a data point; lower y = higher value -->
CSS (draw animation)
polyline { stroke-dasharray: 600; stroke-dashoffset: 600;
           animation: draw 1.6s ease forwards; }
@keyframes draw { to { stroke-dashoffset: 0; } }   /* "draws" the line */
Chart 3

Donut chart & stat counters

A conic-gradient makes a donut with no SVG, and a little JS counts numbers up from zero for that dashboard "wow". Press play:

Direct — 62% Social — 25% Other — 13%
0
Users
0
% uptime
0
Sales
HTML
<div class="donut-wrap">
  <div class="donut"></div>
  <div class="legend">
    <span><i style="background:#14b8a6;"></i> Direct — 62%</span>
    <span><i style="background:#6366f1;"></i> Social — 25%</span>
    <span><i style="background:#e5e7eb;"></i> Other — 13%</span>
  </div>
</div>
<div class="counters">
  <div class="counter"><div class="n" data-to="12480">0</div><div class="l">Users</div></div>
  <div class="counter"><div class="n" data-to="98">0</div><div class="l">% uptime</div></div>
  <div class="counter"><div class="n" data-to="340">0</div><div class="l">Sales</div></div>
</div>
CSS
.donut-wrap { display: flex; align-items: center; gap: 24px; flex-wrap: wrap; }
.donut { width: 130px; height: 130px; border-radius: 50%;
  background: conic-gradient(#14b8a6 0 62%, #6366f1 0 87%, #e5e7eb 0);   /* slices add up */
  display: flex; align-items: center; justify-content: center; }
.donut::before { content: ''; width: 86px; height: 86px; background: #fff; border-radius: 50%; }  /* hole */
.legend { display: grid; gap: 8px; font-size: .85rem; }
.legend span { display: flex; align-items: center; gap: 8px; }
.legend i { width: 12px; height: 12px; border-radius: 3px; }
.counters { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 16px; text-align: center; }
.counter .n { font-size: 2.2rem; font-weight: 800; color: #0f766e; font-variant-numeric: tabular-nums; }
.counter .l { font-size: .8rem; color: #6b7280; }
JavaScript (count up)
document.querySelectorAll('[data-to]').forEach(el => {
  const end = +el.dataset.to; let n = 0;
  const step = end / 60;                          // ~1 second at 60fps
  const t = setInterval(() => {
    n += step;
    if (n >= end) { n = end; clearInterval(t); }
    el.textContent = Math.floor(n).toLocaleString();
  }, 16);
});
Forms 1

Make a form actually submit

A plain HTML form can't email you by itself — you need a backend. The easiest for a capstone: a free service like Formspree or Netlify Forms. You just point the form's action at them.

HTML — Formspree (no server code!)
<form action="https://formspree.io/f/YOUR_ID" method="POST">
  <input name="name" type="text" required>
  <input name="email" type="email" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send message</button>
</form>
<!-- sign up free at formspree.io, paste your form ID -->
HTML — Netlify Forms
<form name="contact" method="POST" data-netlify="true">
  …same fields…
</form>
<!-- deploy to Netlify; submissions appear in your dashboard -->
Forms 2

Multi-step form

Break a long form into friendly steps with a progress bar. Try it — Next / Back move between steps, and the bar fills up.


All done — submitted!
HTML
<form class="form" id="msForm" onsubmit="return false">
  <div class="steps-bar" id="stepsBar"><span class="on"></span><span></span><span></span></div>
  <div class="step active"><label>1 · Your name</label><input type="text" placeholder="Ada Lovelace"></div>
  <div class="step"><label>2 · Your email</label><input type="email" placeholder="you@example.com"></div>
  <div class="step"><label>3 · A message</label><textarea rows="3" placeholder="Hi…"></textarea></div>
  <div class="step-nav"><button class="btn ghost" type="button" id="msBack">← Back</button><button class="btn" type="button" id="msNext">Next →</button></div>
  <div class="msuccess" id="msDone">✓ All done — submitted!</div>
</form>
CSS
.form { display: grid; gap: 12px; max-width: 420px; }
.form label { font-size: .78rem; font-weight: 700; color: #374151; }
.form input, .form textarea { width: 100%; padding: 11px 13px; border: 1px solid #d1d5db; border-radius: 9px; font: inherit; }
.btn { border: none; background: #14b8a6; color: #fff; font-weight: 700; padding: 11px 20px; border-radius: 9px; cursor: pointer; }
.btn.ghost { background: #f0fdfa; color: #0f766e; }
.steps-bar { display: flex; gap: 6px; margin-bottom: 16px; }
.steps-bar span { flex: 1; height: 6px; border-radius: 3px; background: #e5e7eb; }
.steps-bar span.on { background: #14b8a6; }
.step { display: none; }
.step.active { display: grid; gap: 12px; }
.step-nav { display: flex; justify-content: space-between; margin-top: 6px; }
.msuccess { text-align: center; color: #0f766e; font-weight: 700; padding: 20px; display: none; }
.msuccess.show { display: block; }
JavaScript
const form  = document.getElementById('msForm');
const steps = form.querySelectorAll('.step');
const bars  = document.querySelectorAll('#stepsBar span');
const back  = document.getElementById('msBack');
const next  = document.getElementById('msNext');
const done  = document.getElementById('msDone');
let step = 0;

function show(i) {
  steps[step].classList.remove('active');
  step = Math.max(0, Math.min(steps.length - 1, i));
  steps[step].classList.add('active');
  for (let k = 0; k <= step; k++) bars[k].classList.add('on');   // fill progress
  back.style.visibility = step === 0 ? 'hidden' : 'visible';
  next.textContent = step === steps.length - 1 ? 'Submit' : 'Next →';
}

next.onclick = () => {
  if (step === steps.length - 1) {                                // last step → finish
    form.querySelectorAll('.step, .step-nav').forEach(e => e.style.display = 'none');
    done.classList.add('show');
  } else show(step + 1);
};
back.onclick = () => show(step - 1);
show(0);
Forms 3

File upload with live preview

Let users pick an image and see it instantly before uploading — FileReader reads the file in the browser. Click the box and choose any image.

Click to choose an image
Preview
HTML
<div class="drop" id="drop">Click to choose an image</div>
<input type="file" id="fileInput" accept="image/*" hidden>
<div class="preview" id="preview"><img id="previewImg" alt="Preview"></div>
CSS
.drop { border: 2px dashed #99f6e4; border-radius: 12px; padding: 26px; text-align: center; color: #0f766e; cursor: pointer; }
.drop i { font-size: 2rem; display: block; margin-bottom: 6px; }
.preview { margin-top: 12px; display: none; }
.preview img { max-width: 100%; max-height: 160px; border-radius: 10px; }
.preview.show { display: block; }
JavaScript
const drop = document.getElementById('drop');
const file = document.getElementById('fileInput');
const preview = document.getElementById('preview');
const previewImg = document.getElementById('previewImg');

drop.onclick = () => file.click();              // open the file picker
file.onchange = () => {
  const f = file.files[0];
  if (!f) return;
  const reader = new FileReader();
  reader.onload = e => {                         // read as a data URL → show it
    previewImg.src = e.target.result;
    preview.classList.add('show');
    drop.textContent = f.name;
  };
  reader.readAsDataURL(f);
};
Capstone-ready combos: a dashboard = counters + a chart + a data table; a contact page = a real submitting form; a profile editor = file upload + form. Pull live numbers from Data & APIs and feed them into these charts.
Recap

The takeaway

Charts need no library: div heights for bars, an SVG polyline for lines, a conic-gradient for donuts, and a tiny loop to count numbers up. For forms, point action at Formspree or Netlify to actually receive submissions, split long ones into steps, and use FileReader for instant upload previews.