How to Create a JavaScript Accordion in Articulate Storyline
By Nathan Stoker · February 8, 2026 · 12 min read

Looking to add an engaging, interactive accordion to your Articulate Storyline course? This JavaScript accordion interaction creates a sleek card-based navigation where learners click through overlapping panels that slide smoothly across the screen. It's perfect for presenting sequential content, comparing options, or creating memorable menu systems.
In this tutorial, you'll learn how to implement this JavaScript accordion from scratch—no advanced coding experience required. By the end, you'll have a fully functional accordion system that tracks learner progress and marks completion when all panels have been viewed.
Check out the demo and download the file to follow along.
What You'll Build
This interaction creates a stack of overlapping cards that animate when clicked. Panels slide left or right to reveal content, with visual feedback showing which sections have been visited. The system automatically tracks completion, making it ideal for compliance training or structured learning paths.
The interaction includes:
Smooth GSAP-powered animations
Hover effects that brighten panels
Visual "visited" state with reduced saturation
Automatic completion tracking via Storyline variables
Responsive design that adapts to window resizing
Prerequisites
Before starting, make sure you have:
Articulate Storyline 360 (or Storyline 3)
Basic familiarity with adding JavaScript triggers
The panel graphics you want to use (PNG or SVG format works best)
GSAP library loaded in your project
Loading GSAP in Your Project
This interaction requires the GSAP animation library. You'll need to load it before the accordion system initializes.
Add this JavaScript trigger to your slide's timeline start (set to execute at 0.25 seconds):
var script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js';
document.head.appendChild(script);This gives GSAP time to load before your accordion system needs it.
Step 1: Design Your Panels in Storyline
Start by creating your panel graphics directly on the slide. Each panel should be a separate object—you can use shapes, images, or grouped elements.
Important design tips:
Make all panels the same size for consistent behavior
Position panels so they overlap in a stack formation
Place the first panel (Tab 1) at the bottom of the stacking order
Each subsequent panel should appear above the previous one
To set the stacking order, right-click each panel and use "Send to Back" or "Bring to Front" until Tab 1 is at the bottom and Tab 4 (or your final panel) is on top.
Step 2: Find Your Data Model IDs
Every object in Storyline has a unique identifier called a data-model-id. You'll need these IDs to connect your JavaScript to your panel objects.
Here's how to find them:
Preview your project in a web browser
Right-click on your first panel and select "Inspect" (or press F12)
Look for the
data-model-idattribute in the HTML elementCopy the alphanumeric string (it looks like "6EwRQaNDtUK")
Repeat for each panel and your container object
Write down each ID—you'll enter them in the configuration section of the code.
Step 3: Create Your Storyline Variable
The interaction tracks when learners have viewed all panels. Create a True/False variable to capture this:
Open the Variables panel in Storyline
Click "Create a new variable"
Name it
accordionCompleteSet the type to True/False
Set the default value to False
You can use this variable to control navigation, trigger completion, or unlock additional content.
Step 4: Add the JavaScript Accordion Code
Now add the main interaction code. Create a new trigger with these settings:
Action: Execute JavaScript
When: Timeline starts
Time: 0.5 seconds (this ensures GSAP has loaded)
Copy and paste the complete code below into the JavaScript editor:
function initializeCardSystem() {
// ============ CONFIGURATION ============
var player = GetPlayer();
var containerId = "6T5d1wMy0MS";
var completionVariable = "accordionComplete";
var cardGapPercent = 6; // Visible "tab" gap (as % of container width)
var startPosition = "left"; // "left" or "right"
var hoverBrightness = 1.05;
var visitedSaturation = 0.9;
var cards = [
{ id: "5h0Cwn23b7w" },
{ id: "63Agf8h13An" },
{ id: "64rCwfGnuVA" },
{ id: "6jzcEoMtReY" }
];
// =======================================
var cardStates = {};
var cardVisited = {};
var cardWidth = 0; // Assume all cards same width
var cardBaseLeft = 0; // Where cards sit in DOM (x=0)
var isInitialized = false;
function getContainer() {
return document.querySelector('[data-model-id="' + containerId + '"]');
}
function getContainerBounds() {
var container = getContainer();
if (!container) return { left: 0, width: 800 };
var rect = container.getBoundingClientRect();
return { left: rect.left, width: rect.width };
}
function getGapInPixels() {
var bounds = getContainerBounds();
return (cardGapPercent / 100) * bounds.width;
}
function getCardX(cardIndex, position) {
var bounds = getContainerBounds();
var gap = getGapInPixels();
var targetViewportX;
if (position === "left") {
if (startPosition === "right") {
// Expanded: left edges stagger from container left
targetViewportX = bounds.left + (cardIndex * gap);
} else {
// Collapsed: cards mostly off-screen left
var gapMultiplier = cards.length - cardIndex;
targetViewportX = bounds.left - cardWidth + (gap * gapMultiplier);
}
} else {
if (startPosition === "right") {
// Collapsed: cards mostly off-screen right, tabs visible on left
var gapMultiplier = cards.length - cardIndex;
targetViewportX = bounds.left + bounds.width - (gap * gapMultiplier);
} else {
// Expanded: stagger from right
targetViewportX = bounds.left + bounds.width - cardWidth - (cardIndex * gap);
}
}
// Convert viewport position to transform value
return targetViewportX - cardBaseLeft;
}
function getCardElement(cardId) {
return document.querySelector('[data-model-id="' + cardId + '"]') ||
document.querySelector('[data-acc-id="' + cardId + '"]');
}
function getLowestZIndexChild(parentElement) {
var children = parentElement.querySelectorAll('*');
var lowestChild = null;
var lowestZ = Infinity;
children.forEach(function(child) {
var z = parseInt(window.getComputedStyle(child).zIndex, 10);
if (isNaN(z)) z = 0;
if (z < lowestZ) {
lowestZ = z;
lowestChild = child;
}
});
return lowestChild;
}
function waitForElements(callback) {
var attempts = 0;
var check = function() {
attempts++;
var ready = getContainer() && cards.every(function(c) { return getCardElement(c.id); });
if (ready) callback();
else if (attempts < 30) setTimeout(check, 150);
};
check();
}
function applyHoverState(element) {
var target = getLowestZIndexChild(element) || element;
gsap.to(target, {
filter: "brightness(" + hoverBrightness + ")",
duration: 0.2,
ease: "power2.out"
});
}
function removeHoverState(element, cardId) {
var target = getLowestZIndexChild(element) || element;
var filterValue = cardVisited[cardId] ? "saturate(" + visitedSaturation + ")" : "brightness(1)";
gsap.to(target, {
filter: filterValue,
duration: 0.2,
ease: "power2.out"
});
}
function applyVisitedState(element) {
var target = getLowestZIndexChild(element) || element;
gsap.to(target, {
filter: "saturate(" + visitedSaturation + ")",
duration: 0.3
});
}
function checkAllVisited() {
var allVisited = cards.every(function(card) {
return cardVisited[card.id] === true;
});
if (allVisited) {
player.SetVar(completionVariable, true);
console.log("All cards visited - " + completionVariable + " set to true");
}
}
function measureCards() {
// Get measurements from first card (assume all same size)
var firstElement = getCardElement(cards[0].id);
if (firstElement) {
// Reset transform to get true DOM position
gsap.set(firstElement, { x: 0 });
var rect = firstElement.getBoundingClientRect();
cardWidth = rect.width;
cardBaseLeft = rect.left;
}
}
function initializeCards() {
if (isInitialized) return;
measureCards();
var startFromLeft = startPosition === "left";
for (var i = 0; i < cards.length; i++) {
var card = cards[i];
var element = getCardElement(card.id);
if (!element) continue;
cardVisited[card.id] = false;
var targetX = getCardX(i, startPosition);
var offscreenX = startFromLeft ? (targetX - 500) : (targetX + 500);
if (typeof gsap !== 'undefined') {
gsap.set(element, { x: offscreenX });
gsap.to(element, {
x: targetX,
duration: 0.6,
delay: i * 0.15,
ease: "power3.out"
});
}
cardStates[card.id] = startPosition;
}
isInitialized = true;
}
function moveCards(clickedCardId) {
var clickedIndex = cards.findIndex(function(c) { return c.id === clickedCardId; });
if (clickedIndex === -1) return;
if (!cardVisited[clickedCardId]) {
cardVisited[clickedCardId] = true;
var clickedElement = getCardElement(clickedCardId);
if (clickedElement) {
applyVisitedState(clickedElement);
}
checkAllVisited();
}
var clickedCardState = cardStates[cards[clickedIndex].id];
if (startPosition === "right") {
if (clickedCardState === 'right') {
for (var i = 0; i <= clickedIndex; i++) {
var card = cards[i];
var element = getCardElement(card.id);
if (!element) continue;
gsap.to(element, { x: getCardX(i, 'left'), duration: 0.5, ease: "power2.inOut" });
cardStates[card.id] = 'left';
}
} else {
for (var i = clickedIndex; i < cards.length; i++) {
var card = cards[i];
var element = getCardElement(card.id);
if (!element) continue;
gsap.to(element, { x: getCardX(i, 'right'), duration: 0.5, ease: "power2.inOut" });
cardStates[card.id] = 'right';
}
}
} else {
if (clickedCardState === 'left') {
for (var i = 0; i <= clickedIndex; i++) {
var card = cards[i];
var element = getCardElement(card.id);
if (!element) continue;
gsap.to(element, { x: getCardX(i, 'right'), duration: 0.5, ease: "power2.inOut" });
cardStates[card.id] = 'right';
}
} else {
for (var i = clickedIndex; i < cards.length; i++) {
var card = cards[i];
var element = getCardElement(card.id);
if (!element) continue;
gsap.to(element, { x: getCardX(i, 'left'), duration: 0.5, ease: "power2.inOut" });
cardStates[card.id] = 'left';
}
}
}
}
function addClickListeners() {
cards.forEach(function(card) {
var element = getCardElement(card.id);
if (element) {
element.style.cursor = 'pointer';
var target = getLowestZIndexChild(element) || element;
gsap.set(target, { filter: "brightness(1)" });
element.addEventListener('click', function() {
moveCards(card.id);
});
element.addEventListener('mouseenter', function() {
applyHoverState(element);
});
element.addEventListener('mouseleave', function() {
removeHoverState(element, card.id);
});
}
});
}
var resizeTimeout;
window.addEventListener('resize', function() {
if (!isInitialized) return;
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function() {
// Re-measure from first card
var firstElement = getCardElement(cards[0].id);
if (firstElement) {
var currentX = parseFloat(gsap.getProperty(firstElement, "x")) || 0;
gsap.set(firstElement, { x: 0 });
var rect = firstElement.getBoundingClientRect();
cardWidth = rect.width;
cardBaseLeft = rect.left;
gsap.set(firstElement, { x: currentX });
}
// Animate all cards to correct positions
cards.forEach(function(card, index) {
var element = getCardElement(card.id);
if (!element) return;
var targetX = getCardX(index, cardStates[card.id]);
gsap.to(element, { x: targetX, duration: 0.3 });
});
}, 100);
});
waitForElements(function() {
initializeCards();
addClickListeners();
});
window.moveCards = moveCards;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeCardSystem);
} else {
initializeCardSystem();
}Step 5: Configure the Code for Your Project
The configuration section at the top of the code controls how your accordion behaves. Update these values to match your project:
Container ID
var containerId = "6EwRQaNDtUK";Replace with your container's data-model-id. The container defines the boundaries where panels will animate.
Panel IDs
javascript
var cards = [
{ id: "684vi27AJJH" },
{ id: "6WYVxtWtFdc" },
{ id: "6HcbXnGrH2T" },
{ id: "5YsnWSnjEPE" }
];Replace each ID with your actual panel IDs. The order matters—list them from bottom to top of your visual stack. Add or remove entries to match your number of panels.
Start Position
javascript
var startPosition = "right";This determines where panels begin. Set to "right" to have panels stacked on the right side (sliding left when clicked) or "left" for the opposite behavior.
Visual Settings
var cardGapPercent = 6;
var hoverBrightness = 1.05;
var visitedSaturation = 0.9;cardGapPercent: How much of each panel's tab remains visible when stacked (as a percentage of container width)
hoverBrightness: How much to brighten panels on mouse hover (1.0 = no change, 1.1 = 10% brighter)
visitedSaturation: Saturation level for visited panels (0.9 = slightly desaturated to indicate completion)
Understanding How the JavaScript Accordion Works
The code uses several key techniques that make it reliable and flexible.
Element Selection with data-model-id
The getCardElement function locates your Storyline objects in the browser's DOM. Storyline assigns each object a unique data-model-id attribute, which remains consistent across previews and publishes. Learn more about DOM selection methods in the MDN querySelector documentation.
Responsive Positioning
Rather than using fixed pixel positions, the code calculates panel positions based on the container's current size. The getCardX function determines where each panel should be based on the container bounds, panel index, and current state. This means your interaction works correctly even when the browser window resizes.
Staggered Animation Entry
When the slide loads, panels animate in with a staggered timing effect:
gsap.to(element, {
x: targetX,
duration: 0.6,
delay: i * 0.15,
ease: "power3.out"
});Each panel enters 0.15 seconds after the previous one, creating a polished cascade effect.
State Management
The code tracks two types of state for each panel. The cardStates object records whether each panel is currently positioned left or right. The cardVisited object tracks which panels the learner has clicked. When all panels show as visited, the completion variable triggers.
Troubleshooting Common Issues
Panels don't appear or animate: Verify that GSAP loaded successfully by checking the browser console for errors. Make sure your GSAP loading trigger fires before the main code trigger.
Panels animate to wrong positions: Double-check your container ID. The container defines the boundaries for positioning calculations.
Clicking doesn't work: Confirm your panel IDs are correct. Preview the project and use browser developer tools to verify the data-model-id values match your configuration.
Completion variable doesn't trigger: Ensure your Storyline variable name exactly matches the completionVariable value in the code (including capitalization).
Extending the Interaction
Once you have the basic interaction working, consider these enhancements:
Add content layers that show/hide based on which panel is expanded
Connect the completion variable to a results slide or navigation controls
Adjust timing values to match your course's pacing
Create custom entrance animations by modifying the
offscreenXcalculation
Conclusion
You've now built a professional JavaScript accordion in Articulate Storyline. This technique demonstrates how combining Storyline's native authoring with JavaScript and GSAP opens up interaction possibilities that aren't available through the standard toolset alone.
The principles you learned here—finding element IDs, managing state, and using GSAP for animation—apply to many other custom interactions. Start with this foundation and experiment with your own variations.
For more Storyline JavaScript techniques, explore the Articulate Community Forums where developers share solutions and troubleshooting tips.