MCPInstall

static-spa-interactions

Use this skill when a vanilla static SPA scaffold exists and needs to feel like a polished product — progress persistence, sidebar + scrollspy navigation, dark/light theme, content zoom, responsive (mobile sidebar overlay), keyboard nav, fade-in entrances, toast notifications, iframe modal viewers, quiz scoring with section back-links. Triggers on phrases like "加進度勾選", "響應式", "暗色模式", "縮放", "scrollspy", "sidebar 收合", "手機版", "RWD", "dark mode", "progress tracking", "quiz UX", "interactive polish". Always invoke AFTER static-spa-conversion (renders working), as a standalone enhancement layer.

Installs
64
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/kevintsai1202/teaching-site-skills --skill static-spa-interactions
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads static-spa-interactions automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

Static SPA Interactions

Schema authority: the Task primitive (id-as-localStorage-key rules) and the QuizItem primitive (storage of selected option) come from _shared/domain-primitives.md §8 and §10. localStorage schema lives in §8.

Reference implementation: d:/GitHub/ai-workshop/index.html:2900-3030 for renderSidebar / setupScrollSpy / applyTheme / setupFadeIn patterns.

This skill adds the interaction layer to a vanilla SPA: state, navigation, theme, responsiveness, accessibility. Each pattern below is a self-contained module that can be added independently.

The patterns here are documented because they all hide subtle bugs that took real time to find in the example workshop. Read the "Why" notes carefully — they're not optional context.

Pattern 1: localStorage Progress (Task Checkboxes + Quiz Answers)

const STORAGE_KEY = 'your-app-progress-v1';   // version suffix lets you breaking-change later

const store = {
  load() {
    try {
      return JSON.parse(localStorage.getItem(STORAGE_KEY)) || { tasks: {}, quiz: {}, theme: 'light' };
    } catch { return { tasks: {}, quiz: {}, theme: 'light' }; }
  },
  save(state) { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); },
  reset() { localStorage.removeItem(STORAGE_KEY); location.reload(); }
};

Why a version suffix in the key: when you later realise the schema needs to change, increment the suffix (-v2). Old keys remain harmless; new visitors get fresh state. Renaming the key wipes everyone — only do that intentionally.

Why try/catch around JSON.parse: a corrupted entry (e.g. user opened devtools and edited) will crash init() and break the whole page.

Pattern 2: Sidebar — Two Modes, Don't Merge Them

The sidebar has opposite default states on desktop vs. mobile. Trying to express this with a single CSS class is the most common bug.

:root { --sidebar-w: 280px; }

/* Desktop: sidebar visible by default */
.sidebar { position: fixed; left: 0; top: 0; width: var(--sidebar-w); height: 100vh; }
.main { margin-left: var(--sidebar-w); transition: margin-left .2s ease; }
.app.sidebar-closed .sidebar { transform: translateX(-100%); }
.app.sidebar-closed .main { margin-left: 0; }

@media (max-width: 768px) {
  /* Mobile: sidebar hidden by default */
  .sidebar { transform: translateX(-100%); }
  .main { margin-left: 0; }
  .app.sidebar-open .sidebar { transform: translateX(0); }
  .sidebar-backdrop { /* overlay shown only when sidebar-open */ }
}
const mql = matchMedia('(max-width: 768px)');
function toggleSidebar() {
  if (mql.matches) document.querySelector('.app').classList.toggle('sidebar-open');
  else document.querySelector('.app').classList.toggle('sidebar-closed');
}
// On viewport crossing 768px, clear both classes so the new viewport's default takes over:
mql.addEventListener('change', () => {
  document.querySelector('.app').classList.remove('sidebar-open', 'sidebar-closed');
});

Why two classes: on desktop, "closed" means transformed-out; on mobile, "open" means transformed-in. Same class would have opposite CSS rules per viewport — unmaintainable.

Why position: fixed not sticky: any ancestor with overflow: hidden breaks sticky. fixed is immune. (Bonus: overflow-x: hidden implicitly forces overflow-y: auto, making the window non-scrollable — see Pattern 9.)

Pattern 3: ScrollSpy (Auto-highlight Current Section in Sidebar)

function setupScrollSpy() {
  const sections = document.querySelectorAll('[data-section-id]');
  const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const id = entry.target.dataset.sectionId;
        document.querySelectorAll('.sidebar-link').forEach(a => a.classList.toggle('active', a.dataset.target === id));
      }
    });
  }, { rootMargin: '-30% 0px -60% 0px', threshold: 0 });
  sections.forEach(s => observer.observe(s));
}

Why rootMargin: '-30% 0px -60% 0px': it creates a 10%-tall "active zone" in the upper third of the viewport. The section centred in that zone is considered "current". Pure threshold alone gives jittery results.

Pattern 4: Theme Toggle (Dark / Light)

function applyTheme(theme) {
  document.documentElement.dataset.theme = theme;
  const s = store.load(); s.theme = theme; store.save(s);
}
applyTheme(store.load().theme);  // on load

CSS uses [data-theme="dark"] attribute selector on :root. No prefers-color-scheme fallback unless the user explicitly wants it — the toggle is the source of truth.

Pattern 5: Content Zoom (Don't Apply to Sidebar!)

:root { --content-zoom: 1; }
.content { zoom: var(--content-zoom); }
/* Sidebar is OUTSIDE .content — never inherits zoom */
function applyZoom(value) {
  if (mql.matches) {
    // On mobile, kill the CSS variable so @media rules govern
    document.documentElement.style.removeProperty('--content-zoom');
  } else {
    document.documentElement.style.setProperty('--content-zoom', value);
  }
}

Why never html { zoom: X }: CSS zoom multiplies across descendants. If html is 1.35× and .content is 1.25×, the actual content is 1.6875×. Restrict zoom to one layer.

Why kill zoom on mobile: phones don't need to upscale, and the desktop's 1.35 would make text comically large. The @media (max-width: 768px) :root { --content-zoom: 1 } only applies if no inline style overrides it — so removeProperty() is necessary, not just setting it back to 1.

Pattern 6: Accordion + Fade-in Entrance

function setupFadeIn() {
  const observer = new IntersectionObserver(entries => {
    entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); });
  }, { threshold: 0.15 });
  document.querySelectorAll('.fade-in').forEach(el => observer.observe(el));
}
.fade-in { opacity: 0; transform: translateY(20px); transition: opacity .5s, transform .5s; }
.fade-in.visible { opacity: 1; transform: translateY(0); }

Pitfall: if the section uses zoom and IntersectionObserver gets confused about visibility, the opacity: 0 element stays invisible forever. Use @keyframes instead of opacity: 0 → 1 transitions when zoom is in play (the example workshop hit this and switched the Day hero numbers to keyframes).

Pattern 7: Copy-to-Clipboard Buttons

async function copyPrompt(text, btn) {
  try {
    await navigator.clipboard.writeText(text);
    showToast('已複製');
  } catch {
    // Fallback for non-secure context (e.g. file://)
    const ta = document.createElement('textarea');
    ta.value = text; document.body.appendChild(ta);
    ta.select(); document.execCommand('copy');
    ta.remove();
    showToast('已複製');
  }
}

The fallback matters because the SPA might be opened from file:// in some contexts (offline zip delivery) where navigator.clipboard is gated.

Pattern 8: Toast (Minimal)

function showToast(msg) {
  const t = document.createElement('div');
  t.className = 'toast'; t.textContent = msg;
  document.body.appendChild(t);
  setTimeout(() => t.classList.add('show'), 10);
  setTimeout(() => { t.classList.remove('show'); setTimeout(() => t.remove(), 300); }, 1800);
}

Pattern 9: Quiz with Source-Chapter Back-link

Each quiz item has a sourceUnit field from course-content-authoring. After submission, wrong-answer rows include a "去複習 →" link that scrolls to that unit:

function gradeQuiz() {
  const state = store.load();
  const results = window.COURSE.quiz.map(q => ({
    q, given: state.quiz[q.id], correct: q.answer, isWrong: state.quiz[q.id] !== q.answer
  }));
  // Render with back-links for wrong ones; scroll behaviour:
  // document.querySelector(`[data-section-id="${q.sourceUnit}"]`).scrollIntoView({ behavior: 'smooth' });
}

The N hardcoded places trap: total question count appears in (1) section title (2) lead paragraph (3) score display "— / N" (4) "你答對 N 題" toast (5) passing threshold s >= K. When quiz count changes, audit all five. Consider computing from window.COURSE.quiz.length instead — most can be derived.

Pattern 10: iframe Modal for Material Preview

Instead of opening materials in a new tab (loses progress context), embed them in a modal:

function openViewer(url) {
  const modal = document.createElement('div'); modal.className = 'viewer-modal';
  modal.innerHTML = `<div class="viewer-backdrop"></div><div class="viewer-frame">
    <button class="viewer-close">×</button>
    <iframe src="${url}" loading="lazy"></iframe>
  </div>`;
  document.body.appendChild(modal);
  modal.querySelector('.viewer-close').onclick = () => modal.remove();
  modal.querySelector('.viewer-backdrop').onclick = () => modal.remove();
  document.addEventListener('keydown', function esc(e) {
    if (e.key === 'Escape') { modal.remove(); document.removeEventListener('keydown', esc); }
  });
}

For PDFs that should download, bypass the modal: render <a href="..." download> directly so the browser triggers a save dialog. Markdown / HTML / TXT → modal; PDF → download. This branching belongs in getMaterialUrl()'s caller.

Pattern 11: Body Scroll Lock When Modal Open

function openViewer(url) { /* ... */ document.body.style.overflow = 'hidden'; }
function closeViewer() { document.body.style.overflow = ''; }

Without this, scrolling the modal scrolls the page underneath on macOS Safari.

Verification

Don't bake verification scripts into this skill — they have their own dedicated skill. After wiring any interaction pattern above, invoke web-visual-verification to produce the matching verify script (verify-rwd / verify-progress / verify-quiz / verify-modal, etc.). That skill documents the four script roles (verify / capture / diagnose / probe), reusable assertions, and multi-viewport patterns.

The short version: every interaction pattern in this skill has a corresponding verify script. Don't ship interactions without one.

Hand-off

Tell the user: "interaction layer complete. Open in browser, click around, then run the verify scripts. Next stage (web-visual-assets) fills in the artwork — your page probably has missing or placeholder images right now."

FAQ

What does the static-spa-interactions skill do?

Use this skill when a vanilla static SPA scaffold exists and needs to feel like a polished product — progress persistence, sidebar + scrollspy navigation, dark/light theme, content zoom, responsive (mobile sidebar overlay), keyboard nav, fade-in entrances, toast notifications, iframe modal viewers, quiz scoring with section back-links. Triggers on phrases like "加進度勾選", "響應式", "暗色模式", "縮放", "scrollspy", "sidebar 收合", "手機版", "RWD", "dark mode", "progress tracking", "quiz UX", "interactive polish". Always invoke AFTER static-spa-conversion (renders working), as a standalone enhancement layer.

How do I install static-spa-interactions?

Run npx skills add https://github.com/kevintsai1202/teaching-site-skills --skill static-spa-interactions in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/static-spa-interactions/ (personal) or .claude/skills/static-spa-interactions/ (project).

Which AI agents can use static-spa-interactions?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is static-spa-interactions safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains static-spa-interactions?

It is published by kevintsai1202 in kevintsai1202/teaching-site-skills. It has 64 recorded installs.

More from kevintsai1202/teaching-site-skills

View all →

course-outline-design

kevintsai1202/teaching-site-skills

Use this skill when starting a new course / workshop / training program and you need to design its outline before any lecture content is written. Triggers on phrases like "規劃課程", "課程大綱", "幾天怎麼排", "學習目標", "workshop outline", "syllabus design", "course outline", "learning objectives", "課程總覽", or when the user has a topic and audience but no structure yet. This skill produces the skeleton (.md files) that all downstream stages — content authoring, SPA conversion, visual assets — depend on. Always invoke this BEFORE jumping into writing lecture notes or building a site.

course-ebook-publishing

kevintsai1202/teaching-site-skills

Use this skill ONLY AFTER a teaching site is feature-complete and stable, when the user wants to produce a printable / archivable / shareable version — typically a PDF ebook with cover, page numbers, table of contents, or a DOCX for editorial review. Triggers on phrases like "做電子書", "產 PDF", "ebook", "PDF ebook", "印給學員", "課程講義 PDF", "course handbook", "DOCX deliverable", "book-style layout", "build:ebook". Do NOT invoke during early site development — this is a downstream consumer that breaks if the site changes underneath. The skill produces PDF via Playwright page.pdf() and DOCX via pandoc, both from a single composed markdown source.

Docs & Writing77 installs

course-content-authoring

kevintsai1202/teaching-site-skills

Use this skill when an outline exists and you need to fill in the actual teaching content — lecture notes, sample materials (CSV / YAML / MD samples), quiz items, and any course-specific artifacts like prompt templates or worksheets. Triggers on phrases like "寫講義", "補素材", "出測驗題", "提示詞範本", "撰寫課程內容", "lecture notes", "course material", "quiz questions", "quiz authoring", "prompt templates". Always invoke AFTER course-outline-design is stable, BEFORE static-spa-conversion. This skill produces only the raw .md/.csv/.yaml artifacts — turning them into a website is the next stage's job.

course-corporate-edition

kevintsai1202/teaching-site-skills

Use this skill when an existing multi-day public-class teaching site needs to be condensed and re-packaged for a corporate in-house training — typically a half-day or one-day intensive (e.g. 4-day public class → 6-hour corporate). Triggers on phrases like "企業包班", "濃縮版", "客製化課程", "corporate edition", "intensive version", "in-house training", "single-file deliverable", "客戶端課程", "壓縮成一天", or when the user wants a single-folder zippable deliverable for a client. This skill builds on top of an existing site — picking units, condensing time allocation, embedding data into a single HTML, setting up asset fallback chains, and producing an offline-ready package.

teaching-site-design-system

kevintsai1202/teaching-site-skills

Use this skill whenever you're making visual / styling decisions on a teaching site or content-driven SPA — picking colors, fonts, badge styles, card layouts, hero treatments, dark mode tokens, or any "how should this look?" question. Triggers on phrases like "視覺風格", "設計系統", "色票", "字體", "暗色模式", "玻璃卡片", "提示詞徽章", "Day hero 怎麼設計", "design tokens", "design system", "color system", "glass cards", "typography", "look and feel", "視覺一致性", or any moment when the user evaluates aesthetics rather than behaviour. This is the cross-cutting visual authority — every stage (SPA, interactions, corporate edition, ebook) reads tokens from here so the brand stays coherent across formats.

teaching-site

kevintsai1202/teaching-site-skills

Use this skill as the main entry point whenever the user wants to build, plan, or evolve an interactive teaching website / course microsite / workshop landing page — from a blank slate, from existing course materials, or any state in between. Triggers on broad phrases like "做課程網站", "做教學網頁", "做工作坊網站", "把講義變網頁", "course microsite", "workshop site", "interactive lesson page", "multi-day curriculum website", "做一套課程". This skill is the router for the whole teaching-site pipeline (outline → content → SPA → interactions → visuals → corporate / ebook) and dispatches to the 10 specialised sub-skills as needed. Prefer this when the user's request is broad or unclear about which stage they're at — sub-skills (e.g. course-ebook-publishing) are still triggerable directly for stage-specific requests.

Popular frontend & design skills

View category →

setup-matt-pocock-skills

mattpocock/skills

Configure this repo for the engineering skills: set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.

Frontend & Design1.1M installs

frontend-design

anthropics/skills

Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.

OfficialFrontend & Design1M installs

hyperframes-animation

heygen-com/hyperframes

All animation knowledge for HyperFrames — atomic motion rules, multi-phase scene blueprints, scene transitions, broader motion-design techniques, AND the seven runtime adapters (GSAP default, plus Lottie, Three.js, Anime.js, CSS keyframes, Web Animations API, TypeGPU). Use for any motion or animation task: pick 2-4 rules and compose, or load a blueprint, or look up runtime-specific API (e.g. GSAP eases / Lottie player / Three.js mixer). Also covers auditing an existing composition's choreography (animation map) and 24 named text-animation effects. HyperFrames-native: single paused timeline, seek-safe, deterministic.

prototype

mattpocock/skills

Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.

Frontend & Design999.2K installs

hyperframes-core

heygen-com/hyperframes

The HyperFrames composition contract — build one renderable project. Use for composition structure, the data- timing attributes, class="clip", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.

Frontend & Design930.5K installs

vercel-react-best-practices

vercel-labs/agent-skills

React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.

OfficialFrontend & Design843.1K installs