Skip to content

Embedding Squiffy in a custom page

If you want more control over how your Squiffy story is presented, you can embed it in your own HTML page. This allows you to customize the layout, add your own styling, or integrate the story into an existing website.

First, export your story using the CLI with --scriptonly:

npx @textadventures/squiffy-cli mygame.squiffy --scriptonly

This creates story.js containing your compiled story.

You’ll also need the Squiffy runtime. You can either:

  • Run a full export first and keep squiffy.runtime.global.js
  • Or use a CDN (when available)

Here’s a minimal HTML page that runs a Squiffy story:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Story</title>
<script src="squiffy.runtime.global.js"></script>
<script src="story.js"></script>
</head>
<body>
<div id="squiffy"></div>
<script>
document.addEventListener('DOMContentLoaded', async function () {
const squiffyApi = await squiffyRuntime.init({
element: document.getElementById('squiffy'),
story: story,
});
await squiffyApi.begin();
});
</script>
</body>
</html>

The squiffyRuntime.init() function accepts an options object:

OptionTypeDefaultDescription
elementHTMLElement(required)The container element for the story output
storyStory(required)The compiled story object (from story.js)
persistbooleantrueWhether to save state to localStorage
storyIdstringURL pathKey used for localStorage (allows multiple stories on same domain)
scrollstring"body"Scroll behavior: "body", "element", or "none"
onSetfunction-Callback when an attribute is set

By default, Squiffy saves the player’s progress to localStorage, so they can return to where they left off. The storyId option determines the storage key:

const squiffyApi = await squiffyRuntime.init({
element: document.getElementById('squiffy'),
story: story,
persist: true,
storyId: 'my-unique-story-id',
});

Set persist: false to disable saving:

const squiffyApi = await squiffyRuntime.init({
element: document.getElementById('squiffy'),
story: story,
persist: false,
});

The scroll option controls how the page scrolls when new content appears:

  • "body" (default) - Scrolls the page body
  • "element" - Scrolls within the container element (useful for fixed-height containers)
  • "none" - No automatic scrolling
const squiffyApi = await squiffyRuntime.init({
element: document.getElementById('squiffy'),
story: story,
scroll: 'element',
});

The init() function returns a SquiffyApi object with these methods:

Starts or resumes the story. Call this after initialization:

await squiffyApi.begin();

Restarts the story from the beginning, clearing saved state:

squiffyApi.restart();

Gets the value of a story attribute:

const score = squiffyApi.get('score');
const hasKey = squiffyApi.get('hasKey');

Sets a story attribute:

squiffyApi.set('score', 100);
squiffyApi.set('playerName', 'Alice');

Goes back to the previous section or passage (undo):

squiffyApi.goBack();

You can listen for events using on(), off(), and once():

Fired when a story link is clicked:

squiffyApi.on('linkClick', (event) => {
console.log('Link type:', event.linkType); // "section" or "passage"
});

Fired when an attribute value changes:

squiffyApi.on('set', (event) => {
console.log(`${event.attribute} = ${event.value}`);
});

Fired when the ability to go back changes:

squiffyApi.on('canGoBackChanged', (event) => {
backButton.disabled = !event.canGoBack;
});

The on() method returns an unsubscribe function:

const unsubscribe = squiffyApi.on('linkClick', handler);
// Later, to stop listening:
unsubscribe();

Or use off():

squiffyApi.off('linkClick', handler);

Here’s a more complete example with custom controls:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Story</title>
<script src="squiffy.runtime.global.js"></script>
<script src="story.js"></script>
<style>
#controls {
padding: 10px;
background: #f0f0f0;
margin-bottom: 20px;
}
#squiffy {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
button:disabled {
opacity: 0.5;
}
</style>
</head>
<body>
<div id="controls">
<button id="restart">Restart</button>
<button id="back" disabled>Back</button>
<span id="status"></span>
</div>
<div id="squiffy"></div>
<script>
document.addEventListener('DOMContentLoaded', async function () {
const squiffyApi = await squiffyRuntime.init({
element: document.getElementById('squiffy'),
story: story,
persist: true,
storyId: window.location.pathname,
});
const backButton = document.getElementById('back');
const statusEl = document.getElementById('status');
// Restart button
document.getElementById('restart').addEventListener('click', function () {
if (confirm('Are you sure you want to restart?')) {
squiffyApi.restart();
}
});
// Back button
backButton.addEventListener('click', function () {
squiffyApi.goBack();
});
// Update back button state
squiffyApi.on('canGoBackChanged', (event) => {
backButton.disabled = !event.canGoBack;
});
// Show score changes
squiffyApi.on('set', (event) => {
if (event.attribute === 'score') {
statusEl.textContent = `Score: ${event.value}`;
}
});
await squiffyApi.begin();
});
</script>
</body>
</html>

Squiffy adds CSS classes to its output that you can style:

  • .squiffy-output-section - Container for each section
  • .squiffy-output-passage - Container for each passage
  • .squiffy-link - All clickable links
  • .squiffy-link.disabled - Links that have been clicked/disabled

You can include the default styles from a full export (style.css) and customize from there, or write your own styles from scratch.