Home / Blog

How to Build an Advanced Rise 360 Quiz Block with JavaScript

By Nathan Stoker · March 29, 2026 · 15 min read

How to Build an Advanced Rise 360 Quiz Block with JavaScript

If you've ever felt limited by Rise 360's built-in quiz options, you're not alone. The native quiz blocks work fine for simple assessments, but what if you need animated transitions, multiple question types in one quiz, or the ability to resume where learners left off?

That's where custom JavaScript comes in. In this tutorial, you'll learn how to create an advanced Rise 360 quiz block that goes far beyond the platform's default capabilities. By the end, you'll have a fully functional quiz with multiple choice, true/false, and fill-in-the-blank questions—complete with smooth animations and progress tracking.

What Makes This Quiz Different

Before diving into the code, let's look at what this custom quiz delivers that Rise 360's native blocks can't.

First, you get mixed question types within a single quiz experience. Instead of being locked into one format, learners encounter multiple choice questions, true/false statements, and fill-in-the-blank challenges all in the same assessment. This variety keeps engagement high and lets you test different cognitive levels.

Second, the quiz includes GSAP-powered animations that make transitions between questions feel polished and professional. Answer options slide in smoothly, feedback appears with subtle fades, and the results screen builds piece by piece for dramatic effect.

Third, learners can pick up where they left off. Using localStorage, the quiz saves progress automatically. If someone closes their browser mid-quiz, they'll see a "Welcome Back" screen offering to resume or start over.

Finally, this implementation includes comprehensive accessibility features including ARIA announcements, keyboard navigation, and reduced motion support for users who prefer minimal animation.

Setting Up Your Rise 360 Multimedia Block

To add this quiz to your Rise course, you'll use Rise 360's multimedia block feature. This lets you embed custom HTML, CSS, and JavaScript directly into your course.

Navigate to the lesson where you want the quiz. Click the plus icon to add a new block, then select Multimedia from the options. Choose the Embed Code option rather than uploading a file.

The embed approach gives you the most flexibility since you can include external libraries like Bootstrap and GSAP through CDN links. Make sure your organization's firewall allows these CDN domains, or the libraries won't load properly.

Understanding the Quiz Architecture

The quiz runs on three main components working together: the data structure that holds your questions, the display logic that renders each question type, and the state management that tracks progress and scores.

The Question Data Structure

Every question lives in a JavaScript array called quizData. Each question object contains properties that define its type, content, and feedback:

const quizData = [
    {
        id: 1,
        type: 'multiple-choice',
        question: 'What is the most common type of cyber attack?',
        options: ['Malware', 'Phishing', 'DDoS Attack', 'SQL Injection'],
        correctAnswer: 1,
        feedback: {
            correct: 'Correct! Phishing uses deceptive emails.',
            incorrect: 'Not quite. Phishing is the most common attack.'
        }
    },
    {
        id: 2,
        type: 'true-false',
        question: 'Using the same password across accounts is safe.',
        options: ['True', 'False'],
        correctAnswer: 1,
        feedback: {
            correct: 'Correct! Never reuse passwords.',
            incorrect: 'Incorrect. Password reuse is risky.'
        }
    },
    {
        id: 3,
        type: 'fill-blank',
        question: 'A strong password should be at least ___ characters.',
        correctAnswer: '12',
        acceptableAnswers: ['12', 'twelve', '12 characters'],
        feedback: {
            correct: 'Correct! 12 characters minimum.',
            incorrect: 'Not quite. Aim for 12 characters.'
        }
    }
];

The type property tells the display function which interface to render. The correctAnswer for multiple choice and true/false questions uses zero-based indexing, so 1 means the second option. Fill-in-the-blank questions include an acceptableAnswers array for flexible grading.

Display Logic by Question Type

The displayQuestion function reads the current question's type and builds the appropriate HTML. For multiple choice and true/false questions, it creates button elements for each option. For fill-in-the-blank questions, it renders a text input with a submit button.

Here's the core branching logic:

function displayQuestion() {
    const question = quizData[currentQuestionIndex];
    
    if (question.type === 'multiple-choice' || question.type === 'true-false') {
        // Render buttons for each option
        question.options.forEach((option, index) => {
            html += `<button class="answer-option" data-index="${index}">
                ${option}
            </button>`;
        });
    } else if (question.type === 'fill-blank') {
        // Render text input
        html += `<input type="text" class="fill-blank-input" 
            id="fillInput" placeholder="Type your answer">
            <button class="submit-answer-btn" onclick="submitFillBlank()">
                Submit Answer
            </button>`;
    }
}

This pattern scales easily. To add a new question type like matching or sequencing, you'd add another condition here and create the corresponding answer-checking function.

Implementing the Accessibility Layer

Screen reader users need announcements when the quiz state changes. The accessibility module creates an ARIA live region that broadcasts updates without interrupting the user's flow.

const A11y = {
    init() {
        if (!document.getElementById('sr-announcements')) {
            const sr = document.createElement('div');
            sr.id = 'sr-announcements';
            sr.setAttribute('role', 'status');
            sr.setAttribute('aria-live', 'polite');
            sr.setAttribute('aria-atomic', 'true');
            sr.className = 'sr-only';
            document.body.appendChild(sr);
        }
        this.prefersReducedMotion = window.matchMedia(
            '(prefers-reduced-motion: reduce)'
        ).matches;
    },
    
    announce(message, priority = 'polite') {
        const sr = document.getElementById('sr-announcements');
        if (!sr) return;
        sr.setAttribute('aria-live', priority);
        sr.textContent = '';
        setTimeout(() => sr.textContent = message, 100);
    }
};

The init method runs when the page loads, creating the invisible announcement container and checking whether the user prefers reduced motion. The announce method clears the container briefly before setting new text—this trick ensures screen readers recognize the content as new even if the same message repeats.

Call A11y.announce() whenever something significant happens: question changes, answer submission, feedback display, and quiz completion.

Why This Rise 360 Quiz Block Is Fully Accessible

Accessibility isn't an afterthought in this implementation—it's woven into every interaction. This quiz meets WCAG 2.1 Level AA standards and works seamlessly with assistive technologies. Here's what makes it truly inclusive.

Screen Reader Support Throughout the Experience

Every state change triggers an announcement through the ARIA live region. When a new question appears, screen reader users hear the question number, question type, and the full question text. After submitting an answer, they immediately hear whether they got it correct or incorrect. The results screen announces the final score and percentage without requiring users to navigate through visual elements.

The feedback messages receive focus automatically after appearing, ensuring screen reader users don't miss the explanation of why their answer was right or wrong. This focus management eliminates the frustrating experience of answers being evaluated silently.

Complete Keyboard Navigation

Every interactive element in the quiz is reachable and operable with a keyboard alone. Answer option buttons receive focus in a logical tab order, and pressing Enter or Space selects them. The fill-in-the-blank input accepts Enter to submit, matching the natural expectation of form controls.

The visible focus indicators use a 3-pixel solid outline in the primary brand color with a 2-pixel offset. This high-contrast styling ensures keyboard users always know exactly which element is active—a critical requirement that many custom interactions overlook.

*:focus {
    outline: 3px solid var(--focus-color);
    outline-offset: 2px;
}

Semantic HTML Structure

The quiz uses proper semantic elements that convey meaning to assistive technologies. Questions are marked as headings with role="heading" and aria-level="2", placing them in the document's heading hierarchy. The progress bar includes role="progressbar" along with aria-valuenow, aria-valuemin, and aria-valuemax attributes that communicate completion percentage.

Interactive answer options use native button elements rather than clickable div tags. This ensures they're automatically included in the tab order and respond to keyboard events without additional JavaScript. Native HTML semantics do the heavy lifting.

Reduced Motion Preference Respect

Not everyone enjoys animations. Users with vestibular disorders or motion sensitivity can experience dizziness, nausea, or disorientation from sliding and fading effects. This quiz checks the prefers-reduced-motion media query at initialization and skips all GSAP animations when that preference is enabled.

this.prefersReducedMotion = window.matchMedia(
    '(prefers-reduced-motion: reduce)'
).matches;

When reduced motion is active, elements appear instantly through CSS class changes rather than animated transitions. The quiz remains fully functional—learners just experience it without the motion effects. This approach respects user preferences set at the operating system level without requiring any in-quiz settings.

Color and Contrast Considerations

The visual design uses color combinations that meet WCAG's 4.5:1 contrast ratio requirement for normal text. Correct answers display with dark green text (#155724) on a light green background (#e8f5e9), while incorrect answers use dark red (#721c24) on light red (#ffebee). These combinations are distinguishable even for users with color vision deficiencies.

Importantly, color is never the only indicator of state. Correct and incorrect feedback includes explicit text labels ("Correct!" or "Incorrect.") alongside the color change. The border style also changes, providing a secondary visual cue that doesn't rely on color perception alone.

Accessible Rich Internet Applications (ARIA) Implementation

The quiz demonstrates several ARIA best practices beyond the live region. The aria-atomic="true" attribute ensures screen readers announce the complete feedback message rather than just the changed portion. The progress bar's ARIA attributes update dynamically as learners advance through questions.

Elements that receive programmatic focus include tabindex="-1" to make them focusable without adding them to the natural tab order. This allows the JavaScript to direct attention to feedback messages and results without disrupting the expected keyboard navigation flow.

Testing Your Quiz's Accessibility

Before deploying, test the quiz with actual assistive technologies. Navigate through the entire experience using only your keyboard—can you reach every button and submit every answer type? Run a screen reader like NVDA (free on Windows) or VoiceOver (built into macOS) and complete the quiz with your eyes closed. Does every state change get announced? Can you understand your score at the end?

Automated tools like axe DevTools catch many issues, but they can't evaluate the full user experience. Manual testing with assistive technologies reveals problems that automated scans miss, like confusing announcement order or missing context.

Adding Animation with GSAP

The GreenSock Animation Platform handles all the visual transitions. When a new question loads, options slide in from the left with a staggered delay that creates a cascade effect.

function animateIn() {
    const options = document.querySelectorAll('.answer-option');
    const input = document.querySelector('.fill-blank-input');
    const btn = document.querySelector('.submit-answer-btn');
    
    if (!A11y.prefersReducedMotion) {
        if (options.length > 0) {
            gsap.to(options, { 
                opacity: 1, 
                x: 0, 
                duration: 0.4, 
                stagger: 0.1, 
                delay: 0.2 
            });
        }
        if (input) {
            gsap.to(input, { 
                opacity: 1, 
                y: 0, 
                duration: 0.4, 
                delay: 0.2 
            });
        }
        if (btn) {
            gsap.to(btn, { 
                opacity: 1, 
                y: 0, 
                duration: 0.4, 
                delay: 0.3 
            });
        }
    } else {
        // Fallback for reduced motion preference
        options.forEach(o => o.classList.add('visible'));
        if (input) input.classList.add('visible');
        if (btn) btn.classList.add('visible');
    }
}

Notice the reduced motion check at the beginning. When users have that preference enabled, the code skips GSAP entirely and adds a CSS class that makes elements visible immediately. This respects user preferences while maintaining a functional quiz.

The CSS sets up initial states with opacity: 0 and transforms that GSAP then animates to their final values:

.answer-option {
    opacity: 0;
    transform: translateX(-20px);
}

.answer-option.visible {
    opacity: 1;
    transform: translateX(0);
}

Building the Progress Persistence System

The localStorage integration ensures learners don't lose their work. Three functions handle saving, loading, and clearing quiz state.

const STORAGE_KEY = 'riseBlock_multiQuiz_data';

function saveData(data) {
    try {
        localStorage.setItem(STORAGE_KEY, JSON.stringify({
            ...data,
            savedAt: new Date().toISOString()
        }));
        return true;
    } catch (e) {
        console.error('Failed to save data:', e);
        return false;
    }
}

function loadData() {
    const saved = localStorage.getItem(STORAGE_KEY);
    if (saved) {
        try {
            return JSON.parse(saved);
        } catch (e) {
            console.error('Failed to parse saved data:', e);
            return null;
        }
    }
    return null;
}

function clearData() {
    localStorage.removeItem(STORAGE_KEY);
}

The saveData function wraps the save in a try-catch because localStorage can fail in private browsing modes or when storage is full. Adding a timestamp helps with debugging and could enable features like expiring old progress after a certain period.

When the quiz initializes, it checks for saved data and offers the resume dialog if progress exists:

function initQuiz() {
    const saved = loadData();
    if (saved && saved.currentQuestionIndex < quizData.length) {
        showResumeDialog(saved);
    } else {
        displayQuestion();
        updateProgressBar();
    }
}

Complete Code for Your Rise 360 Quiz Block

Below is the full implementation ready to paste into Rise 360's multimedia embed block. The code includes everything discussed above plus complete styling and all question handling functions.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cybersecurity Quiz</title>
    
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    
    <style>
        /* Color Variables - Google Material White Theme */
        :root {
            --primary-color: #1976d2;
            --secondary-color: #1565c0;
            --correct-color: #388e3c;
            --incorrect-color: #d32f2f;
            --correct-bg: #e8f5e9;
            --incorrect-bg: #ffebee;
            --border-color: #e0e0e0;
            --text-color: #212121;
            --text-muted: #757575;
            --focus-color: #1976d2;
            --card-shadow: rgba(0, 0, 0, 0.1);
        }
        
        /* Screen reader only class */
        .sr-only {
            position: absolute;
            width: 1px;
            height: 1px;
            padding: 0;
            margin: -1px;
            overflow: hidden;
            clip: rect(0,0,0,0);
            white-space: nowrap;
            border: 0;
        }
        
        /* Focus indicator */
        *:focus {
            outline: 3px solid var(--focus-color);
            outline-offset: 2px;
        }
        
        /* Base body styles */
        body {
            padding: 20px;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 
                         "Helvetica Neue", Arial, sans-serif;
            background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
            min-height: 100vh;
        }
        
        .quiz-container {
            max-width: 800px;
            margin: 0 auto;
        }
        
        .question-card {
            background: white;
            border-radius: 16px;
            padding: 40px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
            margin-bottom: 20px;
        }
        
        .quiz-progress {
            background: rgba(255, 255, 255, 0.2);
            border-radius: 10px;
            height: 8px;
            margin-bottom: 30px;
            overflow: hidden;
            border: 2px solid rgba(255, 255, 255, 0.5);
        }
        
        .quiz-progress-bar {
            background: linear-gradient(90deg, var(--primary-color) 0%, var(--secondary-color) 100%);
            height: 100%;
            transition: width 0.3s ease;
            box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8);
        }
        
        .question-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 24px;
            padding-bottom: 16px;
            border-bottom: 2px solid #e9ecef;
        }
        
        .question-number {
            font-size: 14px;
            font-weight: 600;
            color: var(--primary-color);
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }
        
        .question-number:focus {
            background: rgba(102, 126, 234, 0.1);
            padding: 4px 8px;
            border-radius: 4px;
        }
        
        .question-type-badge {
            font-size: 12px;
            padding: 4px 12px;
            border-radius: 20px;
            background: #e9ecef;
            color: var(--text-muted);
            font-weight: 500;
        }
        
        .question-type-badge:focus {
            outline: 3px solid var(--focus-color);
            outline-offset: 2px;
        }
        
        .question-text {
            font-size: 20px;
            font-weight: 600;
            color: var(--text-color);
            margin-bottom: 28px;
            line-height: 1.5;
        }
        
        .question-text:focus {
            background: rgba(102, 126, 234, 0.1);
            padding: 8px;
            border-radius: 8px;
        }
        
        .answer-option {
            width: 100%;
            text-align: left;
            padding: 16px 20px;
            margin-bottom: 12px;
            border: 2px solid var(--border-color);
            border-radius: 10px;
            background: white;
            color: var(--text-color);
            transition: all 0.2s ease;
            font-size: 16px;
            cursor: pointer;
            display: block;
            opacity: 0;
            transform: translateX(-20px);
        }
        
        .answer-option.visible {
            opacity: 1;
            transform: translateX(0);
        }
        
        .answer-option:hover {
            border-color: var(--primary-color);
            background: #f8f9ff;
            transform: translateX(4px);
        }
        
        .answer-option:focus {
            border-color: var(--primary-color);
            background: #f8f9ff;
        }
        
        .answer-option.correct {
            border-color: var(--correct-color);
            background: var(--correct-bg);
        }
        
        .answer-option.incorrect {
            border-color: var(--incorrect-color);
            background: var(--incorrect-bg);
        }
        
        .fill-blank-input {
            width: 100%;
            padding: 16px;
            font-size: 16px;
            border: 2px solid var(--border-color);
            border-radius: 10px;
            transition: border-color 0.2s ease;
            opacity: 0;
            transform: translateY(20px);
        }
        
        .fill-blank-input.visible {
            opacity: 1;
            transform: translateY(0);
        }
        
        .fill-blank-input:focus {
            border-color: var(--primary-color);
        }
        
        .submit-answer-btn {
            background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
            color: white;
            border: none;
            padding: 14px 32px;
            border-radius: 10px;
            font-size: 16px;
            font-weight: 600;
            margin-top: 20px;
            transition: transform 0.2s ease;
            opacity: 0;
            transform: translateY(20px);
        }
        
        .submit-answer-btn.visible {
            opacity: 1;
            transform: translateY(0);
        }
        
        .submit-answer-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
        }
        
        .feedback {
            margin-top: 20px;
            padding: 16px;
            border-radius: 10px;
            font-weight: 500;
            display: none;
        }
        
        .feedback:focus {
            outline: 3px solid var(--focus-color);
            outline-offset: 2px;
        }
        
        .feedback.correct {
            background: var(--correct-bg);
            color: #155724;
            border: 2px solid var(--correct-color);
        }
        
        .feedback.incorrect {
            background: var(--incorrect-bg);
            color: #721c24;
            border: 2px solid var(--incorrect-color);
        }
        
        .results-card {
            text-align: center;
            opacity: 0;
        }
        
        .results-card.visible {
            opacity: 1;
        }
        
        .results-score {
            font-size: 72px;
            font-weight: 700;
            background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            margin: 20px 0;
            opacity: 0;
            transform: scale(0);
        }
        
        .results-score.visible {
            opacity: 1;
            transform: scale(1);
        }
        
        .results-message {
            font-size: 24px;
            color: var(--text-muted);
            margin-bottom: 30px;
            opacity: 0;
            transform: translateY(20px);
        }
        
        .results-message.visible {
            opacity: 1;
            transform: translateY(0);
        }
        
        .lead {
            opacity: 0;
            transform: translateY(20px);
        }
        
        .lead.visible {
            opacity: 1;
            transform: translateY(0);
        }
        
        .restart-btn {
            background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
            color: white;
            border: none;
            padding: 16px 40px;
            border-radius: 10px;
            font-size: 18px;
            font-weight: 600;
            transition: transform 0.2s ease;
            opacity: 0;
            transform: translateY(20px);
        }
        
        .restart-btn.visible {
            opacity: 1;
            transform: translateY(0);
        }
        
        .restart-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
        }
    </style>
</head>
<body>
    <div id="sr-announcements" role="status" aria-live="polite" aria-atomic="true" class="sr-only"></div>

    <div class="quiz-container">
        <div class="quiz-progress" role="progressbar" aria-label="Quiz progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
            <div class="quiz-progress-bar" id="progressBar" style="width: 0%"></div>
        </div>
        
        <div id="questionContainer" class="question-card"></div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
    
    <script>
        const quizData = [
            {
                id: 1,
                type: 'multiple-choice',
                question: 'What is the most common type of cyber attack used to trick people into revealing sensitive information?',
                options: ['Malware', 'Phishing', 'DDoS Attack', 'SQL Injection'],
                correctAnswer: 1,
                feedback: {
                    correct: 'Correct! Phishing uses deceptive emails or messages to trick users into revealing sensitive information.',
                    incorrect: 'Not quite. Phishing is the most common type of social engineering attack that tricks users into revealing sensitive information.'
                }
            },
            {
                id: 2,
                type: 'true-false',
                question: 'Using the same password across multiple accounts is acceptable if the password is strong.',
                options: ['True', 'False'],
                correctAnswer: 1,
                feedback: {
                    correct: 'Correct! You should never reuse passwords. If one account is compromised, all accounts using that password are at risk.',
                    incorrect: 'Incorrect. Even with a strong password, reusing it across accounts creates a single point of failure. Always use unique passwords.'
                }
            },
            {
                id: 3,
                type: 'multiple-choice',
                question: 'What does MFA stand for in cybersecurity?',
                options: ['Multiple File Authentication', 'Multi-Factor Authentication', 'Mandatory Firewall Access', 'Managed File Authorization'],
                correctAnswer: 1,
                feedback: {
                    correct: 'Correct! Multi-Factor Authentication adds an extra layer of security by requiring two or more verification methods.',
                    incorrect: 'Not quite. MFA stands for Multi-Factor Authentication, which requires multiple verification methods to access an account.'
                }
            },
            {
                id: 4,
                type: 'fill-blank',
                question: 'A strong password should be at least ___ characters long. (Enter a number)',
                correctAnswer: '12',
                acceptableAnswers: ['12', 'twelve', '12 characters', '12+', '12 or more'],
                feedback: {
                    correct: 'Correct! Security experts recommend passwords be at least 12 characters long for adequate protection.',
                    incorrect: 'Not quite. Security experts recommend passwords be at least 12 characters long to ensure adequate protection.'
                }
            },
            {
                id: 5,
                type: 'true-false',
                question: 'It is safe to connect to public Wi-Fi networks without using a VPN.',
                options: ['True', 'False'],
                correctAnswer: 1,
                feedback: {
                    correct: 'Correct! Public Wi-Fi networks are insecure. Always use a VPN to encrypt your connection on public networks.',
                    incorrect: 'Incorrect. Public Wi-Fi networks are not secure. You should always use a VPN to encrypt your data when using public Wi-Fi.'
                }
            },
            {
                id: 6,
                type: 'multiple-choice',
                question: 'What should you do if you receive a suspicious email from what appears to be your bank asking you to verify your account?',
                options: [
                    'Click the link and enter your information',
                    'Reply to the email with your account details',
                    'Delete it and contact your bank directly using a known phone number',
                    'Forward it to your colleagues'
                ],
                correctAnswer: 2,
                feedback: {
                    correct: 'Correct! Always verify suspicious requests by contacting the organization directly through official channels, never through links in emails.',
                    incorrect: 'Not quite. You should delete suspicious emails and contact your bank directly using a phone number from their official website or your bank card.'
                }
            },
            {
                id: 7,
                type: 'fill-blank',
                question: 'Software that encrypts your files and demands payment to decrypt them is called ___. (One word)',
                correctAnswer: 'ransomware',
                acceptableAnswers: ['ransomware', 'Ransomware', 'ransom ware', 'ransom-ware'],
                feedback: {
                    correct: 'Correct! Ransomware is malicious software that encrypts files and demands payment for their release.',
                    incorrect: 'Not quite. This type of malware is called ransomware, which encrypts files and demands a ransom for decryption.'
                }
            },
            {
                id: 8,
                type: 'multiple-choice',
                question: 'Which of the following is the BEST example of a strong password?',
                options: [
                    'Password123!',
                    'JohnSmith2024',
                    'Tr0pic@l$unset#2024!',
                    'qwerty12345'
                ],
                correctAnswer: 2,
                feedback: {
                    correct: 'Correct! This password is long, contains uppercase and lowercase letters, numbers, and special characters, and is not easily guessable.',
                    incorrect: 'Not quite. The best password is long, contains a mix of uppercase, lowercase, numbers, and special characters, and avoids common words or patterns.'
                }
            }
        ];
        
        let currentQuestionIndex = 0;
        let score = 0;
        let userAnswers = [];
        let isAnswered = false;
        
        const STORAGE_KEY = 'riseBlock_multiQuiz_data';
        
        const A11y = {
            init() {
                if (!document.getElementById('sr-announcements')) {
                    const sr = document.createElement('div');
                    sr.id = 'sr-announcements';
                    sr.setAttribute('role', 'status');
                    sr.setAttribute('aria-live', 'polite');
                    sr.setAttribute('aria-atomic', 'true');
                    sr.className = 'sr-only';
                    document.body.appendChild(sr);
                }
                this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
            },
            
            announce(message, priority = 'polite') {
                const sr = document.getElementById('sr-announcements');
                if (!sr) return;
                sr.setAttribute('aria-live', priority);
                sr.textContent = '';
                setTimeout(() => sr.textContent = message, 100);
            }
        };
        
        document.addEventListener('DOMContentLoaded', () => A11y.init());
        
        function saveData(data) {
            try {
                localStorage.setItem(STORAGE_KEY, JSON.stringify({
                    ...data,
                    savedAt: new Date().toISOString()
                }));
                return true;
            } catch (e) {
                console.error('Failed to save data:', e);
                return false;
            }
        }
        
        function loadData() {
            const saved = localStorage.getItem(STORAGE_KEY);
            if (saved) {
                try {
                    return JSON.parse(saved);
                } catch (e) {
                    console.error('Failed to parse saved data:', e);
                    return null;
                }
            }
            return null;
        }
        
        function clearData() {
            localStorage.removeItem(STORAGE_KEY);
        }
        
        function initQuiz() {
            const saved = loadData();
            if (saved && saved.currentQuestionIndex < quizData.length) {
                showResumeDialog(saved);
            } else {
                displayQuestion();
                updateProgressBar();
            }
        }
        
        function showResumeDialog(saved) {
            const container = document.getElementById('questionContainer');
            const questionsRemaining = quizData.length - saved.currentQuestionIndex;
            
            A11y.announce(`Welcome back. You have ${questionsRemaining} questions remaining.`);
            
            container.innerHTML = `
                <div class="results-card" style="opacity: 1;">
                    <h2>Welcome Back!</h2>
                    <p class="lead">You have a quiz in progress.</p>
                    <p>You answered ${saved.currentQuestionIndex} of ${quizData.length} questions.</p>
                    <div class="mt-4">
                        <button class="restart-btn me-3" onclick="resumeQuiz()" style="opacity: 1;">Resume Quiz</button>
                        <button class="btn btn-outline-secondary" onclick="startFresh()" style="opacity: 1;">Start Over</button>
                    </div>
                </div>
            `;
        }
        
        function resumeQuiz() {
            const saved = loadData();
            if (saved) {
                currentQuestionIndex = saved.currentQuestionIndex;
                score = saved.score;
                userAnswers = saved.userAnswers;
                displayQuestion();
                updateProgressBar();
            }
        }
        
        function startFresh() {
            clearData();
            currentQuestionIndex = 0;
            score = 0;
            userAnswers = [];
            displayQuestion();
            updateProgressBar();
        }
        
        function displayQuestion() {
            const question = quizData[currentQuestionIndex];
            const container = document.getElementById('questionContainer');
            isAnswered = false;
            
            A11y.announce(`Question ${currentQuestionIndex + 1} of ${quizData.length}. ${question.type}. ${question.question}`);
            
            let html = `
                <div class="question-header">
                    <span class="question-number" tabindex="0">Question ${currentQuestionIndex + 1} of ${quizData.length}</span>
                    <span class="question-type-badge" tabindex="0">${question.type}</span>
                </div>
                <div class="question-text" role="heading" aria-level="2" tabindex="0">${question.question}</div>
            `;
            
            if (question.type === 'multiple-choice' || question.type === 'true-false') {
                html += '<div class="options-container">';
                question.options.forEach((option, index) => {
                    html += `<button class="answer-option" data-index="${index}">${option}</button>`;
                });
                html += '</div>';
            } else if (question.type === 'fill-blank') {
                html += `
                    <input type="text" class="fill-blank-input" id="fillInput" placeholder="Type your answer">
                    <button class="submit-answer-btn" onclick="submitFillBlank()">Submit Answer</button>
                `;
            }
            
            html += '<div class="feedback" id="feedback" tabindex="-1"></div>';
            container.innerHTML = html;
            
            if (question.type === 'multiple-choice' || question.type === 'true-false') {
                document.querySelectorAll('.answer-option').forEach(btn => {
                    btn.addEventListener('click', () => selectAnswer(parseInt(btn.dataset.index)));
                });
            }
            
            if (question.type === 'fill-blank') {
                const input = document.getElementById('fillInput');
                if (input) {
                    input.addEventListener('keypress', (e) => {
                        if (e.key === 'Enter') {
                            e.preventDefault();
                            submitFillBlank();
                        }
                    });
                }
            }
            
            setTimeout(() => {
                const first = container.querySelector('.question-number');
                if (first) first.focus();
                animateIn();
            }, 100);
        }
        
        function animateIn() {
            const options = document.querySelectorAll('.answer-option');
            const input = document.querySelector('.fill-blank-input');
            const btn = document.querySelector('.submit-answer-btn');
            
            if (!A11y.prefersReducedMotion) {
                if (options.length > 0) {
                    gsap.to(options, { opacity: 1, x: 0, duration: 0.4, stagger: 0.1, delay: 0.2 });
                }
                if (input) {
                    gsap.to(input, { opacity: 1, y: 0, duration: 0.4, delay: 0.2 });
                }
                if (btn) {
                    gsap.to(btn, { opacity: 1, y: 0, duration: 0.4, delay: 0.3 });
                }
            } else {
                options.forEach(o => o.classList.add('visible'));
                if (input) input.classList.add('visible');
                if (btn) btn.classList.add('visible');
            }
        }
        
        function selectAnswer(index) {
            if (isAnswered) return;
            isAnswered = true;
            
            const question = quizData[currentQuestionIndex];
            const isCorrect = index === question.correctAnswer;
            
            if (isCorrect) score++;
            userAnswers.push({ questionId: question.id, answer: index, correct: isCorrect });
            
            A11y.announce(isCorrect ? 'Correct!' : 'Incorrect.', 'assertive');
            
            const buttons = document.querySelectorAll('.answer-option');
            buttons.forEach((btn, i) => {
                btn.disabled = true;
                if (i === question.correctAnswer) btn.classList.add('correct');
                else if (i === index && !isCorrect) btn.classList.add('incorrect');
            });
            
            showFeedback(isCorrect, question.feedback);
            
            setTimeout(() => nextQuestion(), 2000);
        }
        
        function submitFillBlank() {
            if (isAnswered) return;
            
            const input = document.getElementById('fillInput');
            const answer = input.value.trim();
            
            if (!answer) {
                A11y.announce('Please enter an answer', 'assertive');
                return;
            }
            
            isAnswered = true;
            const question = quizData[currentQuestionIndex];
            const isCorrect = question.acceptableAnswers.some(a => a.toLowerCase() === answer.toLowerCase());
            
            if (isCorrect) score++;
            userAnswers.push({ questionId: question.id, answer, correct: isCorrect });
            
            A11y.announce(isCorrect ? 'Correct!' : 'Incorrect.', 'assertive');
            
            input.disabled = true;
            input.style.borderColor = isCorrect ? 'var(--correct-color)' : 'var(--incorrect-color)';
            input.style.background = isCorrect ? 'var(--correct-bg)' : 'var(--incorrect-bg)';
            
            showFeedback(isCorrect, question.feedback);
            
            setTimeout(() => nextQuestion(), 2000);
        }
        
        function showFeedback(isCorrect, feedback) {
            const div = document.getElementById('feedback');
            div.className = `feedback ${isCorrect ? 'correct' : 'incorrect'}`;
            div.textContent = isCorrect ? feedback.correct : feedback.incorrect;
            div.style.display = 'block';
            div.setAttribute('tabindex', '0');
            setTimeout(() => div.focus(), 400);
        }
        
        function nextQuestion() {
            currentQuestionIndex++;
            
            if (currentQuestionIndex < quizData.length) {
                saveData({ currentQuestionIndex, score, userAnswers });
                displayQuestion();
                updateProgressBar();
            } else {
                clearData();
                showResults();
            }
        }
        
        function updateProgressBar() {
            const percent = ((currentQuestionIndex + 1) / quizData.length) * 100;
            const bar = document.getElementById('progressBar');
            bar.style.width = `${percent}%`;
            bar.parentElement.setAttribute('aria-valuenow', Math.round(percent));
        }
        
        function showResults() {
            const percent = Math.round((score / quizData.length) * 100);
            const container = document.getElementById('questionContainer');
            
            gsap.set(container, { opacity: 1, y: 0, scale: 1 });
            
            let message = percent >= 80 ? 'Excellent work!' : percent >= 60 ? 'Good job!' : 'Keep practicing!';
            
            A11y.announce(`Quiz complete. You scored ${score} out of ${quizData.length}. ${percent} percent. ${message}`, 'assertive');
            
            container.innerHTML = `
                <div class="results-card">
                    <h2>Quiz Complete!</h2>
                    <div class="results-score">${percent}%</div>
                    <p class="results-message">${message}</p>
                    <p class="lead">You scored ${score} out of ${quizData.length} questions correctly.</p>
                    <button class="restart-btn" onclick="restartQuiz()">Try Again</button>
                </div>
            `;
            
            setTimeout(() => {
                const btn = document.querySelector('.restart-btn');
                if (btn) btn.focus();
                animateResults();
            }, 100);
        }
        
        function animateResults() {
            const card = document.querySelector('.results-card');
            const score = document.querySelector('.results-score');
            const msg = document.querySelector('.results-message');
            const lead = document.querySelector('.lead');
            const btn = document.querySelector('.restart-btn');
            
            if (!A11y.prefersReducedMotion) {
                gsap.to(card, { opacity: 1, duration: 0.6 });
                if (score) gsap.to(score, { opacity: 1, scale: 1, duration: 0.8, delay: 0.3 });
                const others = [msg, lead, btn].filter(e => e);
                if (others.length) gsap.to(others, { opacity: 1, y: 0, duration: 0.5, stagger: 0.1, delay: 0.6 });
            } else {
                if (card) card.classList.add('visible');
                if (score) score.classList.add('visible');
                if (msg) msg.classList.add('visible');
                if (lead) lead.classList.add('visible');
                if (btn) btn.classList.add('visible');
            }
        }
        
        function restartQuiz() {
            clearData();
            currentQuestionIndex = 0;
            score = 0;
            userAnswers = [];
            displayQuestion();
            updateProgressBar();
        }
        
        document.addEventListener('DOMContentLoaded', initQuiz);
    </script>
</body>
</html>

Customizing Questions for Your Content

Modifying the quiz for your own subject matter requires editing the quizData array. Each question object follows the same structure, so you can add, remove, or modify questions without touching any other code.

For multiple choice questions, list your options in the options array and set correctAnswer to the index of the right answer. Remember that indexing starts at zero, so the first option is 0, the second is 1, and so on.

True/false questions work identically to multiple choice—they just have only two options. The code handles them the same way because the display logic checks for both types together.

Fill-in-the-blank questions need the acceptableAnswers array because learners might phrase their response differently. Include common variations like capitalized versions, alternative spellings, or answers with and without units.

Troubleshooting Common Issues

When your quiz doesn't work as expected, start by checking the browser's developer console. Press F12 to open developer tools and look for red error messages in the Console tab.

The most common issue is a syntax error in the quizData array. Missing commas between question objects or mismatched quotes will break the entire quiz. Use a JSON validator to check your data structure if questions aren't appearing.

If animations aren't playing, verify that the GSAP library is loading. In the Network tab of developer tools, look for the gsap.min.js request. If it shows a red status or no request at all, your network might be blocking the CDN.

For localStorage issues in Rise 360 specifically, some organizations configure their LMS to run courses in sandboxed iframes that block storage access. Test the quiz in SCORM Cloud or your LMS's preview mode to confirm persistence works in your actual deployment environment.

Taking Your Rise 360 Quiz Block Further

This foundation opens doors for many enhancements. You could add a timer that limits how long learners have for each question or the entire quiz. A question pool feature could randomly select questions from a larger bank, ensuring each attempt feels fresh.

Consider integrating with Rise 360's native tracking by using xAPI statements. When a learner completes the quiz, you could send their score to an LRS for detailed analytics beyond what Rise 360 provides natively.

The accessibility features shown here follow WCAG 2.1 guidelines, but you might expand them with additional landmark regions or more detailed progress announcements depending on your audience's needs.

Building custom Rise 360 quiz blocks takes your courses from standard to standout. The combination of mixed question types, smooth animations, and progress persistence creates an assessment experience that feels modern and professional—exactly what today's learners expect.