This mini project builds a small interactive UI without a framework – tabs, a modal dialog, and a sortable list. It combines modules, events, DOM updates, and basic accessibility patterns from these tutorials.
What you are building
A single-page panel with three tabs (Details, Notes, Activity), a modal for confirming deletes, and a list you can re-order with buttons or keyboard. Plain HTML, CSS, and ES modules.
Suggested file layout
project/
index.html
styles.css
js/
main.js
tabs.js
modal.js
sortable-list.js
Tabs module
export function initTabs(root) {
const tabs = [...root.querySelectorAll('[role="tab"]')];
const panels = [...root.querySelectorAll('[role="tabpanel"]')];
function activate(id) {
tabs.forEach((tab) => {
const selected = tab.id === id;
tab.setAttribute('aria-selected', String(selected));
tab.tabIndex = selected ? 0 : -1;
});
panels.forEach((panel) => {
panel.hidden = panel.getAttribute('aria-labelledby') !== id;
});
}
root.addEventListener('click', (event) => {
const tab = event.target.closest('[role="tab"]');
if (tab) activate(tab.id);
});
activate(tabs[0].id);
}
Modal with focus trap (simplified)
export function openModal(dialog) {
dialog.showModal();
const focusable = dialog.querySelector('button, [href], input');
focusable?.focus();
dialog.addEventListener('cancel', (event) => {
event.preventDefault();
closeModal(dialog);
}, { once: true });
}
export function closeModal(dialog) {
dialog.close();
}
Native <dialog> gives you showModal, backdrop click handling options, and escape key behaviour with less boilerplate than a fully custom overlay.
Sortable list pattern
export function initSortableList(list) {
list.addEventListener('click', (event) => {
const btn = event.target.closest('[data-move]');
if (!btn) return;
const item = btn.closest('li');
const dir = btn.dataset.move;
if (dir === 'up' && item.previousElementSibling) {
list.insertBefore(item, item.previousElementSibling);
}
if (dir === 'down' && item.nextElementSibling) {
list.insertBefore(item.nextElementSibling, item);
}
});
}
Acceptance checklist
- Tab panel visibility matches
aria-selected. - Modal traps focus and returns focus to the trigger on close.
- List order updates in the DOM without re-rendering the whole page.
- Listeners use delegation where lists are dynamic.
- No globals – modules export init functions called from
main.js.
Stretch goals: persist list order in localStorage, add keyboard shortcuts for tab switching (arrow keys), debounce a search filter on the list.
Next: second mini project – a small API client that fetches public data and handles loading and error states properly.

