Lean, hackable, extensible slide deck framework. Create basic slides by just writing HTML and CSS, do fancy custom stuff with JS, the sky is the limit!
This repo is the core engine (@inspirejs/core). Inspire.js is split across a few packages:
| Package | What it is |
|---|---|
@inspirejs/core |
this repo — the core engine + inspire.css |
@inspirejs/plugins |
optional plugins, autoloaded on demand |
inspirejs.org |
meta package: bundles core + plugins in one install |
| demo / theme | the inspirejs.org site + the default theme |
npm install @inspirejs/core@inspirejs/core is native ESM and imports its dependencies by bare specifier (e.g. @inspirejs/core). Resolve those however your project already does — an import map, a bundler, or a no-build tool. Then include the stylesheet and import the engine:
<link href="/path/to/@inspirejs/core/inspire.css" rel="stylesheet" />
<script type="module">
import "@inspirejs/core"; // initializes automatically
</script>That’s it. Slides are any elements with class="slide" — no JS needed to create them. import Inspire from "@inspirejs/core" gives you the API (also the global Inspire).
You don’t need plugins to use Inspire.js. Extra functionality (syntax highlighting, presenter view, live demos, …) lives in the separate @inspirejs/plugins package, which autoloads on demand — just add a second import:
import "@inspirejs/plugins";Or install inspirejs.org, which bundles core + plugins.
Inspire waits for plugins (and any promises in Inspire.delayInit) before initializing, but not forever: after Inspire.initTimeout (default 10 s) it initializes anyway and warns in the console about what it stopped waiting for. Set Inspire.initTimeout right after importing core to change the budget.
A few fundamental features ship bundled with core as built-in plugins — modular internally, but always on, with nothing extra to install. Currently:
autosize— shrinks a slide's font-size to keep its content from overflowing the viewport. Opt out for the whole deck withclass="no-autosize"(orno-plugins) on<body>, or per slide/subtree withclass="dont-resize",--dont-resize,--font-sizing: fixed, oroverflow: hidden | clip.data-steps— numbered steps on an element, see Incremental display.
Old absolute URLs like https://inspirejs.org/inspire.mjs, /inspire.css, and /plugins/… still resolve, but migrate off them as soon as possible. They only redirect to the new packages to soften breakage; because the underlying files have been reorganized, they are unlikely to keep working correctly. Depend on the npm packages instead.
To reveal parts of a slide one step at a time, add class="delayed" to any element. Each delayed element appears on its own step as you advance, in source order:
<ul>
<li class="delayed">First, then…</li>
<li class="delayed">second, then…</li>
<li class="delayed">third.</li>
</ul>Use class="delayed-children" on a container to make all of its direct children delayed, without annotating each one:
<ul class="delayed-children">
<li>First</li>
<li>Second</li>
<li>Third</li>
</ul>Every delayed element carries one of three classes at any time: future (its step hasn't come yet), current (its step is the one you're on) or past (you've moved past it). Style them however you like; inspire.css fades future out. A few modifiers are built in:
| Modifier | Effect |
|---|---|
transient (or delayed-transient on a container) |
past items dim to 30% |
collapse (or delayed-collapse) |
future items take no space |
collapse-notcurrent (or delayed-collapse-notcurrent) |
only the current item takes space |
delayed-last on the slide |
one extra step at the end where nothing is current |
By default, delayed items reveal in source order. Add data-index to override that order — items are revealed from lowest index to highest (items without data-index count as 0, so they come first). The values only set relative order; gaps don't matter, so data-index="2" then data-index="10" is the same as 1 then 2.
Items that share the same data-index are revealed together, in a single step. This is handy for revealing a whole group at once — e.g. a table column:
<tr><td>R</td><td>255</td><td class="delayed" data-index="1">0xFF</td></tr>
<tr><td>G</td><td>0</td> <td class="delayed" data-index="1">0x00</td></tr>
<tr><td>B</td><td>140</td><td class="delayed" data-index="1">0x8C</td></tr>All three 0x… cells appear on the same step. Items without data-index are never grouped — each still gets its own step.
data-index="0" is special: it means "with the slide". Such items are already past when the slide appears and are rewound when you leave it, which is mostly useful with the attributes below.
Revealing is only one thing a step can do. The same delayed word, as a prefix, folds anything else into the slide's flow:
<!-- The slide gains `zoomed`, then `answered`, one step each. Nothing is hidden. -->
<section class="slide delayed:zoomed delayed:answered">…</section>
<!-- Attributes: <details> opens at its step; inline styles merge per property -->
<details delayed:open>…</details>
<p style="color: gray" delayed:style="color: red" delayed[2]:style="color: blue">…</p>
<!-- Code runs when its step is reached, with `this` being the element -->
<button delayed:script="this.click()">Play</button>The grammar is delayed(.modifier)*([index])?(:name)?, the same for class tokens and attribute names:
[index]works likedata-index:delayed[2]:litshares a step with a.delayeditem that hasdata-index="2", anddelayed[0]:scriptruns as soon as the slide appears..transientholds only while the step is current (delayed.transient:glow), instead of from the step on..alwaysre-runs a script every time its step is reached; by default scripts run once per element.- Steps follow source order: attributes in the order they are written, and class tokens where the
classattribute sits among them. - Setting the same attribute at two steps needs two distinct names, e.g.
delayed:styleanddelayed[1]:style; the latest active one wins. delayed:scriptwith an empty value runs the element's text, for longer code:<script type="text/plain" delayed:script>…</script>.class,idanddata-indexcannot be delayed.
Because a slide is left by rewinding it, a delayed <style> that must affect things outside the slide can be <style media="not all" delayed[0]:media="all">. Styles scoped to the slide need no help: <style>@scope { … }</style>.
data-steps="3" gives an element three anonymous steps and reflects where you are as attributes: data-step (the current step, absent before the first) and data-step-all (every step reached, e.g. "0 1 2", for cumulative styles via [data-step-all~="1"]). Descendants can declare when they show, relative to their closest stepped ancestor: data-step="2" (only during step 2), data-step-min="2" (from step 2 on), data-step-max="2" (until step 2). The delayed: classes above are usually the more readable option, since you name states instead of numbering them.
Every element whose item changes state gets a bubbling itemchange event, with event.item, event.state and event.active:
slide.addEventListener("itemchange", evt => {
if (evt.state === "current") {
// evt.target just got its turn
}
});Inspire.item is the current step, Inspire.gotoItem(n) goes to one, Inspire.items.all lists the current slide's items and Inspire.items.count its step count. The gotoitem-end hook runs after every step change. DOM changes inside the current slide are picked up automatically (class changes and added/removed elements); after changing attributes from JS, call Inspire.domchanged(element).
All of the above are item types registered through one API, and you can add your own:
Inspire.items.register({
name: "pulse",
selector: "[data-pulse]", // elements that get items (the slide too, if it matches)
items: element => [{}, {}], // descriptors per element; default is one. `index`, `transient`, `key` are understood by core
apply (element, items, env) { // called once per element whose items changed state
element.classList.toggle("pulsing", items.some(item => item.active));
},
});Each item has element, index, step, state (future, current or past) and active (past-or-current, or only current when transient). A type without a selector gets items(slide) called once and returns descriptors with their own element, for syntaxes no selector can find.
await Inspire.importsLoaded;
// code to run after imports have loadedNote that await needs to be inside an async function otherwise it will error. However, this could just be a self-executing async function.
await Inspire.importsLoaded;
await Inspire.plugins.loaded.PLUGIN_ID.loaded;
// code to run after the plugin with id PLUGIN_ID has loaded and executedor:
await Inspire.loadPlugin(PLUGIN_ID);
// code to run after the plugin with id PLUGIN_ID has loaded and executedThe second example would load the plugin if it hasn't otherwise been loaded, but it will never be loaded twice.
These promises reject if the plugin fails to load. A plugin that stalls (e.g. a remote dependency that never responds) keeps them pending; initTimeout only stops the slideshow from waiting, it does not settle them.
You can do this via the slidechange hook:
Inspire.hooks.add("slidechange", env => {
if (Inspire.currentSlide.id === "slide-id") {
// Code to run
}
});or, via an event:
document.addEventListener("slidechange", evt => {
if (Inspire.currentSlide.id === "slide-id") {
// Code to run
}
});You can do this via the slidechange hook:
Inspire.hooks.add("slidechange", env => {
if (Inspire.currentSlide.id === "slide-id" && env.firstTime) {
// Code to run
}
});or, via an event:
document.addEventListener("slidechange", evt => {
if (Inspire.currentSlide.id === "slide-id" && evt.firstTime) {
// Code to run
}
});or:
$("#slide-id").addEventListener(
"slidechange",
evt => {
// Code to run
},
{ once: true },
);You can do this via the slidechange hook:
Inspire.hooks.add("slidechange", env => {
if (env.prevSlide.id === "slide-id") {
// Code to run
}
});or, via an event:
document.addEventListener("slidechange", evt => {
if (evt.prevSlide.id === "slide-id") {
// Code to run
}
});