Tabs
Click a tab to switch panels. One class toggle does it all.
<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>
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');
});
});
.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 */
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.
<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 -->
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';
}
});
});
.acc-a { max-height: 0; overflow: hidden; transition: max-height .3s ease; } /* JS sets the px height */
Image carousel
Prev/next arrows and clickable dots slide the track with a CSS transform.
<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>
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();
.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 */
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
<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>
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
});
.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; }
Modal dialog
Opens on click, closes on the button, the backdrop, or the Escape key.
Welcome!
This is a modal dialog. Click the backdrop, the button, or press Esc to close.
<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>
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'); });
.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; }
Countdown timer
Counts down to a target date — perfect for event or launch pages. This one targets 30 days out.
<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>
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();
.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; }
Instant form validation
Checks each field as you type and shows a clear message — green when valid, red with a reason when not.
<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>
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!';
});
.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; }