JavaScript

Interactive features

The widgets that make a capstone feel alive — tabs, accordions, a carousel, live search, a modal, a countdown, and instant form validation. Each one is working below, with the HTML, CSS, and JavaScript to drop straight into your project.

Widget 1

Tabs

Click a tab to switch panels. One class toggle does it all.

A quick summary of the product goes here.
Bullet out the features in this panel.
Show your plans and prices here.
HTML
<div class="tabs">
  <button class="tab active" data-tab="t1">Overview</button>
  <button class="tab" data-tab="t2">Features</button>
  <button class="tab" data-tab="t3">Pricing</button>
</div>
<div class="panel active" id="t1">A quick summary of the product goes here.</div>
<div class="panel" id="t2">Bullet out the features in this panel.</div>
<div class="panel" id="t3">Show your plans and prices here.</div>
JavaScript
document.querySelectorAll('.tab').forEach(tab => {
  tab.addEventListener('click', () => {
    document.querySelector('.tab.active').classList.remove('active');
    document.querySelector('.panel.active').classList.remove('active');
    tab.classList.add('active');
    document.getElementById(tab.dataset.tab).classList.add('active');
  });
});
CSS
.tabs { display: flex; gap: 4px; border-bottom: 2px solid #eee; }
.tab  { border: none; background: none; padding: 10px 16px; font: inherit; font-weight: 600;
        color: #6b7280; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; }
.tab.active   { color: #f59e0b; border-bottom-color: #f59e0b; }   /* highlight the active tab */
.panel        { display: none; }
.panel.active { display: block; }                                 /* show only the active panel */
Widget 2

Accordion / FAQ

Click a question to expand its answer — smooth height transition, only one open at a time.

A final project that shows off everything you've learned.

Yes! Copy any widget on this page into your project.

No — this is plain HTML, CSS, and JavaScript.

HTML
<div class="acc-item">
  <button class="acc-q">What is a capstone? <i class="ph-bold ph-plus ico"></i></button>
  <div class="acc-a"><p>A final project that shows off everything you've learned.</p></div>
</div>
<!-- repeat .acc-item for each question -->
JavaScript
document.querySelectorAll('.acc-q').forEach(q => {
  q.addEventListener('click', () => {
    const item = q.parentElement;
    const open = item.classList.contains('open');
    document.querySelectorAll('.acc-item').forEach(i => {       // close all
      i.classList.remove('open');
      i.querySelector('.acc-a').style.maxHeight = null;
    });
    if (!open) {                                                // open the clicked one
      item.classList.add('open');
      const a = item.querySelector('.acc-a');
      a.style.maxHeight = a.scrollHeight + 'px';
    }
  });
});
CSS
.acc-a { max-height: 0; overflow: hidden; transition: max-height .3s ease; }   /* JS sets the px height */
Widget 3

Image carousel

Prev/next arrows and clickable dots slide the track with a CSS transform.

HTML
<div class="carousel">
  <div class="car-view"><div class="car-track" id="carTrack">
    <div class="car-slide cs1">Slide 1</div>
    <div class="car-slide cs2">Slide 2</div>
    <div class="car-slide cs3">Slide 3</div>
  </div></div>
  <div class="car-ctrl">
    <button id="carPrev" aria-label="Previous"><i class="ph-bold ph-caret-left"></i></button>
    <div class="car-dots" id="carDots"></div>
    <button id="carNext" aria-label="Next"><i class="ph-bold ph-caret-right"></i></button>
  </div>
</div>
JavaScript
const track = document.getElementById('carTrack');
const dots  = document.getElementById('carDots');
const n = track.children.length;
let i = 0;

for (let k = 0; k < n; k++) {                       // build one dot per slide
  const dot = document.createElement('span');
  dot.dataset.i = k;
  dots.appendChild(dot);
}
function render() {
  track.style.transform = `translateX(${-i * 100}%)`;            // slide the track
  [...dots.children].forEach((d, idx) => d.classList.toggle('active', idx === i));
}
function go(x) { i = (x + n) % n; render(); }                    // wrap around

document.getElementById('carPrev').onclick = () => go(i - 1);
document.getElementById('carNext').onclick = () => go(i + 1);
dots.addEventListener('click', e => { if (e.target.dataset.i) go(+e.target.dataset.i); });
render();
CSS
.carousel { max-width: 380px; margin: 0 auto; }
.car-view { overflow: hidden; border-radius: 12px; }
.car-track { display: flex; transition: transform .5s cubic-bezier(.25,.8,.25,1); }
.car-slide { min-width: 100%; height: 200px; display: flex; align-items: center;
  justify-content: center; color: #fff; font-size: 1.4rem; font-weight: 800; }
.cs1 { background: linear-gradient(135deg,#f59e0b,#ef4444); }   /* each slide a colour */
.cs2 { background: linear-gradient(135deg,#3b82f6,#8b5cf6); }
.cs3 { background: linear-gradient(135deg,#10b981,#06b6d4); }
.car-ctrl { display: flex; align-items: center; justify-content: center; gap: 14px; margin-top: 12px; }
.car-ctrl button { border: none; background: #1d1d1f; color: #fff; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; }
.car-dots { display: flex; gap: 6px; }
.car-dots span { width: 9px; height: 9px; border-radius: 50%; background: #d1d5db; cursor: pointer; }
.car-dots span.active { background: #f59e0b; width: 22px; border-radius: 5px; }   /* current slide */
Widget 4

Live search & filter

Type to instantly filter a list — the single most useful interaction for any data-heavy capstone.

  • Apple
  • Banana
  • Cherry
  • Grapefruit
  • Mango
  • Orange
  • Strawberry
HTML
<input class="filter-input" id="filterInput" type="search" placeholder="Search fruits…">
<ul class="filter-list" id="filterList">
  <li>Apple</li><li>Banana</li><li>Cherry</li><li>Mango</li><li>Orange</li>
</ul>
<div class="filter-empty" id="filterEmpty" style="display:none;">No matches.</div>
JavaScript
const input = document.getElementById('filterInput');
const list  = document.getElementById('filterList');
const empty = document.getElementById('filterEmpty');

input.addEventListener('input', () => {
  const q = input.value.toLowerCase();
  let shown = 0;
  list.querySelectorAll('li').forEach(li => {
    const match = li.textContent.toLowerCase().includes(q);
    li.style.display = match ? '' : 'none';        // hide non-matches
    if (match) shown++;
  });
  empty.style.display = shown ? 'none' : 'block';   // "no results" state
});
CSS
.filter-input { width: 100%; padding: 11px 14px; border: 1px solid #d1d5db; border-radius: 9px; }
.filter-input:focus { outline: none; border-color: #f59e0b; }
.filter-list { list-style: none; display: grid; gap: 6px; }
.filter-list li { padding: 10px 14px; background: #f6f7f9; border-radius: 8px; }
Widget 5

Modal dialog

Opens on click, closes on the button, the backdrop, or the Escape key.

HTML
<button class="open-modal" id="openModal">Open dialog</button>
<div class="modal-back" id="modalBack">
  <div class="modal">
    <h4>Welcome!</h4>
    <p>This is a modal dialog. Click the backdrop, the button, or press Esc to close.</p>
    <button class="close" id="closeModal">Got it</button>
  </div>
</div>
JavaScript
const back     = document.getElementById('modalBack');
const openBtn  = document.getElementById('openModal');
const closeBtn = document.getElementById('closeModal');

openBtn.onclick  = () => back.classList.add('open');
closeBtn.onclick = () => back.classList.remove('open');
back.onclick = e => { if (e.target === back) back.classList.remove('open'); };  // click backdrop
document.addEventListener('keydown', e => { if (e.key === 'Escape') back.classList.remove('open'); });
CSS
.open-modal { border: none; background: #f59e0b; color: #fff; font-weight: 700; padding: 12px 22px; border-radius: 9px; cursor: pointer; }
.modal-back { position: fixed; inset: 0; background: rgba(0,0,0,.5);
  display: none; align-items: center; justify-content: center; z-index: 9999; padding: 20px; }
.modal-back.open { display: flex; }            /* toggled by JS */
.modal { background: #fff; color: #1d1d1f; border-radius: 16px; padding: 28px; max-width: 360px; box-shadow: 0 24px 60px rgba(0,0,0,.4); }
.modal h4 { font-size: 1.2rem; margin-bottom: 8px; }
.modal p { color: #6b7280; font-size: .9rem; margin-bottom: 16px; }
.modal .close { border: none; background: #1d1d1f; color: #fff; padding: 10px 18px; border-radius: 8px; cursor: pointer; font-weight: 600; }
Widget 6

Countdown timer

Counts down to a target date — perfect for event or launch pages. This one targets 30 days out.

00
Days
00
Hours
00
Mins
00
Secs
HTML
<div class="countdown" id="countdown">
  <div class="cd-unit"><div class="num" data-d>00</div><div class="lbl">Days</div></div>
  <div class="cd-unit"><div class="num" data-h>00</div><div class="lbl">Hours</div></div>
  <div class="cd-unit"><div class="num" data-m>00</div><div class="lbl">Mins</div></div>
  <div class="cd-unit"><div class="num" data-s>00</div><div class="lbl">Secs</div></div>
</div>
JavaScript
const cd = document.getElementById('countdown');
const target = Date.now() + 30 * 86400000;          // 30 days from now
const dEl = cd.querySelector('[data-d]');
const hEl = cd.querySelector('[data-h]');
const mEl = cd.querySelector('[data-m]');
const sEl = cd.querySelector('[data-s]');
const pad = x => String(x).padStart(2, '0');

function tick() {
  let gap = target - Date.now();
  if (gap < 0) gap = 0;
  dEl.textContent = pad(Math.floor(gap / 86400000));
  hEl.textContent = pad(Math.floor(gap % 86400000 / 3600000));
  mEl.textContent = pad(Math.floor(gap % 3600000 / 60000));
  sEl.textContent = pad(Math.floor(gap % 60000 / 1000));
}
setInterval(tick, 1000); tick();
CSS
.countdown { display: flex; gap: 12px; justify-content: center; }
.cd-unit { background: #1d1d1f; color: #fff; border-radius: 12px; padding: 14px 8px;
           min-width: 64px; text-align: center; }
.cd-unit .num { font-size: 1.8rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.cd-unit .lbl { font-size: .62rem; text-transform: uppercase; letter-spacing: .1em; color: #9ca3af; }
Widget 7

Instant form validation

Checks each field as you type and shows a clear message — green when valid, red with a reason when not.

HTML
<form class="vform" id="vform" novalidate>
  <div>
    <label>Email</label>
    <input type="email" name="email" placeholder="you@example.com">
    <div class="err" data-err></div>
  </div>
  <div>
    <label>Password (min 8)</label>
    <input type="password" name="pw" placeholder="••••••••">
    <div class="err" data-err></div>
  </div>
  <button type="submit">Create account</button>
  <div class="ok" id="vformOk"></div>
</form>
JavaScript
const form = document.getElementById('vform');
const ok   = document.getElementById('vformOk');

function validate(input) {
  if (!input.name) return true;                    // skip the submit button
  let msg = '';
  if (input.name === 'email' && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(input.value)) msg = 'Enter a valid email.';
  if (input.name === 'pw' && input.value.length < 8) msg = 'At least 8 characters.';
  input.classList.toggle('invalid', !!msg && input.value !== '');
  input.classList.toggle('valid', !msg && input.value !== '');
  input.parentElement.querySelector('[data-err]').textContent = input.value ? msg : '';
  return !msg;
}
form.addEventListener('input', e => { validate(e.target); ok.textContent = ''; });
form.addEventListener('submit', e => {
  e.preventDefault();
  const allOk = [...form.elements].filter(el => el.name).map(validate).every(Boolean);
  if (allOk) ok.textContent = '✓ Account created!';
});
CSS
.vform input.invalid { border-color: #ef4444; background: #fef2f2; }   /* red on error */
.vform input.valid   { border-color: #10b981; }                       /* green when valid */
.vform .err { color: #ef4444; font-size: .76rem; min-height: 14px; }
.vform .ok  { color: #059669; font-weight: 700; }
Build a capstone with these: combine tabs + a carousel + live search and you already have an interactive product page. For dynamic data, see Data & APIs; for the visual layer, the Microinteractions Lab and Web Page Motion.