Lab
Components from real projects, rebuilt so you can use them.
Client sites can't be shown here, so these are rebuilt from scratch with neutral content. Each one is plain JavaScript with no libraries, works with a keyboard, and shows its source.
01Masonry portfolio grid
Shortest-column placement with a category filter, no library. Rebuilt from an architecture portfolio.
- ResidentialLakeside house
- CommercialMarket hall
- CivicBranch library
- ResidentialHillside cabin
- CommercialOffice renovation
- CivicCommunity center
- ResidentialWaterfront remodel
- CommercialCorner café
- CivicFire station
View source
const GAP = 12;
for (const root of document.querySelectorAll('[data-masonry]')) {
const grid = root.querySelector('.mz-grid');
const items = [...grid.children];
const buttons = root.querySelectorAll('[data-filter]');
const status = root.querySelector('[data-masonry-status]');
let lastWidth = 0;
function layout() {
const width = grid.clientWidth;
const cols = width >= 560 ? 3 : 2;
const colWidth = (width - GAP * (cols - 1)) / cols;
const heights = new Array(cols).fill(0);
for (const item of items) {
if (item.hidden) continue;
item.style.width = `${colWidth}px`;
const col = heights.indexOf(Math.min(...heights));
item.style.transform = `translate(${col * (colWidth + GAP)}px, ${heights[col]}px)`;
heights[col] += item.offsetHeight + GAP;
}
grid.style.height = `${Math.max(0, Math.max(...heights) - GAP)}px`;
}
function filter(category) {
let shown = 0;
for (const item of items) {
item.hidden = category !== 'all' && item.dataset.category !== category;
if (!item.hidden) shown++;
}
buttons.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.filter === category)));
status.textContent = `Showing ${shown} of ${items.length} projects`;
layout();
}
buttons.forEach((button) => button.addEventListener('click', () => filter(button.dataset.filter)));
// Re-layout only when the width changes; our own height changes would otherwise loop.
new ResizeObserver(() => {
if (grid.clientWidth === lastWidth) return;
lastWidth = grid.clientWidth;
layout();
}).observe(grid);
grid.classList.add('is-laid-out');
layout();
// Turn on transitions after the first layout so items don't fly in on page load.
requestAnimationFrame(() => grid.classList.add('is-animated'));
}02Off-canvas menu
Focus stays inside while open, Esc closes it, focus returns to the button. A pattern used on several builds.
Try it with the keyboard: Tab to the button, Enter, then Tab and Esc.
View source
for (const root of document.querySelectorAll('[data-offcanvas]')) {
const openButton = root.querySelector('[data-oc-open]');
const closeButton = root.querySelector('[data-oc-close]');
const panel = root.querySelector('.oc-panel');
const page = root.querySelector('.oc-page');
const overlay = root.querySelector('.oc-overlay');
const focusables = () => [...panel.querySelectorAll('a[href], button:not([disabled])')];
function open() {
root.classList.add('is-open');
panel.inert = false;
page.inert = true; // the page behind can't be reached while the menu is open
openButton.setAttribute('aria-expanded', 'true');
closeButton.focus();
}
function close() {
root.classList.remove('is-open');
panel.inert = true;
page.inert = false;
openButton.setAttribute('aria-expanded', 'false');
openButton.focus();
}
openButton.addEventListener('click', open);
closeButton.addEventListener('click', close);
overlay.addEventListener('click', close);
panel.querySelectorAll('a[href]').forEach((link) => link.addEventListener('click', close));
root.addEventListener('keydown', (event) => {
if (!root.classList.contains('is-open')) return;
if (event.key === 'Escape') {
close();
return;
}
if (event.key !== 'Tab') return;
// Keep Tab cycling inside the panel.
const items = focusables();
const first = items[0];
const last = items[items.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});
panel.inert = true;
}04Scroll card stack
Cards pin and settle into a stack as you scroll. Rebuilt from a leadership development site.
Scroll inside this box ↓
- 01
Assess
Start from where the team really is, measured, not assumed.
- 02
Develop
Build skills in short cycles tied to real work.
- 03
Apply
Use them on live projects with support close by.
- 04
Sustain
Check back, adjust, and keep what works.
View source
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
for (const scroller of document.querySelectorAll('[data-card-stack]')) {
const cards = [...scroller.querySelectorAll('.cs-card')];
let ticking = false;
function update() {
ticking = false;
cards.forEach((card, i) => {
const next = cards[i + 1];
if (!next || reduceMotion.matches) {
card.style.transform = '';
return;
}
// 0 while the next card is a full card-height away, 1 once it has landed on top.
const distance = next.getBoundingClientRect().top - card.getBoundingClientRect().top;
const progress = Math.min(Math.max(1 - distance / card.offsetHeight, 0), 1);
card.style.transform = `scale(${1 - progress * 0.06})`;
});
}
scroller.addEventListener(
'scroll',
() => {
if (ticking) return;
ticking = true;
requestAnimationFrame(update);
},
{ passive: true },
);
update();
}05Multi-step form
Validation per step, clear errors, focus moved to each new step. Rebuilt from member signup and registration flows.
View source
function messageFor(field) {
if (field.validity.valueMissing) {
return field.type === 'checkbox' ? 'Please confirm to continue.' : 'This field is required.';
}
if (field.validity.typeMismatch) {
return field.type === 'email' ? 'Enter a valid email address.' : 'Enter a full URL, like https://example.com.';
}
return 'Check this field.';
}
for (const form of document.querySelectorAll('[data-multistep]')) {
const steps = [...form.querySelectorAll('[data-step]')];
const markers = [...form.querySelectorAll('[data-step-marker]')];
const status = form.querySelector('[data-step-status]');
const done = form.querySelector('[data-done]');
const summary = form.querySelector('[data-summary]');
let current = 0;
// We show our own messages, so the browser's popups stay off.
form.noValidate = true;
form.classList.add('is-enhanced');
function show(index, moveFocus = true) {
current = index;
steps.forEach((step, i) => (step.hidden = i !== index));
markers.forEach((marker, i) => {
marker.dataset.state = i < index ? 'done' : i === index ? 'current' : 'todo';
if (i === index) marker.setAttribute('aria-current', 'step');
else marker.removeAttribute('aria-current');
});
status.textContent = `Step ${index + 1} of ${steps.length}`;
if (moveFocus) steps[index].querySelector('legend').focus();
}
function validate(step) {
let firstInvalid = null;
for (const field of step.querySelectorAll('input, select, textarea')) {
const valid = field.checkValidity();
field.setAttribute('aria-invalid', String(!valid));
form.querySelector(`#${field.id}-error`).textContent = valid ? '' : messageFor(field);
if (!valid && !firstInvalid) firstInvalid = field;
}
firstInvalid?.focus();
return !firstInvalid;
}
form.addEventListener('click', (event) => {
const button = event.target.closest('[data-next], [data-back]');
if (!button) return;
if (button.hasAttribute('data-back')) show(current - 1);
else if (validate(steps[current])) show(current + 1);
});
form.addEventListener('submit', (event) => {
event.preventDefault();
if (!validate(steps[current])) return;
const data = new FormData(form);
summary.replaceChildren(
...['company', 'name', 'email', 'role'].map((key) => {
const row = document.createElement('li');
row.textContent = `${form.querySelector(`[name="${key}"]`).dataset.label}: ${data.get(key)}`;
return row;
}),
);
steps.forEach((step) => (step.hidden = true));
form.querySelector('[data-progress]').hidden = true;
done.hidden = false;
done.querySelector('h3').focus();
});
form.querySelector('[data-restart]').addEventListener('click', () => {
form.reset();
form.querySelectorAll('[aria-invalid]').forEach((field) => field.removeAttribute('aria-invalid'));
form.querySelectorAll('[id$="-error"]').forEach((error) => (error.textContent = ''));
done.hidden = true;
form.querySelector('[data-progress]').hidden = false;
show(0);
});
show(0, false);
}06Pre-launch QA checklist
The 17 checks every landing page passed before going live. Your progress is saved in this browser.
View source
const STORAGE_KEY = 'lab-qa-checklist';
function load() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '[]');
} catch {
return [];
}
}
function save(checked) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(checked));
} catch {
// Storage blocked: progress just won't survive a reload.
}
}
for (const root of document.querySelectorAll('[data-qa-checklist]')) {
const boxes = [...root.querySelectorAll('input[type="checkbox"]')];
const count = root.querySelector('[data-qa-count]');
const bar = root.querySelector('[data-qa-bar]');
const launch = root.querySelector('[data-qa-launch]');
const result = root.querySelector('[data-qa-result]');
function update() {
const done = boxes.filter((box) => box.checked).length;
count.textContent = `${done} / ${boxes.length}`;
bar.style.width = `${(done / boxes.length) * 100}%`;
launch.disabled = done < boxes.length;
result.textContent = '';
save(boxes.filter((box) => box.checked).map((box) => box.value));
}
const saved = new Set(load());
boxes.forEach((box) => {
box.checked = saved.has(box.value);
box.addEventListener('change', update);
});
launch.addEventListener('click', () => {
result.textContent = 'All 17 checks passed. Ready to go live.';
});
root.querySelector('[data-qa-reset]').addEventListener('click', () => {
boxes.forEach((box) => (box.checked = false));
update();
});
update();
}