Skip to main content

React Text Game

Build interactive fiction with a small engine layer and use as much—or as little—of the supplied React UI as you want.

React Text Game is split into four libraries:

PackageUse it for
@react-text-game/corereactive game entities, passage navigation, saves, migrations, audio, the game clock, and i18n
@react-text-game/uiready-made story and map renderers, save UI, and replaceable component slots
@react-text-game/mdxauthoring story passages in MDX
@react-text-game/messengerpersistent chat transcripts with unread tracking, for messenger sims and visual novels

Only core is required. Each of the others is optional and independently usable.

One more package ships as a development tool rather than a library:

PackageUse it for
@react-text-game/devtoolsthe rtg CLI, which tells you whether a release needs a save migration

A passage in 30 seconds

src/game/intro.ts
import { defineStory } from "@react-text-game/core";

export const intro = defineStory("intro", (h) => [
h.header("The station", { level: 1 }),
h.text("The last train is waiting."),
h.actions([{ content: "Board the train", action: h.jump("inside-train") }]),
]);

The content callback receives a toolbox of component builders (h) as its first argument, so you never have to write component objects by hand.

Passages and entities register when their modules are imported. GameProvider initializes the engine and renders the current passage:

src/main.tsx
import "./game/intro";
import "@react-text-game/ui/styles";
import { GameProvider, PassageController } from "@react-text-game/ui";

const options = {
gameName: "Night Train",
startPassage: "intro",
isDevMode: import.meta.env.DEV,
};

root.render(
<GameProvider options={options}>
<PassageController />
</GameProvider>
);

Choose your path

Design principles

  • Game state belongs to core entities, not UI components.
  • A passage describes what to display; a renderer decides how it looks.
  • Map coordinates are percentages of the fitted source image, so hotspots remain anchored across viewport sizes.
  • Callable content is evaluated when a passage is displayed, allowing state-driven stories without a second schema.

The packages are independently usable, fully typed, and published under the MIT license. Source and example applications are available on GitHub.