LMS — Curriculum Accordion
A self-contained e-learning curriculum accordion where course modules expand to reveal their lessons. Each lesson row shows a type icon for video, quiz, or reading, its duration, and a tappable completion check. Module progress bars and an overall completion ring update live as you check lessons off, free-preview links open a toast, and locked modules unlock automatically once the previous module is fully finished.
MCP
Code
:root {
--brand: #5b5bd6;
--brand-d: #4444c2;
--brand-50: #eeeefc;
--accent: #13b981;
--amber: #f59e0b;
--ink: #1a1a2e;
--ink-2: #44465f;
--muted: #6b6e87;
--bg: #f7f7fb;
--surface: #ffffff;
--line: rgba(26, 26, 46, 0.1);
--ok: #13b981;
--danger: #e05656;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-1: 0 1px 2px rgba(26, 26, 46, 0.05), 0 4px 14px rgba(26, 26, 46, 0.06);
--shadow-2: 0 8px 30px rgba(68, 68, 194, 0.14);
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background:
radial-gradient(1200px 480px at 8% -10%, rgba(91, 91, 214, 0.08), transparent 60%),
var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.page {
max-width: 760px;
margin: 0 auto;
padding: 28px 20px 56px;
}
/* ---------- Course header ---------- */
.course-head {
display: flex;
gap: 20px;
align-items: flex-start;
justify-content: space-between;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px 24px;
box-shadow: var(--shadow-1);
}
.course-head__crumb {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 13px;
font-weight: 500;
}
.dot {
width: 3px;
height: 3px;
border-radius: 50%;
background: var(--muted);
opacity: 0.6;
}
.course-head h1 {
margin: 12px 0 4px;
font-size: 26px;
font-weight: 800;
letter-spacing: -0.02em;
}
.course-head__by {
margin: 0;
color: var(--ink-2);
font-size: 14px;
font-weight: 500;
}
.course-head__progress {
display: flex;
align-items: center;
gap: 14px;
flex-shrink: 0;
}
.ring {
position: relative;
width: 72px;
height: 72px;
display: grid;
place-items: center;
}
.ring svg { transform: rotate(-90deg); }
.ring__track {
fill: none;
stroke: var(--brand-50);
stroke-width: 8;
}
.ring__fill {
fill: none;
stroke: var(--accent);
stroke-width: 8;
stroke-linecap: round;
stroke-dasharray: 188.5;
stroke-dashoffset: 188.5;
transition: stroke-dashoffset 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.ring__label {
position: absolute;
font-size: 16px;
font-weight: 800;
color: var(--ink);
}
.course-head__progress-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.course-head__progress-text strong { font-size: 15px; font-weight: 700; }
.course-head__progress-text span { font-size: 12px; color: var(--muted); }
.resume {
margin-top: 8px;
align-self: flex-start;
background: var(--brand);
color: #fff;
border: none;
border-radius: 999px;
padding: 7px 14px;
font: inherit;
font-size: 12.5px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, transform 0.1s;
}
.resume:hover { background: var(--brand-d); }
.resume:active { transform: translateY(1px); }
/* ---------- Pills ---------- */
.pill {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.02em;
padding: 3px 9px;
border-radius: 999px;
text-transform: uppercase;
}
.pill--level { background: var(--brand-50); color: var(--brand-d); }
/* ---------- Toolbar ---------- */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 22px 2px 14px;
flex-wrap: wrap;
}
.toolbar__hint { margin: 0; color: var(--muted); font-size: 13px; }
.toolbar__actions { display: flex; gap: 8px; }
.ghost-btn {
background: var(--surface);
border: 1px solid var(--line);
color: var(--ink-2);
border-radius: var(--r-sm);
padding: 7px 12px;
font: inherit;
font-size: 12.5px;
font-weight: 600;
cursor: pointer;
transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.ghost-btn:hover { border-color: var(--brand); color: var(--brand-d); background: var(--brand-50); }
/* ---------- Curriculum / modules ---------- */
.curriculum {
display: flex;
flex-direction: column;
gap: 12px;
}
.module {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--shadow-1);
overflow: hidden;
transition: box-shadow 0.2s, border-color 0.2s;
}
.module.is-open { box-shadow: var(--shadow-2); border-color: rgba(91, 91, 214, 0.3); }
.module.is-locked { opacity: 0.72; }
.module__head {
width: 100%;
display: flex;
align-items: center;
gap: 14px;
padding: 16px 18px;
background: none;
border: none;
text-align: left;
cursor: pointer;
font: inherit;
color: inherit;
}
.module.is-locked .module__head { cursor: not-allowed; }
.module__head:focus-visible {
outline: 2px solid var(--brand);
outline-offset: -2px;
}
.module__index {
width: 34px;
height: 34px;
flex-shrink: 0;
display: grid;
place-items: center;
border-radius: 10px;
background: var(--brand-50);
color: var(--brand-d);
font-weight: 800;
font-size: 14px;
}
.module.is-done .module__index {
background: rgba(19, 185, 129, 0.14);
color: var(--accent);
}
.module__title-wrap { flex: 1; min-width: 0; }
.module__title {
margin: 0;
font-size: 15.5px;
font-weight: 700;
letter-spacing: -0.01em;
}
.module__meta {
margin: 3px 0 0;
display: flex;
align-items: center;
gap: 7px;
font-size: 12px;
color: var(--muted);
flex-wrap: wrap;
}
.module__bar {
position: relative;
height: 6px;
width: 92px;
flex-shrink: 0;
border-radius: 999px;
background: var(--brand-50);
overflow: hidden;
}
.module__bar > i {
position: absolute;
inset: 0;
width: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--accent), #34d399);
transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.module__pct {
font-size: 12px;
font-weight: 700;
color: var(--ink-2);
min-width: 34px;
text-align: right;
}
.chev {
flex-shrink: 0;
width: 16px;
height: 16px;
color: var(--muted);
transition: transform 0.25s ease;
}
.module.is-open .chev { transform: rotate(180deg); color: var(--brand); }
.lock-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-weight: 700;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.02em;
}
/* ---------- Lesson list (collapsible body) ---------- */
.module__body {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease;
}
.module.is-open .module__body { grid-template-rows: 1fr; }
.module__body > div { overflow: hidden; }
.lessons {
list-style: none;
margin: 0;
padding: 2px 14px 14px;
border-top: 1px solid var(--line);
}
.lesson {
display: flex;
align-items: center;
gap: 12px;
padding: 11px 8px;
border-radius: var(--r-sm);
transition: background 0.15s;
}
.lesson + .lesson { border-top: 1px solid rgba(26, 26, 46, 0.05); }
.lesson:not(.lesson--locked):hover { background: var(--brand-50); }
.lesson__check {
width: 24px;
height: 24px;
flex-shrink: 0;
border-radius: 50%;
border: 2px solid var(--line);
background: var(--surface);
display: grid;
place-items: center;
cursor: pointer;
padding: 0;
color: transparent;
transition: background 0.18s, border-color 0.18s, color 0.18s, transform 0.1s;
}
.lesson__check:hover { border-color: var(--accent); }
.lesson__check:active { transform: scale(0.9); }
.lesson__check:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.lesson.is-done .lesson__check {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.lesson__check svg { width: 13px; height: 13px; }
.lesson__icon {
width: 30px;
height: 30px;
flex-shrink: 0;
border-radius: 9px;
display: grid;
place-items: center;
color: var(--brand-d);
background: var(--brand-50);
}
.lesson__icon svg { width: 16px; height: 16px; }
.lesson--video .lesson__icon { background: rgba(91, 91, 214, 0.12); color: var(--brand-d); }
.lesson--quiz .lesson__icon { background: rgba(245, 158, 11, 0.14); color: #b06f04; }
.lesson--reading .lesson__icon { background: rgba(19, 185, 129, 0.13); color: #0a8c61; }
.lesson__main { flex: 1; min-width: 0; }
.lesson__name {
font-size: 14px;
font-weight: 600;
color: var(--ink);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lesson.is-done .lesson__name { color: var(--muted); }
.lesson__sub {
margin-top: 2px;
display: flex;
align-items: center;
gap: 7px;
font-size: 11.5px;
color: var(--muted);
}
.tag {
text-transform: capitalize;
font-weight: 600;
}
.lesson__dur {
font-size: 12px;
font-weight: 600;
color: var(--muted);
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
.preview-link {
flex-shrink: 0;
font-size: 11.5px;
font-weight: 700;
color: var(--brand);
text-decoration: none;
padding: 4px 9px;
border-radius: 999px;
border: 1px solid rgba(91, 91, 214, 0.3);
cursor: pointer;
background: none;
font-family: inherit;
transition: background 0.15s;
}
.preview-link:hover { background: var(--brand-50); }
.lesson--locked { color: var(--muted); }
.lesson--locked .lesson__name { color: var(--muted); }
.lesson__lock {
width: 24px;
height: 24px;
flex-shrink: 0;
display: grid;
place-items: center;
color: var(--muted);
}
.lesson__lock svg { width: 15px; height: 15px; }
/* ---------- Foot ---------- */
.foot-note {
margin: 22px 4px 0;
font-size: 12.5px;
color: var(--muted);
text-align: center;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 20px);
background: var(--ink);
color: #fff;
font-size: 13px;
font-weight: 600;
padding: 11px 18px;
border-radius: 999px;
box-shadow: 0 10px 30px rgba(26, 26, 46, 0.3);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.page { padding: 18px 14px 44px; }
.course-head {
flex-direction: column;
padding: 18px;
}
.course-head__progress { width: 100%; }
.course-head h1 { font-size: 22px; }
.module__bar { display: none; }
.module__head { padding: 14px; gap: 11px; }
.preview-link { display: none; }
.lesson__name { white-space: normal; }
}(function () {
"use strict";
// ---- Icons ----
const ICONS = {
video:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>',
quiz:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line></svg>',
reading:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>',
check:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>',
chevron:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>',
lock:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>',
};
// ---- Fictional course data ----
const MODULES = [
{
title: "Foundations of Calm Design",
lessons: [
{ name: "Why interfaces feel noisy", type: "video", dur: "9:12", done: true, preview: true },
{ name: "The attention budget", type: "reading", dur: "6 min", done: true },
{ name: "Reading: signal vs. ornament", type: "reading", dur: "8 min", done: true },
{ name: "Module 1 checkpoint", type: "quiz", dur: "5 min", done: true },
],
},
{
title: "Spacing, Rhythm & Hierarchy",
lessons: [
{ name: "An 8-point grid that breathes", type: "video", dur: "12:40", done: true, preview: true },
{ name: "Building a type scale", type: "video", dur: "10:05", done: true },
{ name: "Whitespace as a feature", type: "reading", dur: "7 min", done: false },
{ name: "Layout teardown: Linnea Mail", type: "video", dur: "14:18", done: false },
{ name: "Hierarchy quiz", type: "quiz", dur: "6 min", done: false },
],
},
{
title: "Color, Contrast & Mood",
lessons: [
{ name: "Choosing a restrained palette", type: "video", dur: "11:22", done: false, preview: true },
{ name: "Contrast for accessibility (AA)", type: "reading", dur: "9 min", done: false },
{ name: "Dark mode without losing calm", type: "video", dur: "13:47", done: false },
{ name: "Color checkpoint", type: "quiz", dur: "5 min", done: false },
],
},
{
title: "Motion & Micro-interactions",
locked: true,
lessons: [
{ name: "Easing curves that feel right", type: "video", dur: "10:30", done: false },
{ name: "When NOT to animate", type: "reading", dur: "6 min", done: false },
{ name: "Building a gentle toast", type: "video", dur: "12:09", done: false },
{ name: "Motion checkpoint", type: "quiz", dur: "5 min", done: false },
],
},
{
title: "Capstone: Redesign Northwind",
locked: true,
lessons: [
{ name: "Briefing the capstone", type: "video", dur: "8:55", done: false },
{ name: "Critique & ship", type: "reading", dur: "10 min", done: false },
{ name: "Final review", type: "quiz", dur: "12 min", done: false },
],
},
];
const TYPE_LABEL = { video: "Video", quiz: "Quiz", reading: "Reading" };
const root = document.getElementById("curriculum");
const toastEl = document.getElementById("toast");
let toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 2200);
}
// ---- Render ----
function moduleStats(mod) {
const total = mod.lessons.length;
const done = mod.lessons.filter((l) => l.done).length;
return { total, done, pct: total ? Math.round((done / total) * 100) : 0 };
}
function buildModule(mod, mi) {
const stats = moduleStats(mod);
const allDone = stats.done === stats.total && !mod.locked;
const wrap = document.createElement("section");
wrap.className = "module";
if (mod.locked) wrap.classList.add("is-locked");
if (allDone) wrap.classList.add("is-done");
const bodyId = "mod-body-" + mi;
const head = document.createElement("button");
head.type = "button";
head.className = "module__head";
head.setAttribute("aria-expanded", "false");
head.setAttribute("aria-controls", bodyId);
if (mod.locked) head.setAttribute("aria-disabled", "true");
head.innerHTML =
'<span class="module__index">' + (mod.locked ? ICONS.lock : mi + 1) + "</span>" +
'<span class="module__title-wrap">' +
'<h2 class="module__title">' + mod.title + "</h2>" +
'<p class="module__meta">' +
"<span>" + stats.total + " lessons</span>" +
(mod.locked
? '<span class="lock-badge">Locked</span>'
: '<span class="js-mod-count">' + stats.done + " / " + stats.total + " done</span>") +
"</p>" +
"</span>" +
(mod.locked
? '<span class="lock-badge">' + ICONS.lock + "</span>"
: '<span class="module__bar"><i class="js-mod-fill"></i></span>' +
'<span class="module__pct js-mod-pct">' + stats.pct + "%</span>") +
'<span class="chev">' + ICONS.chevron + "</span>";
const body = document.createElement("div");
body.className = "module__body";
body.id = bodyId;
const inner = document.createElement("div");
const list = document.createElement("ul");
list.className = "lessons";
mod.lessons.forEach((lesson, li) => {
list.appendChild(buildLesson(lesson, mod, mi, li));
});
inner.appendChild(list);
body.appendChild(inner);
wrap.appendChild(head);
wrap.appendChild(body);
head.addEventListener("click", () => {
if (mod.locked) {
toast("🔒 Finish “" + MODULES[mi - 1].title + "” to unlock this module.");
return;
}
const open = wrap.classList.toggle("is-open");
head.setAttribute("aria-expanded", open ? "true" : "false");
});
return wrap;
}
function buildLesson(lesson, mod, mi, li) {
const li_el = document.createElement("li");
li_el.className = "lesson lesson--" + lesson.type;
if (lesson.done) li_el.classList.add("is-done");
if (mod.locked) {
li_el.classList.add("lesson--locked");
li_el.innerHTML =
'<span class="lesson__lock">' + ICONS.lock + "</span>" +
'<span class="lesson__icon">' + ICONS[lesson.type] + "</span>" +
'<span class="lesson__main">' +
'<div class="lesson__name">' + lesson.name + "</div>" +
'<div class="lesson__sub"><span class="tag">' + TYPE_LABEL[lesson.type] + "</span></div>" +
"</span>" +
'<span class="lesson__dur">' + lesson.dur + "</span>";
return li_el;
}
const checkBtn = document.createElement("button");
checkBtn.type = "button";
checkBtn.className = "lesson__check";
checkBtn.setAttribute("aria-pressed", lesson.done ? "true" : "false");
checkBtn.setAttribute(
"aria-label",
(lesson.done ? "Mark incomplete: " : "Mark complete: ") + lesson.name
);
checkBtn.innerHTML = ICONS.check;
const main = document.createElement("span");
main.className = "lesson__main";
main.innerHTML =
'<div class="lesson__name">' + lesson.name + "</div>" +
'<div class="lesson__sub"><span class="tag">' + TYPE_LABEL[lesson.type] + "</span></div>";
const icon = document.createElement("span");
icon.className = "lesson__icon";
icon.innerHTML = ICONS[lesson.type];
const dur = document.createElement("span");
dur.className = "lesson__dur";
dur.textContent = lesson.dur;
li_el.appendChild(checkBtn);
li_el.appendChild(icon);
li_el.appendChild(main);
if (lesson.preview) {
const pv = document.createElement("button");
pv.type = "button";
pv.className = "preview-link";
pv.textContent = "Preview";
pv.addEventListener("click", (e) => {
e.stopPropagation();
toast("▶ Free preview: " + lesson.name);
});
li_el.appendChild(pv);
}
li_el.appendChild(dur);
checkBtn.addEventListener("click", (e) => {
e.stopPropagation();
lesson.done = !lesson.done;
li_el.classList.toggle("is-done", lesson.done);
checkBtn.setAttribute("aria-pressed", lesson.done ? "true" : "false");
checkBtn.setAttribute(
"aria-label",
(lesson.done ? "Mark incomplete: " : "Mark complete: ") + lesson.name
);
refresh(mi);
toast(lesson.done ? "✓ Marked complete — " + lesson.name : "Marked incomplete");
});
return li_el;
}
// ---- Progress refresh ----
const moduleEls = [];
function refresh(changedIndex) {
// update affected module + unlock cascade
MODULES.forEach((mod, mi) => {
const el = moduleEls[mi];
const stats = moduleStats(mod);
if (!mod.locked) {
const fill = el.querySelector(".js-mod-fill");
const pct = el.querySelector(".js-mod-pct");
const count = el.querySelector(".js-mod-count");
if (fill) fill.style.width = stats.pct + "%";
if (pct) pct.textContent = stats.pct + "%";
if (count) count.textContent = stats.done + " / " + stats.total + " done";
el.classList.toggle("is-done", stats.done === stats.total);
}
// unlock if previous module fully done
if (mod.locked && mi > 0) {
const prev = moduleStats(MODULES[mi - 1]);
if (!MODULES[mi - 1].locked && prev.done === prev.total) {
mod.locked = false;
rebuildModule(mi);
toast("🔓 Unlocked: " + mod.title);
}
}
});
updateOverall();
}
function rebuildModule(mi) {
const oldEl = moduleEls[mi];
const wasOpen = oldEl.classList.contains("is-open");
const fresh = buildModule(MODULES[mi], mi);
if (wasOpen) {
fresh.classList.add("is-open");
fresh.querySelector(".module__head").setAttribute("aria-expanded", "true");
}
oldEl.replaceWith(fresh);
moduleEls[mi] = fresh;
refresh(mi);
}
function updateOverall() {
let total = 0,
done = 0;
MODULES.forEach((m) => {
total += m.lessons.length;
done += m.lessons.filter((l) => l.done).length;
});
const pct = total ? Math.round((done / total) * 100) : 0;
const circ = 188.5;
document.getElementById("overallRingFill").style.strokeDashoffset =
circ - (circ * pct) / 100;
document.getElementById("overallRingLabel").textContent = pct + "%";
document.getElementById("overallRing").setAttribute(
"aria-label",
"Course " + pct + " percent complete"
);
document.getElementById("overallCount").textContent = done + " of " + total;
}
// ---- Init ----
MODULES.forEach((mod, mi) => {
const el = buildModule(mod, mi);
moduleEls.push(el);
root.appendChild(el);
});
// open first incomplete module
const firstActive = MODULES.findIndex(
(m) => !m.locked && m.lessons.some((l) => !l.done)
);
const openIdx = firstActive === -1 ? 0 : firstActive;
moduleEls[openIdx].classList.add("is-open");
moduleEls[openIdx].querySelector(".module__head").setAttribute("aria-expanded", "true");
updateOverall();
MODULES.forEach((m, i) => {
if (!m.locked) {
const fill = moduleEls[i].querySelector(".js-mod-fill");
if (fill) fill.style.width = moduleStats(m).pct + "%";
}
});
// ---- Toolbar ----
document.getElementById("expandAll").addEventListener("click", () => {
moduleEls.forEach((el, i) => {
if (!MODULES[i].locked) {
el.classList.add("is-open");
el.querySelector(".module__head").setAttribute("aria-expanded", "true");
}
});
});
document.getElementById("collapseAll").addEventListener("click", () => {
moduleEls.forEach((el) => {
el.classList.remove("is-open");
el.querySelector(".module__head").setAttribute("aria-expanded", "false");
});
});
document.getElementById("resumeBtn").addEventListener("click", () => {
const idx = MODULES.findIndex(
(m) => !m.locked && m.lessons.some((l) => !l.done)
);
const target = idx === -1 ? 0 : idx;
moduleEls.forEach((el) => el.classList.remove("is-open"));
const el = moduleEls[target];
el.classList.add("is-open");
el.querySelector(".module__head").setAttribute("aria-expanded", "true");
el.scrollIntoView({ behavior: "smooth", block: "center" });
const next = MODULES[target].lessons.find((l) => !l.done);
toast(next ? "Next up: " + next.name : "You're all caught up!");
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>LMS — Curriculum Accordion</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="course-head">
<div class="course-head__main">
<div class="course-head__crumb">
<span class="pill pill--level">Intermediate</span>
<span class="dot" aria-hidden="true"></span>
<span>8 modules</span>
<span class="dot" aria-hidden="true"></span>
<span>34 lessons</span>
<span class="dot" aria-hidden="true"></span>
<span>6h 52m</span>
</div>
<h1>Designing Calm Interfaces</h1>
<p class="course-head__by">with Marisol Vega & the Northwind Studio team</p>
</div>
<div class="course-head__progress">
<div class="ring" id="overallRing" role="img" aria-label="Course 22 percent complete">
<svg viewBox="0 0 72 72" width="72" height="72">
<circle class="ring__track" cx="36" cy="36" r="30" />
<circle class="ring__fill" id="overallRingFill" cx="36" cy="36" r="30" />
</svg>
<span class="ring__label" id="overallRingLabel">22%</span>
</div>
<div class="course-head__progress-text">
<strong id="overallCount">7 of 34</strong>
<span>lessons complete</span>
<button class="resume" id="resumeBtn" type="button">Resume learning ›</button>
</div>
</div>
</header>
<div class="toolbar" role="region" aria-label="Curriculum controls">
<p class="toolbar__hint">Tap a module to expand its lessons.</p>
<div class="toolbar__actions">
<button class="ghost-btn" id="expandAll" type="button">Expand all</button>
<button class="ghost-btn" id="collapseAll" type="button">Collapse all</button>
</div>
</div>
<main class="curriculum" id="curriculum" aria-label="Course curriculum"></main>
<p class="foot-note">Lessons unlock as you complete earlier modules. Locked rows show a padlock.</p>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Curriculum Accordion
A focused, light-theme course outline for a fictional design course. The header pairs a difficulty pill and at-a-glance stats (modules, lessons, total runtime) with an animated SVG completion ring and a “Resume learning” button that jumps straight to your next unfinished lesson. Below it, each module is a collapsible card: tap the header to expand its lessons, and a slim progress bar plus percentage track how far you’ve come.
Every lesson row carries a colored type icon — play for video, question mark for quiz, book for reading — alongside its duration and a circular check-off button. Toggling a check animates the fill, recomputes that module’s progress and the overall ring, and raises a small toast. Some lessons expose a free “Preview” link. Locked modules sit dimmed behind a padlock and politely refuse to open until you finish the module before them, at which point they unlock automatically with a confirmation toast.
Toolbar controls let you expand or collapse every module at once, the first incomplete module opens on load, and the whole layout reflows to a comfortable mobile-first column down to ~360px. Interactive elements are real buttons with aria-expanded/aria-pressed state and visible focus rings, keeping it keyboard-usable and AA-readable.
Illustrative UI only — fictional courses, not a real learning platform.