Gym — Class Roster
A high-energy gym class roster with live attendance check-in. A neon-accented header shows the class name, time, coach and a capacity bar, while a searchable member list pairs gradient avatars and tier badges with Check-in and No-show toggles. Toggling animates each row between present, no-show and waitlist-promoted states as present, expected, no-show and waitlist counters update in real time. Includes a pending-only filter, a check-in-all action and a walk-in add row.
MCP
代码
:root {
--bg: #0d0f12;
--surface: #15181d;
--surface-2: #1d2127;
--elevated: #23282f;
--ink: #f4f6f8;
--ink-2: #c2c8d0;
--muted: #8b929c;
--neon: #c6ff3a;
--neon-d: #a6e016;
--neon-50: rgba(198, 255, 58, 0.12);
--orange: #ff6a2b;
--orange-soft: rgba(255, 106, 43, 0.14);
--line: rgba(255, 255, 255, 0.08);
--line-2: rgba(255, 255, 255, 0.16);
--ok: #34d399;
--warn: #fbbf24;
--danger: #f87171;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-1: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 12px 30px -12px rgba(0, 0, 0, 0.7);
--shadow-2: 0 20px 60px -20px rgba(0, 0, 0, 0.8);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(1200px 600px at 80% -10%, rgba(198, 255, 58, 0.06), transparent 60%),
radial-gradient(900px 500px at -10% 10%, rgba(255, 106, 43, 0.07), transparent 55%), var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: clamp(16px, 4vw, 48px);
display: flex;
justify-content: center;
}
.shell {
width: 100%;
max-width: 720px;
}
.card {
background: linear-gradient(180deg, var(--surface), var(--bg));
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-2);
overflow: hidden;
}
/* ---------- Hero ---------- */
.hero {
padding: clamp(20px, 4vw, 30px);
background: linear-gradient(160deg, rgba(198, 255, 58, 0.07), transparent 45%), var(--surface);
border-bottom: 1px solid var(--line);
}
.hero__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.eyebrow {
font-size: 11px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--neon);
}
.pill {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 5px 11px;
border-radius: 999px;
border: 1px solid var(--line-2);
color: var(--ink-2);
}
.pill--live {
color: var(--ok);
border-color: rgba(52, 211, 153, 0.4);
background: rgba(52, 211, 153, 0.1);
}
.dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.6);
animation: pulse 1.8s ease-out infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.55);
}
70% {
box-shadow: 0 0 0 7px rgba(52, 211, 153, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(52, 211, 153, 0);
}
}
h1 {
margin: 0;
font-size: clamp(28px, 6vw, 40px);
font-weight: 900;
letter-spacing: -0.02em;
line-height: 1.05;
}
.hero__meta {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 14px;
color: var(--muted);
font-size: 14px;
}
.meta strong {
color: var(--ink-2);
font-weight: 700;
}
.meta--sep {
position: relative;
padding-left: 14px;
}
.meta--sep::before {
content: "";
position: absolute;
left: 0;
top: 50%;
width: 4px;
height: 4px;
margin-top: -2px;
border-radius: 50%;
background: var(--line-2);
}
/* ---------- Capacity ---------- */
.capacity {
margin-top: 20px;
}
.capacity__head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 8px;
}
.capacity__label {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
}
.capacity__count {
font-size: 18px;
font-weight: 800;
color: var(--ink);
}
.capacity__max {
color: var(--muted);
font-weight: 600;
}
.capacity__bar {
height: 10px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line);
overflow: hidden;
}
.capacity__fill {
display: block;
height: 100%;
width: 90%;
border-radius: 999px;
background: linear-gradient(90deg, var(--neon-d), var(--neon));
transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
/* ---------- Stats ---------- */
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1px;
background: var(--line);
border-bottom: 1px solid var(--line);
}
.stat {
background: var(--surface);
padding: 16px 12px;
text-align: center;
display: flex;
flex-direction: column;
gap: 3px;
}
.stat__num {
font-size: 26px;
font-weight: 900;
line-height: 1;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
}
.stat__label {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
}
.stat--present .stat__num {
color: var(--neon);
}
.stat--noshow .stat__num {
color: var(--danger);
}
.stat--wait .stat__num {
color: var(--warn);
}
/* ---------- Toolbar ---------- */
.toolbar {
display: flex;
gap: 10px;
padding: 16px clamp(16px, 4vw, 24px);
align-items: center;
flex-wrap: wrap;
}
.search {
position: relative;
flex: 1 1 200px;
min-width: 0;
}
.search__icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 18px;
height: 18px;
color: var(--muted);
pointer-events: none;
}
#search {
width: 100%;
padding: 11px 12px 11px 38px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--ink);
font: inherit;
font-size: 14px;
}
#search::placeholder {
color: var(--muted);
}
#search:focus-visible {
outline: none;
border-color: var(--neon-d);
box-shadow: 0 0 0 3px var(--neon-50);
}
.btn {
font: inherit;
font-weight: 700;
font-size: 14px;
border-radius: var(--r-md);
padding: 11px 16px;
border: 1px solid var(--line-2);
background: var(--elevated);
color: var(--ink);
cursor: pointer;
transition: transform 0.12s ease, background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
white-space: nowrap;
}
.btn:hover {
border-color: var(--line-2);
background: #2a3038;
}
.btn:active {
transform: translateY(1px) scale(0.99);
}
.btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--neon-50);
}
.btn--ghost {
background: transparent;
color: var(--ink-2);
border-color: var(--line);
}
.btn--ghost[aria-pressed="true"] {
background: var(--neon-50);
border-color: rgba(198, 255, 58, 0.4);
color: var(--neon);
}
.btn--neon {
background: var(--neon);
color: #0d0f12;
border-color: transparent;
}
.btn--neon:hover {
background: var(--neon-d);
}
.btn--orange {
background: var(--orange);
color: #1a0d05;
border-color: transparent;
}
.btn--orange:hover {
background: #ff7d47;
}
/* ---------- Roster ---------- */
.roster {
list-style: none;
margin: 0;
padding: 0 clamp(16px, 4vw, 24px);
display: flex;
flex-direction: column;
gap: 8px;
}
.row {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 14px;
padding: 12px 14px;
border-radius: var(--r-md);
background: var(--surface-2);
border: 1px solid var(--line);
border-left: 3px solid transparent;
transition: background 0.25s ease, border-color 0.25s ease, transform 0.18s ease,
opacity 0.25s ease;
}
.row[hidden] {
display: none;
}
.row.is-present {
border-left-color: var(--neon);
background: linear-gradient(90deg, var(--neon-50), transparent 50%), var(--surface-2);
}
.row.is-noshow {
border-left-color: var(--danger);
opacity: 0.62;
}
.row.is-promoted {
border-left-color: var(--warn);
background: linear-gradient(90deg, rgba(251, 191, 36, 0.1), transparent 50%), var(--surface-2);
}
.row.flash {
animation: flash 0.45s ease;
}
@keyframes flash {
0% {
transform: scale(1);
}
35% {
transform: scale(1.012);
box-shadow: 0 0 0 1px var(--line-2);
}
100% {
transform: scale(1);
}
}
.avatar {
width: 42px;
height: 42px;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 800;
font-size: 15px;
color: #0d0f12;
letter-spacing: 0.02em;
flex-shrink: 0;
}
.who {
min-width: 0;
}
.who__name {
font-weight: 700;
font-size: 15px;
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.who__sub {
display: flex;
align-items: center;
gap: 8px;
margin-top: 3px;
}
.tier {
font-size: 10px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid var(--line-2);
color: var(--ink-2);
}
.tier--Elite {
color: var(--neon);
border-color: rgba(198, 255, 58, 0.4);
background: var(--neon-50);
}
.tier--Plus {
color: var(--orange);
border-color: rgba(255, 106, 43, 0.4);
background: var(--orange-soft);
}
.state-tag {
font-size: 11px;
font-weight: 700;
color: var(--muted);
}
.row.is-present .state-tag {
color: var(--neon);
}
.row.is-noshow .state-tag {
color: var(--danger);
}
.row.is-promoted .state-tag {
color: var(--warn);
}
.actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
.tog {
font: inherit;
font-weight: 700;
font-size: 12px;
border-radius: 999px;
padding: 7px 12px;
cursor: pointer;
border: 1px solid var(--line-2);
background: var(--elevated);
color: var(--ink-2);
transition: transform 0.12s ease, background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.tog:hover {
background: #2a3038;
color: var(--ink);
}
.tog:active {
transform: scale(0.94);
}
.tog:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--neon-50);
}
.tog--present {
background: var(--neon);
color: #0d0f12;
border-color: transparent;
}
.tog--present:hover {
background: var(--neon-d);
color: #0d0f12;
}
.tog--miss[data-active="true"] {
background: rgba(248, 113, 113, 0.16);
color: var(--danger);
border-color: rgba(248, 113, 113, 0.45);
}
/* ---------- Walk-in ---------- */
.walkin {
display: flex;
gap: 10px;
align-items: center;
padding: 16px clamp(16px, 4vw, 24px) clamp(20px, 4vw, 26px);
margin-top: 8px;
border-top: 1px dashed var(--line-2);
}
.walkin__avatar {
width: 42px;
height: 42px;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 24px;
font-weight: 700;
color: var(--muted);
border: 2px dashed var(--line-2);
flex-shrink: 0;
}
.walkin__input {
flex: 1 1 auto;
min-width: 0;
padding: 11px 12px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--ink);
font: inherit;
font-size: 14px;
}
.walkin__input::placeholder {
color: var(--muted);
}
.walkin__input:focus-visible,
.walkin__select:focus-visible {
outline: none;
border-color: var(--orange);
box-shadow: 0 0 0 3px var(--orange-soft);
}
.walkin__select {
padding: 11px 12px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--ink);
font: inherit;
font-size: 14px;
cursor: pointer;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 28px);
background: var(--elevated);
color: var(--ink);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 11px 20px;
font-size: 14px;
font-weight: 600;
box-shadow: var(--shadow-1);
opacity: 0;
pointer-events: none;
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.3s ease;
z-index: 50;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
.toast__accent {
color: var(--neon);
font-weight: 800;
}
/* ---------- Empty ---------- */
.empty {
text-align: center;
color: var(--muted);
padding: 28px 12px;
font-size: 14px;
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
body {
padding: 12px;
}
.stats {
grid-template-columns: repeat(2, 1fr);
}
.toolbar {
gap: 8px;
}
.search {
flex-basis: 100%;
order: -1;
}
.row {
grid-template-columns: auto 1fr;
gap: 10px;
}
.actions {
grid-column: 1 / -1;
justify-content: flex-end;
margin-top: 4px;
}
.walkin {
flex-wrap: wrap;
}
.walkin__avatar {
display: none;
}
.walkin__input {
flex-basis: 100%;
}
.walkin__select {
flex: 1;
}
}(function () {
"use strict";
var CAPACITY = 20;
// Member states: "expected" | "present" | "noshow" | "promoted"
var members = [
{ id: 1, name: "Diego Salcedo", tier: "Elite", state: "present" },
{ id: 2, name: "Priya Raman", tier: "Plus", state: "expected" },
{ id: 3, name: "Tomás Bianchi", tier: "Core", state: "present" },
{ id: 4, name: "Hannah Okafor", tier: "Elite", state: "expected" },
{ id: 5, name: "Wei Lin", tier: "Core", state: "noshow" },
{ id: 6, name: "Sofía Marín", tier: "Plus", state: "expected" },
{ id: 7, name: "Marcus Hale", tier: "Core", state: "present" },
{ id: 8, name: "Yuki Tanaka", tier: "Elite", state: "expected" },
{ id: 9, name: "Olivia Brandt", tier: "Day Pass", state: "expected" },
{ id: 10, name: "Kwame Asante", tier: "Plus", state: "promoted" },
{ id: 11, name: "Renata Cardoso", tier: "Core", state: "expected" },
{ id: 12, name: "Felix Norberg", tier: "Core", state: "present" },
];
var TIER_COLORS = {
Elite: ["#c6ff3a", "#a6e016"],
Plus: ["#ff6a2b", "#ff8a5a"],
Core: ["#5e9bff", "#7fb2ff"],
"Day Pass": ["#8b929c", "#aeb4bd"],
};
var STATE_LABEL = {
expected: "Expected",
present: "Present",
noshow: "No-show",
promoted: "Waitlist · promoted",
};
var $ = function (sel) {
return document.querySelector(sel);
};
var rosterEl = $("#roster");
var searchEl = $("#search");
var filterBtn = $("#filterToggle");
var checkAllBtn = $("#checkAll");
var toastEl = $("#toast");
var nextId = 100;
var pendingOnly = false;
/* ---------- toast ---------- */
var toastTimer;
function toast(msg) {
toastEl.innerHTML = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2400);
}
function initials(name) {
return name
.trim()
.split(/\s+/)
.slice(0, 2)
.map(function (p) {
return p.charAt(0).toUpperCase();
})
.join("");
}
/* ---------- render ---------- */
function render() {
rosterEl.innerHTML = "";
var q = searchEl.value.trim().toLowerCase();
var shown = 0;
members.forEach(function (m) {
var li = document.createElement("li");
li.className = "row";
li.dataset.id = String(m.id);
applyStateClass(li, m.state);
var matchesQ =
!q || m.name.toLowerCase().indexOf(q) > -1 || m.tier.toLowerCase().indexOf(q) > -1;
var matchesFilter = !pendingOnly || m.state === "expected";
if (!matchesQ || !matchesFilter) {
li.hidden = true;
} else {
shown++;
}
var colors = TIER_COLORS[m.tier] || TIER_COLORS["Day Pass"];
var avatar = document.createElement("div");
avatar.className = "avatar";
avatar.style.background = "linear-gradient(135deg," + colors[0] + "," + colors[1] + ")";
avatar.textContent = initials(m.name);
avatar.setAttribute("aria-hidden", "true");
var who = document.createElement("div");
who.className = "who";
var nm = document.createElement("div");
nm.className = "who__name";
nm.textContent = m.name;
var sub = document.createElement("div");
sub.className = "who__sub";
var tier = document.createElement("span");
tier.className = "tier tier--" + m.tier.replace(/\s+/g, "");
tier.textContent = m.tier;
var tag = document.createElement("span");
tag.className = "state-tag";
tag.textContent = STATE_LABEL[m.state];
sub.appendChild(tier);
sub.appendChild(tag);
who.appendChild(nm);
who.appendChild(sub);
var actions = document.createElement("div");
actions.className = "actions";
var present = m.state === "present" || m.state === "promoted";
var presentBtn = document.createElement("button");
presentBtn.type = "button";
presentBtn.className = "tog" + (present ? " tog--present" : "");
presentBtn.textContent = present ? "Checked in" : "Check in";
presentBtn.setAttribute("aria-pressed", present ? "true" : "false");
presentBtn.addEventListener("click", function () {
togglePresent(m.id);
});
var missBtn = document.createElement("button");
missBtn.type = "button";
missBtn.className = "tog tog--miss";
missBtn.textContent = "No-show";
missBtn.dataset.active = m.state === "noshow" ? "true" : "false";
missBtn.setAttribute("aria-pressed", m.state === "noshow" ? "true" : "false");
missBtn.addEventListener("click", function () {
toggleNoShow(m.id);
});
actions.appendChild(presentBtn);
actions.appendChild(missBtn);
li.appendChild(avatar);
li.appendChild(who);
li.appendChild(actions);
rosterEl.appendChild(li);
});
if (shown === 0) {
var empty = document.createElement("li");
empty.className = "empty";
empty.textContent = pendingOnly
? "No pending members — everyone's accounted for."
: "No members match your search.";
rosterEl.appendChild(empty);
}
updateStats();
}
function applyStateClass(li, state) {
li.classList.toggle("is-present", state === "present");
li.classList.toggle("is-noshow", state === "noshow");
li.classList.toggle("is-promoted", state === "promoted");
}
function find(id) {
for (var i = 0; i < members.length; i++) {
if (members[i].id === id) return members[i];
}
return null;
}
function flash(id) {
var row = rosterEl.querySelector('.row[data-id="' + id + '"]');
if (!row) return;
row.classList.remove("flash");
void row.offsetWidth; // reflow to restart animation
row.classList.add("flash");
}
/* ---------- actions ---------- */
function togglePresent(id) {
var m = find(id);
if (!m) return;
if (m.state === "present" || m.state === "promoted") {
m.state = "expected";
toast(m.name + " set back to expected");
} else {
m.state = "present";
toast('<span class="toast__accent">✓</span> ' + m.name + " checked in");
}
render();
flash(id);
}
function toggleNoShow(id) {
var m = find(id);
if (!m) return;
if (m.state === "noshow") {
m.state = "expected";
toast(m.name + " cleared");
} else {
m.state = "noshow";
toast(m.name + " marked no-show");
}
render();
flash(id);
}
/* ---------- stats ---------- */
function updateStats() {
var present = 0,
expected = 0,
noshow = 0,
wait = 0;
members.forEach(function (m) {
if (m.state === "present") present++;
else if (m.state === "promoted") {
present++;
wait++;
} else if (m.state === "noshow") noshow++;
else expected++;
});
$("#statPresent").textContent = present;
$("#statExpected").textContent = expected;
$("#statNoShow").textContent = noshow;
$("#statWait").textContent = wait;
var enrolled = members.length;
$("#capCurrent").textContent = enrolled;
var pct = Math.min(100, Math.round((enrolled / CAPACITY) * 100));
$("#capFill").style.width = pct + "%";
var bar = $("#capBar");
bar.setAttribute("aria-valuenow", String(enrolled));
bar.setAttribute("aria-valuemax", String(CAPACITY));
}
/* ---------- toolbar ---------- */
searchEl.addEventListener("input", render);
filterBtn.addEventListener("click", function () {
pendingOnly = !pendingOnly;
filterBtn.setAttribute("aria-pressed", pendingOnly ? "true" : "false");
render();
});
checkAllBtn.addEventListener("click", function () {
var changed = 0;
members.forEach(function (m) {
if (m.state === "expected" || m.state === "noshow") {
m.state = "present";
changed++;
}
});
render();
if (changed === 0) {
toast("Everyone's already checked in");
} else {
toast('<span class="toast__accent">✓</span> Checked in ' + changed + " member" + (changed === 1 ? "" : "s"));
}
});
/* ---------- walk-in ---------- */
$("#walkinForm").addEventListener("submit", function (e) {
e.preventDefault();
var nameEl = $("#walkinName");
var name = nameEl.value.trim();
if (!name) {
nameEl.focus();
toast("Enter a name for the walk-in");
return;
}
if (members.length >= CAPACITY) {
toast("Class is at capacity (" + CAPACITY + ")");
return;
}
var tier = $("#walkinTier").value;
var id = nextId++;
members.push({ id: id, name: name, tier: tier, state: "present" });
nameEl.value = "";
pendingOnly = false;
filterBtn.setAttribute("aria-pressed", "false");
searchEl.value = "";
render();
flash(id);
toast('<span class="toast__accent">+</span> ' + name + " added and checked in");
});
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Class Roster — Attendance Check-in</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;900&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell">
<section class="card" aria-labelledby="class-title">
<header class="hero">
<div class="hero__top">
<span class="eyebrow">Live Class · Studio A</span>
<span class="pill pill--live" aria-label="Class in progress">
<span class="dot" aria-hidden="true"></span> In progress
</span>
</div>
<h1 id="class-title">Sunrise HIIT 45</h1>
<div class="hero__meta">
<span class="meta"><strong>06:30</strong> AM · 45 min</span>
<span class="meta meta--sep">Coach <strong>Mara Velez</strong></span>
<span class="meta meta--sep">Studio A</span>
</div>
<div class="capacity" role="group" aria-label="Class capacity">
<div class="capacity__head">
<span class="capacity__label">Capacity</span>
<span class="capacity__count">
<span id="capCurrent">18</span><span class="capacity__max">/20</span>
</span>
</div>
<div
class="capacity__bar"
role="progressbar"
aria-valuemin="0"
aria-valuemax="20"
aria-valuenow="18"
id="capBar"
>
<span class="capacity__fill" id="capFill"></span>
</div>
</div>
</header>
<div class="stats" aria-live="polite">
<div class="stat stat--present">
<span class="stat__num" id="statPresent">0</span>
<span class="stat__label">Present</span>
</div>
<div class="stat stat--expected">
<span class="stat__num" id="statExpected">0</span>
<span class="stat__label">Expected</span>
</div>
<div class="stat stat--noshow">
<span class="stat__num" id="statNoShow">0</span>
<span class="stat__label">No-show</span>
</div>
<div class="stat stat--wait">
<span class="stat__num" id="statWait">0</span>
<span class="stat__label">Waitlist</span>
</div>
</div>
<div class="toolbar">
<div class="search">
<svg class="search__icon" viewBox="0 0 24 24" aria-hidden="true">
<path
d="M21 21l-4.3-4.3M11 19a8 8 0 1 1 0-16 8 8 0 0 1 0 16z"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
<input
type="search"
id="search"
placeholder="Search members or tier…"
aria-label="Search members"
autocomplete="off"
/>
</div>
<button type="button" class="btn btn--ghost" id="filterToggle" aria-pressed="false">
Pending only
</button>
<button type="button" class="btn btn--neon" id="checkAll">Check in all</button>
</div>
<ul class="roster" id="roster" aria-label="Member roster"></ul>
<form class="walkin" id="walkinForm" autocomplete="off">
<div class="walkin__avatar" aria-hidden="true">+</div>
<input
type="text"
id="walkinName"
class="walkin__input"
placeholder="Add a walk-in member…"
aria-label="Walk-in member name"
/>
<select id="walkinTier" class="walkin__select" aria-label="Walk-in membership tier">
<option value="Day Pass">Day Pass</option>
<option value="Core">Core</option>
<option value="Plus">Plus</option>
<option value="Elite">Elite</option>
</select>
<button type="submit" class="btn btn--orange">Add walk-in</button>
</form>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Class Roster
A front-desk attendance panel for a live group class, built in the high-energy performance-gym look. The header leads with a bold class title, the start time, duration and coach, plus a neon capacity bar that reflects how full the session is. A four-up stat strip tracks Present, Expected, No-show and Waitlist counts that recalculate the moment any row changes.
Each member row carries a gradient avatar, name and a colour-coded membership tier (Day Pass, Core, Plus, Elite). Two pill toggles drive the row state: Check in flips a member to present and paints the row with a neon left rail, while No-show dims and flags them. Waitlist-promoted members get an amber accent. Every toggle triggers a quick flash micro-interaction and a toast confirmation.
A search box filters by name or tier, a Pending only toggle hides everyone already accounted for, Check in all sweeps the remaining members in one tap, and the walk-in row appends a new member already checked in — respecting the class capacity limit. Everything runs on vanilla JS with no dependencies.
Illustrative UI only — names, coaches and members are fictional.