inkjs: the ink narrative runtime for JavaScript
A javascript port of inkle's ink scripting language.
At a glance
- What is it?
- inkjs is a JavaScript and TypeScript port of inkle's ink, the interactive narrative scripting language, published with zero dependencies and running in browsers and in Node, including a compiler CLI and typed entry points. It suits a web or Node project that wants ink stories without a Unity integration, and it is a port of another runtime rather than an independent implementation.
- Who is it for?
- Adopt inkjs if you are building an interactive narrative in a browser or a Node service and want to compile ink source with the inkjs compiler and run it on a JavaScript runtime without pulling in a native engine. Do not adopt it as a replacement for the original ink toolchain in a C# or Unity project, where the official runtime is the supported path.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 52 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A runtime port of ink, with no dependencies
inkjs is a JavaScript port of inkle's ink, a scripting language for writing interactive narrative, and the README's claim is blunt: inkjs is fully compatible with the original version, has zero dependency and works in all browsers and node.js.
Zero dependency is worth pausing on, because the original ink toolchain is a C# implementation with a compiler that emits JSON. A JavaScript port that carries no dependencies means the runtime can be dropped into a page with a script tag, bundled without a tree of transitive packages, or required in a Node service without a package audit.
The compatibility claim is the one to treat carefully. The README also lists differences with the C# API, and the section is titled there are a few very minor API differences. So the compatibility is with the ink language and the runtime semantics, with a small set of API differences that the README documents in detail, which is a reasonable thing to expect from a port of this kind and is worth reading before you port code rather than story content.
The implementation is TypeScript, built with tsc and bundled with rollup, and the tests run under jest in two configurations, one for TypeScript and one for the built JavaScript in both the modern dist output and the legacy build. There are separate jest configuration files for the JavaScript runs, a .babelrc for the legacy transpilation, an .eslintrc.js for linting, a .prettierrc.json for formatting, renovate.json for dependency updates, and a .codecov.yml for coverage.
The package metadata shows a deliberate care about module resolution. The type field is commonjs, the main entry is dist/ink-full.js, the types are ink.d.ts, and there is an exports map with separate conditions for import and default, plus subpath exports for ./full, ./types, ./engine/* and ./compiler/*, and a second declaration file, ink.d.mts, for ESM consumers. The files array ships the engine, the compiler, the dist output and the bin directory, so a consumer can import just the runtime without the compiler.
The licence is MIT, the default branch is master, and the project is not archived, with the last push on 2026-09-01 and releases 2.3.1, 2.3.2 and 2.4.0 in late 2024, April 2025 and February 2026.
The quickstart: a Story object and a global
The README points new users at a serverless boilerplate in the templates folder, browser_serverless, and the instruction is to replace the placeholder story in story.js with your own and open index.html. No build step, no server.
The mechanism behind that template is a global object. The README explains that inkjs gives you access to a global object named inkjs which has a property called Story, and that this is the main class we interact with. Creating a story is then a constructor call, var story = new inkjs.Story(storyContent), where storyContent is defined in the story.js file. After that you can use story.Continue() and story.currentChoices as described in the official documentation.
So the runtime surface is small. Continue produces the next chunk of narrative, currentChoices exposes the choices available to the player, and the rest of the API follows the official ink runtime documentation, which the README links to directly for RunningYourInk.
That is the right approach for a port. Rather than inventing a JavaScript-shaped API, inkjs mirrors the C# runtime so that the documentation, the tutorials and the community knowledge apply. The cost is that you are reading documentation written for a different language, and the benefit is that there is exactly one runtime model to learn.
The templates directory also holds a browser_with_server variant and a nodejs variant, so the same story can be moved between an embedded, a served and a server-side setup without rewriting the narrative code, which is the right way round. The templates are part of the repository rather than a separate example site, so they travel with the package version you are using.
Loading a JSON story means dealing with CORS
If you update your story often, pasting the content into story.js becomes tedious, so the README suggests dynamically loading the JSON file for the story. And then it explains the catch, in the most practical terms possible: your browser will not let you do that because of CORS policy, which means you need a web server to do this. The README suggests node.js with the http-server package, or python with a simple HTTP server.
That constraint shapes the architecture of any project using inkjs. A story compiled to JSON and loaded at runtime is the right choice for a narrative that changes often, because you can deploy a new story file without rebuilding the application. It also means the application needs to be served over HTTP rather than opened as a file, which is a one-line difference in how you run it locally and a real difference in how you deploy it.
The boilerplate that handles it, browser_with_server, loads the story content from story.json before creating the story:
fetch('story.json')
.then(function (response) {
return response.text();
})
.then(function (storyContent) {
story = new inkjs.Story(storyContent);
continueStory();
});The asynchronous shape of that code is worth noticing. The story does not exist synchronously at page load, so the flow is fetch, then construct the Story, then continue. Any code that assumed a story was ready on the first line has to move into the continuation, which is a small but real structural constraint on how you write the surrounding application.
The README also links a live demo hosted on the author's site, which is the fastest way to see the runtime working with real content before you set anything up.
Node.js: two import styles and a byte order mark
For Node there is boilerplate in the templates folder under nodejs, and two ways to load the module. The CommonJS require form is var Story = require('inkjs').Story, and the ESM import form is import { Story } from 'inkjs'. The package supports both, which follows from the exports map that offers an import condition and a default condition.
For a story file, the README gives the simple option first, require('./ink_file.json'), and then warns about a real problem with the alternative. If you load the file with fs instead, note that inklecate outputs a json file encoded with BOM, and node isn't very good at handling that. The workaround is to strip it:
var json = fs.readFileSync('./ink_file.json', 'UTF-8').replace(/^\uFEFF/, ''); //strips the BOMA byte order mark at the start of a JSON file is tolerated by many parsers and rejected by others, and it is a classic source of a stack trace that blames the JSON rather than the file encoding. Since the ink compiler produces that BOM, every Node project that reads a compiled story with fs will hit this, and the fix is one line once you know about it.
Starting the story is then the same as in the browser:
var inkStory = new Story(json);
console.log(inkStory.ContinueMaximally());ContinueMaximally is the variant that returns the whole remaining text of the current flow rather than the next chunk, which is what you want when you are rendering a complete page of narrative in one go. From there the README sends you to the official runtime documentation, which covers the rest of the API.
The Node story is also where a server-side narrative game belongs, and the fact that the module is CommonJS-first with a type field of commonjs means it requires cleanly from a CommonJS Node service without any build configuration on your side.
The two API differences that will bite a port
The README documents the differences with the C# API in two subsections, and they are worth reading in full if you are porting runtime code rather than only ink content.
The first is getting and setting ink variables. On platforms that do not support ES2015 Proxies, which the README describes as basically node.js v5, IE 11, Safari 9 and everything below, you cannot directly read and write variables to the story state. Instead you have to use the $ function:
_inkStory.variablesState.$('player_health', 100);
//instead of _inkStory.variablesState["player_health"] = 100;
var health = _inkStory.variablesState.$('player_health');
//instead of var health = _inkStory.variablesState["player_health"];The mechanism behind this is that the C# implementation exposes variable state through a dynamic object, and JavaScript reproduces that behaviour using a Proxy. Where Proxies are unavailable, the proxy layer is missing and the direct property access has no backing store, so the $ function provides an explicit path that works everywhere.
The second difference is retrieving output text when calling EvaluateFunction. The normal call is the same as in C#, var result = EvaluateFunction('my_ink_function', ['arg1', 'arg2']), where result is the return value. But if you also want the text the function wrote to the output, you need to pass a third argument of true, and the result becomes an object with two properties, result.returned for the return value and result.output for the text written while the function was evaluated.
Both differences are the kind that compile fine and behave oddly, which is exactly why they are written down. The Proxies one is now largely a historical concern, since any current Node or browser supports them, but the README keeps the note because the legacy browser build is still shipped.
The compiler CLI and the package layout
inkjs ships a compiler as well as a runtime, and the README documents a command line tool called inkjs-compiler.js, which the package.json also registers as a bin entry named inkjs-compiler so it is on your path after install.
The usage text the README reproduces shows what it does:
$ node inkjs-compiler.js -h
Usage: inkjs-compiler <options> <ink or json file>
-o <filename>: Output file name
-c: Count all visits to knots, stitches and weave points, not
just those referenced by TURNS_SINCE and read counSo the compiler takes an ink or json file, writes an output file with -o, and has a counting mode with -c that counts all visits to knots, stitches and weave points rather than only those referenced by TURNS_SINCE and read counts. That second mode is a diagnostic tool for narrative authors, since it tells you which parts of a story are never reached.
The package layout supports several consumption shapes, and the exports map is the thing to read. The root export gives you the runtime with an import condition pointing at an ESM build and a default condition at the CommonJS build. The ./full subpath gives the full build, which is the main entry point in the package metadata. The ./types subpath exists to give you the type declarations without the runtime, which is useful when you only want the compiler types. The ./engine/* and ./compiler/* subpaths let you import individual pieces.
The build is a tsc compile followed by a rollup bundle, and the watch script does the same in watch mode. The test command runs the build first and then the TypeScript and JavaScript suites, and the JavaScript suite is run twice, once against dist and once against legacy, which is how the project keeps both browser targets honest.
The README also has sections on an online compiler and on differences with the C# compiler, and a compatibility table, all of which point in the same direction: this project tries to be a faithful port rather than a reimagining, and it documents the places where it is not.
Where inkjs is the wrong tool
The first limitation is that it is a port with a port's constraints. The ink language itself is developed as part of the C# toolchain, and a JavaScript implementation necessarily follows at whatever pace that development happens. The inkjs releases are 2.3.1, 2.3.2 and 2.4.0 across late 2024, April 2025 and February 2026, so the project does move, but it is not the place where new ink language features are designed.
The second is the browser support story. The README notes that for projects targeting older browsers with no support for ES2015 features, a heavier but more backward compatible version is exposed, at inkjs/dist/ink.js, and the unpkg link it gives is pinned to version 1.11.0 of that file. The practical consequence is that a project supporting legacy browsers is depending on a much older build than the current release, with none of the later fixes, and the direct property access to variables needs the $ form there.
The third is the compiler's own scope. The tool compiles ink to JSON for the JavaScript runtime, and the README's differences-with-the-C# compiler section implies the two are not identical. If your content depends on a compiler behaviour that only the official C# compiler implements, you will find out at compile time.
The fourth is that inkjs is not an integration. If you are building in Unity, or with any tool that expects the official runtime, inkjs is not the component you install, and using it means writing your own bridge, which is more work than the port saves.
The fifth is the type situation in older projects. The package ships ink.d.ts and ink.d.mts and the exports map is careful about import and default conditions, but a project on an older TypeScript configuration, or one that resolves types differently for CommonJS and ESM, will need the type entry points checked rather than assumed.
My judgement is that inkjs earns its place in exactly two situations: a browser narrative that needs to be updated without a rebuild, and a Node service that wants ink stories without a native runtime. Outside those, the official ink toolchain with its own runtime is the lower-risk path.
Editorial conclusion
Adopt inkjs if you are building an interactive narrative in a browser or a Node service and want to compile ink source with the inkjs compiler and run it on a JavaScript runtime without pulling in a native engine. Do not adopt it as a replacement for the original ink toolchain in a C# or Unity project, where the official runtime is the supported path. Verify first that the inkjs compiler accepts your .ink source, that your Node version supports ES2015 Proxies so you can use the normal variable access path, and that the story JSON you load has no byte order mark, since the licence is MIT, the newest release is 2.4.0 from 2026-02-17, and the last push was on 2026-09-01.
Frequently asked questions
What is inkjs and is it compatible with the original ink?
inkjs is a JavaScript and TypeScript port of inkle's ink, the scripting language for interactive narrative, and the README states it is fully compatible with the original version, has zero dependency and works in all browsers and node.js. It does document a few minor API differences with the C# API.
How do I install inkjs and load a story?
Run npm install inkjs, or grab the latest release from unpkg and include it with a script tag. In the browser the runtime exposes a global inkjs object whose Story property is the main class, so you create a story with new inkjs.Story(storyContent) and continue it with story.Continue() and story.currentChoices.
Why does loading a story JSON in the browser need a web server?
The README explains that the browser will not let you fetch the JSON file because of CORS policy, so you need a web server such as node.js with the http-server package or a simple python HTTP server. The browser_with_server template loads story.json with fetch before creating the story.
How do I read and set ink variables in inkjs?
On platforms without ES2015 Proxies, which the README lists as node.js v5, IE 11, Safari 9 and below, you use the $ function, for example _inkStory.variablesState.$('player_health', 100) instead of direct property access, and the same function to read a value back.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/y-lohse-inkjs)Was this useful?