Articulate JavaScript Tools
JavaScript scripts for Articulate Storyline 360 and Rise 360. View on GitHub
Adds a progress bar with percentage text and slide counter to the Storyline 360 player top bar.
What It Does
Adds a progress bar with percentage text and a slide counter to the Storyline 360 player top bar. Three elements appear side by side: a "Course Progress: XX%" label, a visual bar that fills and changes color at 100%, and a "Slide: X/Y" counter. On-slide progress indicators are no longer needed, which frees up the full slide area for content.
How It Works
On each slide load the script checks whether the progress bar DOM elements already exist in the player links-right container. If they do not exist (first slide), it builds the full widget. If they do exist (subsequent slides), it updates the values in place. A setInterval runs every 500 ms to stay in sync if variables change mid-slide. Two short setTimeout calls at 150 ms and 400 ms catch cases where Storyline updates variables slightly after the timeline starts.
Setup
- Open your Storyline project, go to View, then Slide Master, and select the top-level master slide (the parent, not a child layout).
- Create a variable named Progress (capital P, case-sensitive).
- Add a trigger: Adjust Variable, set Progress equal to Menu.Progress or Project.Progress.
- Add a second trigger: Execute JavaScript, paste the contents of the script.
- If your player has no right-side buttons such as Resources or Glossary, add a placeholder character to a player tab to keep the links-right container visible.
Configurable Variables
| Variable | Default | Description |
|---|---|---|
| bgColor | #F6F9FB | Track background color |
| barColor | #FCCE4B | Progress fill color |
| compColor | #19BB32 | Fill color at 100% |
| borderRad | 100px | Border radius for rounded ends |
| barWidth | 220px | Width of the progress bar |
| barHeight | 15px | Height of the progress bar |
| wrapperX / wrapperY | 20px / 0px | Offset of the entire widget |
| barX | 200px | Left offset of the bar |
| slideTextX | 440px | Left offset of the slide counter |
Slide Counter
The script reads MenuSection.SlideNumber and MenuSection.TotalSlides (Storyline built-ins) first. If those are not readable via JavaScript, it falls back to user-created variables named CurrentSlide and TotalSlides. If neither source is available, the counter shows Slide: --/--.
/**
* JavaScript Trigger Progress Bar Slide Count in Storyline
*
* Purpose: Adds a progress bar with percentage text and a
* slide counter to the Storyline 360 player top bar.
*
* Author: Joseph Black | Version: 1.0.0
* Software: Articulate Storyline 360 x64 v3.113.36519.0
*/
/* ── USER-CONFIGURABLE VARIABLES ─────────────────────────────────── */
const container = document.getElementById("links-right");
const bgColor = "#F6F9FB";
const barColor = "#FCCE4B";
const compColor = "#19BB32";
const borderRad = "100px";
const progressVar = "Progress";
const fallbackCurrentSlideVar = "CurrentSlide";
const fallbackTotalSlidesVar = "TotalSlides";
const wrapperX = "20px";
const wrapperY = "0px";
const progressTextWidth = "190px";
const barWidth = "220px";
const barHeight = "15px";
const slideTextWidth = "95px";
const barX = "200px";
const slideTextX = "440px";
/* ── INTERNAL LOGIC ──────────────────────────────────────────────── */
const player = GetPlayer();
function getRawVar(varName) {
try { return player.GetVar(varName); } catch (e) { return null; }
}
function getNumVar(varName, fallback) {
const num = Number(getRawVar(varName));
return Number.isFinite(num) ? num : fallback;
}
function getProgressValue() {
return Math.max(0, Math.min(100, getNumVar(progressVar, 0)));
}
function getSlideValues() {
const builtInCurrent = getNumVar("MenuSection.SlideNumber", NaN);
const builtInTotal = getNumVar("MenuSection.TotalSlides", NaN);
if (Number.isFinite(builtInCurrent) && Number.isFinite(builtInTotal) && builtInTotal > 0) {
return { current: builtInCurrent, total: builtInTotal, source: "built-in" };
}
const fbCurrent = getNumVar(fallbackCurrentSlideVar, NaN);
const fbTotal = getNumVar(fallbackTotalSlidesVar, NaN);
if (Number.isFinite(fbCurrent) && Number.isFinite(fbTotal) && fbTotal > 0) {
return { current: fbCurrent, total: fbTotal, source: "fallback" };
}
return { current: null, total: null, source: "none" };
}
function ensureUI() {
let progressBarContainer = document.getElementById("progressBarContainer");
if (!progressBarContainer) {
progressBarContainer = document.createElement("div");
progressBarContainer.id = "progressBarContainer";
progressBarContainer.style.cssText =
`height:20px;position:relative;transform:translate(${wrapperX},${wrapperY});white-space:nowrap;`;
const progressBarText = document.createElement("span");
progressBarText.id = "progressBarText";
progressBarText.style.cssText =
`position:absolute;left:0;top:0;width:${progressTextWidth};text-align:right;font-size:15px;font-weight:600;color:white;`;
const progressBarHolder = document.createElement("div");
progressBarHolder.id = "progressBarHolder";
progressBarHolder.style.cssText =
`position:absolute;left:${barX};top:3px;width:${barWidth};height:${barHeight};`;
const bgBar = document.createElement("div");
bgBar.id = "bgBar";
bgBar.style.cssText =
`position:absolute;left:0;top:0;width:100%;height:100%;background:${bgColor};border-radius:${borderRad};`;
const pBar = document.createElement("div");
pBar.id = "pBar";
pBar.style.cssText =
`position:absolute;left:0;top:0;height:100%;border-radius:${borderRad};`;
const slideCounterText = document.createElement("span");
slideCounterText.id = "slideCounterText";
slideCounterText.style.cssText =
`position:absolute;left:${slideTextX};top:0;width:${slideTextWidth};text-align:left;font-size:15px;font-weight:600;color:white;`;
progressBarHolder.appendChild(bgBar);
progressBarHolder.appendChild(pBar);
progressBarContainer.appendChild(progressBarText);
progressBarContainer.appendChild(progressBarHolder);
progressBarContainer.appendChild(slideCounterText);
container.appendChild(progressBarContainer);
if (window.getComputedStyle(container).display === "none") {
container.style.display = "block";
}
}
}
function updateUI() {
const progressValue = getProgressValue();
const slideValues = getSlideValues();
const progressBarText = document.getElementById("progressBarText");
const slideCounterText = document.getElementById("slideCounterText");
const pBar = document.getElementById("pBar");
if (progressBarText) progressBarText.textContent = "Course Progress: " + progressValue + "%";
if (slideCounterText) {
slideCounterText.textContent = slideValues.current !== null
? "Slide: " + slideValues.current + "/" + slideValues.total
: "Slide: --/--";
}
if (pBar) {
pBar.style.width = progressValue + "%";
pBar.style.backgroundColor = (progressValue === 100 && compColor) ? compColor : barColor;
}
}
ensureUI();
updateUI();
setTimeout(updateUI, 150);
setTimeout(updateUI, 400);
if (!window.jbCourseHeaderUpdater) {
window.jbCourseHeaderUpdater = setInterval(updateUI, 500);
}Reads the current scene name and writes it into a Storyline variable for on-slide display. Works with or without the player menu enabled.
What It Does
Reads which scene the learner is currently in and writes the scene name into a Storyline variable called SceneTitle. Reference it on any slide with %SceneTitle% and the current scene name appears without any hard-coding per slide. Storyline does not provide a built-in variable for the scene name, so this script fills that gap. This script works whether the player menu is enabled or not.
How It Works
The script uses two detection methods and tries them in order. Method 1 (Primary) reads DS.slideNumberManager.links, an internal Storyline data structure that contains the full course outline with scenes and their child slides. This works whether the player menu is enabled or not. Method 2 (Fallback) searches the player menu DOM for scene headings if the data store is unavailable. If the menu has not loaded when the fallback runs, it retries up to 20 times at 150 ms intervals.
Setup
- Open your Storyline project, go to View, then Slide Master, and select the parent Slide Master.
- Add a trigger: Adjust Variable. Create a variable called CurrentSlideTitle and set it equal to Menu.SlideTitle. Set it to fire when the timeline starts.
- Add a second trigger: Execute JavaScript, paste the contents of the script. This creates and writes to a variable called SceneTitle.
- On any slide where you want the scene name to appear, insert a text reference using %SceneTitle%.
Menu Numbering
This script works whether player menu numbering is on or off. Slide matching uses an endsWith comparison rather than strict equality, so a numbered entry like "8.17. Decision 3" still matches the slide title "Decision 3". Scene names are stripped of any leading number prefix before being written to SceneTitle, so the variable always contains a clean name.
HTML Entity Decoding
Scene names from the internal data store may contain HTML entities such as & instead of &. The script decodes these automatically so SceneTitle displays clean text like "Curriculum & Lesson Plan Design" instead of "Curriculum & Lesson Plan Design".
Diagnostic Values
| Value in SceneTitle | Meaning |
|---|---|
| ERR reading CurrentSlideTitle | The variable threw an error when read |
| NO CurrentSlideTitle | The variable exists but is empty |
| NO menu rows | Data store had no match and the menu DOM is not available |
| NO slide match: [title] | No menu entry matched the current slide title (fallback only) |
| NO row index | Matched row not found in the full row list |
| BLANK scene row | Scene heading found but had no text |
| NO parent scene | Slide appears before any scene heading (fallback only) |
Changelog
| Version | Date | Notes |
|---|---|---|
| 2.0.0 | 2026-04-08 | Added primary detection via DS.slideNumberManager.links. Menu DOM lookup is now a fallback. Added HTML entity decoding. |
| 1.0.1 | 2026-04-08 | Fixed slide matching to use endsWith so menu numbering prefixes do not break the lookup. Scene names now stripped of number prefixes. |
| 1.0.0 | 2026-04-06 | Initial release. |
/**
* ============================================================================
* JavaScript Trigger Scene Title in Storyline
* ============================================================================
*
* Purpose: Reads the current slide's title from the Storyline player,
* determines which scene the slide belongs to, and writes the
* scene name into a Storyline variable called "SceneTitle".
* This lets authors display the current scene name on-slide
* (via a text reference to %SceneTitle%) without manually
* typing the scene name on every slide.
*
* Storyline does not provide a built-in variable for the
* scene name, so this script fills that gap.
*
* Author: Joseph Black
* Date: 2026-04-08
* Version: 2.0.0
*
* Software: Articulate Storyline 360 x64 v3.114.36620.0
*
* Usage: Add this script as an "Execute JavaScript" trigger on the
* parent Slide Master. It fires each time the timeline
* starts. Two Storyline variables are required:
* - CurrentSlideTitle (text, set via Adjust Variable
* trigger to Menu.SlideTitle)
* - SceneTitle (text, this script writes to it)
*
* Detection: The script uses two methods to find the scene name:
*
* Method 1 (Primary) - Internal Data Store:
* Reads DS.slideNumberManager.links, which contains the
* full course structure with scenes and their child slides.
* This works whether the course menu is enabled or not.
*
* Method 2 (Fallback) - Menu DOM:
* If the data store is unavailable, the script searches the
* player menu DOM for scene headings. This requires the
* course menu to be enabled in the player.
*
* If both methods fail, a diagnostic message is written
* to SceneTitle for troubleshooting.
*
* Notes: - Works with or without the player menu enabled.
* - Works whether player menu numbering is on or off.
* - HTML entities in scene names (e.g. &) are decoded
* automatically.
* - Diagnostic strings (prefixed "ERR", "NO", "BLANK") are
* written to SceneTitle when lookups fail, making it easy
* to troubleshoot during development.
*
* Changelog: 2.0.0 (2026-04-08) - Added primary detection method using
* DS.slideNumberManager.links so the script works without
* the player menu enabled. Menu DOM lookup is now a
* fallback. Handles HTML entity decoding in scene names.
* 1.0.1 (2026-04-08) - Fixed slide matching to use endsWith
* instead of strict equality so menu numbering prefixes
* do not break the lookup. Added number prefix stripping
* on scene titles.
* 1.0.0 (2026-04-06) - Initial release.
* ============================================================================
*/
(function () {
/* --- Storyline player API reference --- */
var player = GetPlayer();
/* --- Retry settings for waiting on data/DOM --- */
var maxAttempts = 20; // Maximum number of retries
var retryDelay = 150; // Milliseconds between retries
/**
* normalizeText - Collapses all whitespace in a string down to single
* spaces and trims the ends. Used to safely compare slide titles that
* may contain extra whitespace or line breaks.
*/
function normalizeText(value) {
return (value || "").replace(/\s+/g, " ").trim();
}
/**
* decodeHtmlEntities - Converts HTML entities (e.g. & < >)
* back to their normal characters. Scene names from the internal data
* store may contain HTML entities that need to be cleaned up before
* displaying on-slide.
*/
function decodeHtmlEntities(str) {
var el = document.createElement("textarea");
el.innerHTML = str;
return el.value;
}
/* =======================================================================
* METHOD 1 (Primary) - Internal Data Store
*
* Reads DS.slideNumberManager.links which contains an array of scene
* objects. Each scene has a displaytext (the scene name) and a links
* array of child slides, each with their own displaytext (slide name).
*
* This data exists whether the player menu is enabled or not.
* ======================================================================= */
/**
* getLinksData - Safely retrieves the links array from the internal
* data store. Returns null if the data store is not available.
*/
function getLinksData() {
try {
if (typeof DS !== "undefined" &&
DS.slideNumberManager &&
DS.slideNumberManager.links &&
DS.slideNumberManager.links.length > 0) {
return DS.slideNumberManager.links;
}
} catch (e) {}
return null;
}
/**
* findSceneFromDataStore - Searches the internal data store for a
* scene that contains a slide matching the current slide title.
* Returns the decoded scene name, or null if no match is found.
*/
function findSceneFromDataStore(currentSlideTitle) {
var links = getLinksData();
if (!links) return null;
for (var i = 0; i < links.length; i++) {
var scene = links[i];
var children = scene.links;
if (!children) continue;
for (var j = 0; j < children.length; j++) {
var childTitle = normalizeText(decodeHtmlEntities(children[j].displaytext || ""));
/* Use endsWith to handle cases where the data store includes
numbering prefixes, and also check strict equality */
if (childTitle === currentSlideTitle || childTitle.endsWith(currentSlideTitle)) {
var sceneName = normalizeText(decodeHtmlEntities(scene.displaytext || ""));
/* Strip leading number prefix if present (e.g. "2. Module 1"
becomes "Module 1") */
sceneName = sceneName.replace(/^\d+\.\s*/, "");
return sceneName || null;
}
}
}
return null;
}
/* =======================================================================
* METHOD 2 (Fallback) - Menu DOM
*
* Searches the player menu for scene headings marked with
* data-is-scene="true". Requires the course menu to be enabled.
* ======================================================================= */
/**
* isSceneRow - Returns true if a menu list-item represents a scene
* heading rather than an individual slide. Storyline marks these
* with a data-is-scene="true" attribute.
*/
function isSceneRow(row) {
return row && row.getAttribute("data-is-scene") === "true";
}
/**
* getMenuRows - Returns an array of all menu list-item elements
* (both scene headings and slide entries) from the player sidebar.
*/
function getMenuRows() {
return Array.from(
document.querySelectorAll('.cs-listitem.listitem[data-slide-title]')
);
}
/**
* findSceneFromMenuDOM - Searches the player menu DOM for the current
* slide, then walks backward through the menu rows until it finds a
* scene heading. Returns the scene name, or a diagnostic string if
* the lookup fails at any step.
*/
function findSceneFromMenuDOM(currentSlideTitle) {
var rows = getMenuRows();
if (!rows.length) return "NO menu rows";
/* Filter to slide-only rows and find the current slide.
Uses endsWith so "8.17. Decision 3" matches "Decision 3"
when menu numbering is enabled. */
var slideRows = rows.filter(function (row) {
return !isSceneRow(row);
});
var matchingSlideRow = slideRows.find(function (row) {
return normalizeText(row.getAttribute("data-slide-title")).endsWith(currentSlideTitle);
});
if (!matchingSlideRow) return "NO slide match: " + currentSlideTitle;
/* Walk backward from the matched slide to find its scene */
var rowIndex = rows.indexOf(matchingSlideRow);
if (rowIndex < 0) return "NO row index";
for (var i = rowIndex - 1; i >= 0; i--) {
var row = rows[i];
if (isSceneRow(row)) {
/* Strip leading number prefix (e.g. "2. Module 1" becomes
"Module 1") so SceneTitle contains a clean name regardless
of whether menu numbering is on or off. */
var sceneTitle = normalizeText(
row.getAttribute("data-slide-title") || row.textContent
).replace(/^\d+\.\s*/, "");
return sceneTitle || "BLANK scene row";
}
}
return "NO parent scene";
}
/* =======================================================================
* MAIN LOGIC
*
* Tries the data store method first. If that fails, falls back to the
* menu DOM method. If both fail, writes a diagnostic message.
* ======================================================================= */
/**
* updateSceneTitle - Main entry point. Reads the current slide title,
* then attempts each detection method in order.
*/
function updateSceneTitle(attempt) {
attempt = attempt || 0;
/* Step 1: Read the current slide title from Storyline */
var currentSlideTitle = "";
try {
currentSlideTitle = normalizeText(player.GetVar("CurrentSlideTitle"));
} catch (e) {
player.SetVar("SceneTitle", "ERR reading CurrentSlideTitle");
return;
}
if (!currentSlideTitle) {
player.SetVar("SceneTitle", "NO CurrentSlideTitle");
return;
}
/* Step 2: Try the internal data store (works with or without menu) */
var sceneFromDS = findSceneFromDataStore(currentSlideTitle);
if (sceneFromDS) {
player.SetVar("SceneTitle", sceneFromDS);
return;
}
/* Step 3: Fall back to the menu DOM */
var menuRows = getMenuRows();
if (!menuRows.length && attempt < maxAttempts) {
/* Menu may not have rendered yet - retry */
setTimeout(function () {
updateSceneTitle(attempt + 1);
}, retryDelay);
return;
}
var sceneFromMenu = findSceneFromMenuDOM(currentSlideTitle);
player.SetVar("SceneTitle", sceneFromMenu);
}
/* --- Run immediately --- */
updateSceneTitle();
})();Pulls the learner name from the LMS via SCORM, reformats it from "Last, First" to "First Last", and stores it for on-slide display.
What It Does
Pulls the learner name from the LMS via the SCORM API and reformats it from "Last, First" to "First Last". The result is stored in a Storyline variable called lmsName. Reference it on any slide with %lmsName% for personalized certificates or greeting messages.
How It Works
Calls lmsAPI.GetStudentName() to get the name from the LMS, which most platforms return as "Last, First". Splits the string on the comma, then reassembles the parts as "First Last". Writes the result into the lmsName Storyline variable.
Setup
- Create a text variable in Storyline named lmsName.
- On the completion slide Slide Master layout, add a trigger: Execute JavaScript, paste the contents of the script.
- Reference the variable on-slide with %lmsName%, for example "Congratulations, %lmsName%!".
Requirements and Considerations
The course must be hosted in an LMS that exposes the SCORM API. This script will not work in Storyline preview or standalone published output since lmsAPI is only available when the course runs from an LMS. Most platforms return names as "Last, First" but some differ. If your LMS already returns "First Last", the output will be incorrect. The split on the comma can leave a leading space on the first name, so add .trim() calls if you see extra whitespace.
/**
* JavaScript Trigger Retrieve LMS Name in Storyline
*
* Purpose: Retrieves the learner name from the LMS via the SCORM
* API, reformats from "Last, First" to "First Last", and
* stores the result in a Storyline variable "lmsName".
*
* Author: Joseph Black | Version: 1.0.0
* Software: Articulate Storyline 360 x64 v3.113.36519.0
*/
/* Get a reference to the Storyline player API */
let player = GetPlayer();
/* Retrieve the learner name from the LMS via the SCORM API.
Most LMS platforms return this in "Last, First" format. */
let myName = lmsAPI.GetStudentName();
/* Split on the comma: ["Last", " First"] */
let array = myName.split(',');
/* Reassemble as "First Last".
Add .trim() calls if your LMS leaves leading whitespace:
let newName = array[1].trim() + ' ' + array[0].trim(); */
let newName = array[1] + ' ' + array[0];
/* Write the reformatted name into the Storyline variable */
player.SetVar("lmsName", newName);