Codeskill

Learn to code, step by step

Mini project: interactive UI

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.

PreviousTooling overview (npm scripts, bundlers)