Skip to content

NamelessProj/NamelessStory

Repository files navigation

NamelessStory

Want to make a visual novel but don't know how to code? NamelessStory is an open-source visual novel engine that lets you create your own interactive story by writing a single JSON file. No programming required — just write your story, drop in your images and music, and play.

React.js version License Repo size Codacy Badge

Table of Contents

  1. What is NamelessStory?
  2. Getting Started (Running Locally)
  3. Project Structure
  4. Writing Your Story
  5. Where to Put Your Files
  6. Connecting Your Story to the App
  7. What You Can Customize
  8. Saving and Loading
  9. Deploying Online for Free (GitHub + Vercel)
  10. Contributing
  11. Schema Validation Test Files
  12. Disclaimer
  13. License

What is NamelessStory?

NamelessStory is a web-based visual novel engine built with React and TypeScript. It renders interactive stories from a JSON script file — think of it like a screenplay format for games.

As a user (story creator), you only need to:

  • Write a .json file describing your story
  • Add your images to public/assets/
  • Add your music to public/audio/
  • Run one command to play it

As a player, they get a classic visual novel experience: background images, character portraits, dialogue, choices, and music.

Getting Started (Running Locally)

Prerequisites

You need Node.js installed on your machine. Download it at nodejs.org. The installer also includes npm (the package manager), so you only need to install Node.js.

Not sure if you have it? Open a terminal and run node -v. If you see a version number, you're good.

Steps

1. Get the project

If you have Git:

git clone https://github.com/NamelessProj/NamelessStory.git
cd NamelessStory

Or download the ZIP from GitHub and unzip it, then open a terminal in that folder.

2. Install dependencies

npm install

This downloads all the libraries the engine needs. It only needs to be done once.

3. Start the development server

npm run dev

Open your browser and go to http://localhost:5173. You'll see the sample story running.

Stop the server with Ctrl + C in the terminal.

Project Structure

Here is what matters to you as a story creator:

NamelessStory/
│
├── public/                   ← Your content goes here
│   ├── assets/               ← Background images & character sprites
│   ├── audio/                ← Background music files
│   └── story/                ← Your story JSON files
│       ├── story.sample.json ← Start here as a template
│       └── tests/            ← Validation test files (for contributors)
│           ├── invalid_syntax.json
│           ├── missing_field.json
│           └── wrong_type.json
│
└── src/
    └── App.tsx               ← Point this to your story file

You should not need to touch anything else to create a story. The engine code is in src/ and handles everything else automatically.

Writing Your Story

The Story File

Your story lives in a single JSON file inside public/story/. Start by copying story.sample.json and renaming it (e.g., my_story.json).

Important

DO NOT put spaces in the name of your file! This could lead to unexpected errors. Here's what you could use instead of spaces: ., -, _.

Note

When the engine loads your story, it automatically validates the entire file before anything runs. If there are structural errors — missing required fields, wrong value types, invalid enum values, or next targets pointing to scenes that don't exist — the engine will display a clear list of every problem instead of silently breaking mid-game. Fix the reported errors and reload.

A story file has three top-level sections:

{
  "settings": { ... },
  "characters": { ... },
  "story": { ... }
}

Settings

The settings block controls global options, the title screen, and the credits screen.

"settings": {
  "startingScene": "start",
  "textSpeed": 50,
  "defaultNameDisplay": "short",

  "titlePage": {
    "title": "My Visual Novel",
    "background": "title_bg.png",
    "logo": "my_logo.png",
    "showTitle": false,
    "buttons": {
      "start": "Start Game",
      "continue": "Continue",
      "load": "Load Save File",
      "credits": "Credits"
    }
  },

  "creditsPage": {
    "title": "Credits",
    "background": "title_bg.png",
    "scrollSpeedInPixelsPerSecond": 100,
    "creditGroups": [
      {
        "groupName": "Story & Writing",
        "credits": [
          { "name": "Your Name", "role": "Writer" }
        ]
      }
    ]
  }
}
Field Required Description
startingScene ✔️ (Yes) The ID of the first scene to play
textSpeed ❌ (No) Typing animation speed. Lower = faster. Default: 50
defaultNameDisplay ❌ (No) "short" (default) or "full" — which character name to show
defaultDialoguePosition ❌ (No) "bottom" (default), "top", or "center" — where the dialogue box appears on screen
defaultSceneTransition ❌ (No) Default transition when entering a new scene. Default: "none" (see Transitions)
defaultDialogueTransition ❌ (No) Default transition between dialogues within the same scene. Default: "none"
transitionDuration ❌ (No) Total duration of any transition in milliseconds. Default: 400
titlePage.title ✔️ (Yes) Your game's title
titlePage.background ✔️ (Yes) Background image filename (from public/assets/)
titlePage.logo ❌ (No) Logo/icon image filename (from public/assets/). Displayed above the buttons instead of (or alongside) the title text
titlePage.showTitle ❌ (No) When a logo is set, controls whether the title text is also shown beneath it. Defaults to true — set to false to hide the title when a logo is present
titlePage.buttons ❌ (No) Custom button labels. You can custom just 1 or 2 if you want also.
creditsPage.scrollSpeedInPixelsPerSecond ❌ (No) How fast the credits scroll in pixels per second. The engine measures the rendered content and calculates the duration automatically. Default: 100. Higher values scroll faster.

Logo behaviour

When logo is set, the image is displayed above the buttons in place of the plain text title. The showTitle field then controls whether the <h1> title is also rendered beneath the logo:

logo showTitle Result
not set Title text is shown (default behaviour)
set omitted or true Logo is shown, title text is also shown beneath it
set false Logo is shown, title text is hidden

CSS variables (title page)

The title page logo and buttons can be restyled from public/custom.css without touching any source file:

Variable Default Description
--title-logo-max-width 400px Maximum width of the logo image
--title-logo-max-height 200px Maximum height of the logo image
--title-text-color #fff Title text color
--title-text-font-size clamp(2rem, 5vw, 4rem) Title text size (scales with viewport)
--title-text-font-weight 700 Title text weight
--title-text-letter-spacing 0.04em Title letter spacing
--title-text-shadow 0 2px 8px rgba(0,0,0,0.7), 0 0 40px rgba(0,0,0,0.4) Title drop shadow
--title-btn-bg rgba(100, 108, 255, 0.25) Button background
--title-btn-bg-hover rgba(100, 108, 255, 0.5) Button background on hover
--title-btn-border 2px solid rgba(100, 108, 255, 0.5) Button border (shorthand)
--title-btn-border-color-hover rgba(100, 108, 255, 0.9) Border color on hover
--title-btn-border-radius 6px Corner rounding
--title-btn-color #fff Text color
--title-btn-font-size 1.1rem Text size
--title-btn-padding 0.6rem 2.5rem Inner padding
--title-btn-min-width 200px Minimum button width
--title-btn-gap 0.75rem Space between buttons

Characters

Define all your speaking characters in the characters block. Each character gets a unique ID (you choose the name), a display name, a color, and optionally sprite images.

"characters": {
  "Alice": {
    "name": "Alice",
    "fullName": "Alice Smith",
    "color": "red",
    "sprite": {
      "idle": "alice_idle.png",
      "happy": "alice_happy.png",
      "wave": "alice_wave.png"
    }
  },
  "Bob": {
    "name": "Bob",
    "color": "#4488ff"
  },
  "playerName": {
    "name": "",
    "color": "green"
  }
}
Field Required Description
name ✔️ (Yes) Short display name shown in the dialogue box
fullName ❌ (No) Long name (used when nameDisplay is "full")
color ✔️ (Yes) Name text color — any CSS color (red, #FF0000, rgb(255, 0, 0))
sprite ❌ (No) An object mapping variant names to image filenames

The playerName variable: To store the player's name, create a character with an empty name field (like above). Use a meaningful ID like "playerName" or "hero". You can then prompt the player to type their name and use it in dialogue later. The variable system will be explained further down (see Player Text Input).

Scenes and Dialogues

The story block contains your scenes, each with a list of dialogues that play in order.

"story": {
  "intro": {
    "background": "forest.png",
    "bgmFile": "music_01.mp3",
    "dialogues": [
      {
        "name": "",
        "text": "It was a quiet evening in the forest."
      },
      {
        "name": "Alice",
        "text": "Hello? Is anyone there?"
      },
      {
        "name": "Alice",
        "text": "I could have sworn I heard something...",
        "next": "scene02"
      }
    ]
  },
  "scene02": { ... }
}

Scene fields:

Field Required Description
background ✔️ (Yes) Background image filename
bgmFile ❌ (No) Music file or music command (see Background Music)
bgmLoop ❌ (No) Whether music loops. Default: true
transition ❌ (No) Transition played when entering this scene (see Transitions)
dialogues ✔️ (Yes) Array of dialogue objects

Dialogue fields:

Field Required Description
text ✔️ (Yes) The dialogue text
name ✔️ (Yes) Character ID whose name is shown. Use "" for narration
next ❌ (No) Where to go after this line (only used when reaching the last dialogue of a scene) (see Navigating Between Scenes)
textSpeed ❌ (No) Override typing speed for this line only
nameDisplay ❌ (No) "short" or "full" — override for this line only
background ❌ (No) Change the background image mid-scene
options ❌ (No) Multiple-choice options (see Player Choices)
input ❌ (No) Prompt the player to type something (see Player Text Input)
sprite ❌ (No) Show a character sprite (see Character Sprites)
dialoguePosition ❌ (No) "bottom", "top", or "center" — override the dialogue box position for this line only (see Dialogue Position)
transition ❌ (No) Transition played when entering this dialogue line (see Transitions)

Player Choices (Options)

Add a options array to a dialogue to give the player a choice. Each option has a label and a destination.

{
  "name": "Alice",
  "text": "What do you want to do?",
  "options": [
    { "text": "Go left",  "next": "path_left" },
    { "text": "Go right", "next": "path_right" },
    { "text": "Stay here", "next": "2" }
  ]
}

Option next values:

Value Effect
"scene_id" Jump to the start of another scene
"scene_id:3" Jump to a specific dialogue index in another scene
"2" Jump to dialogue index 2 in the current scene
"" Continue to the next dialogue in the current scene
"__end__" End the story and show credits

Note

Options and input cannot be used in the same dialogue.

Player Text Input

Use input to prompt the player to type something (like their name). The value is stored and can be used later in text.

{
  "name": "Alice",
  "text": "What's your name?",
  "input": {
    "value": "playerName"
  }
}
Field Required Description
value ✔️ (Yes) The character ID where the typed text will be stored. Must exist in characters with an empty name.
color ❌ (No) Hex color applied to the stored name wherever it appears in dialogue.
placeholder ❌ (No) Hint text shown inside the input box before the player types anything. Defaults to "Type your response here...".

The player cannot proceed without typing something.

To use a custom placeholder:

"input": {
  "value": "playerName",
  "placeholder": "Enter your name..."
}

Character Sprites

Use the sprite field in a dialogue to display a character portrait.

{
  "name": "Alice",
  "text": "Look at this!",
  "sprite": {
    "name": "wave",
    "position": "right",
    "mirror": false
  }
}
Field Required Description
name ✔️ (Yes) Sprite variant name (must exist in the character's sprite object)
position ❌ (No) "left", "center", or "right" (default: center)
mirror ❌ (No) Flip the sprite horizontally. Default: false

Custom position: Instead of a preset string, position can be an object to fine-tune exactly where the sprite appears:

"position": { "x": 100, "y": 600 }
Field Description
x Horizontal offset in pixels from the center of the screen. Positive values move the sprite right, negative values move it left. 0 is perfectly centered.
y Height of the sprite in pixels. Controls how tall the sprite is rendered. Omit to use the image's natural size.

Both fields are optional — you can use only x, only y, or both together:

{ "name": "Alice", "text": "I'm slightly to the right.", "sprite": { "name": "idle", "position": { "x": 200 } } }
{ "name": "Bob",   "text": "I'm tall and centered.",     "sprite": { "name": "idle", "position": { "y": 700 } } }
{ "name": "Alice", "text": "Fine-tuned.",                "sprite": { "name": "happy", "position": { "x": -150, "y": 600 } } }

Tip

Use the preset strings ("left", "center", "right") for most cases. Reach for x / y only when you need a precise placement — for example, two characters standing side by side at custom positions.

Sprites persist within a scene until you specify a different one (or the scene changes). To hide a sprite, advance to a dialogue without a sprite field or change scenes.

Portrait in dialogue box

Set inDialogueBox: true on a sprite to display a square portrait of the character inside the dialogue bar instead of as a full-screen sprite. Use position to choose which side of the dialogue bar it appears on.

{
  "name": "Alice",
  "text": "Nice to meet you!",
  "sprite": {
    "name": "happy",
    "inDialogueBox": true,
    "position": "left"
  }
}
Field Required Description
inDialogueBox ✔️ (Yes) Set to true to show the portrait inside the dialogue bar
position ❌ (No) "left" or "right" — which side of the bar the portrait appears on (default: "left")
mirror ❌ (No) Flip the portrait horizontally. Default: false

Note

When inDialogueBox is true, the full-screen sprite is hidden. Only the portrait box inside the dialogue bar is shown.

The portrait box is always square and crops the image to fill it (object-fit: cover, focused on the top-centre of the image by default — ideal for face-focused sprites). You can customise the portrait's appearance with CSS variables in custom.css:

Variable Default Description
--vn-portrait-size 130px Width and height of the square portrait box
--vn-portrait-border 2px solid rgba(255,255,255,0.3) Border around the portrait
--vn-portrait-bg rgba(0,0,0,0.4) Background visible behind transparent areas
--vn-portrait-gap 1.25rem Space between the portrait and the dialogue text
--vn-portrait-outer-padding 0.5rem 2rem 2rem Padding around the portrait + text row
--vn-portrait-object-position top center Focus point for cropping the portrait image

Dialogue Position

By default, the dialogue box sits at the bottom of the screen, but you can place it at the top or in the center — either for the entire story or on a per-line basis.

Global default (settings)

Set defaultDialoguePosition in settings to apply a position to every dialogue that does not override it:

"settings": {
  "defaultDialoguePosition": "bottom"
}

If the field is omitted, "bottom" is used automatically.

Per-dialogue override

Add dialoguePosition directly on any dialogue line to override the global default for that specific line:

{
  "name": "Alice",
  "text": "Look up here!",
  "dialoguePosition": "top"
},
{
  "name": "Narrator",
  "text": "A mysterious voice echoes from the void...",
  "dialoguePosition": "center"
},
{
  "name": "Alice",
  "text": "Back to normal now.",
  "dialoguePosition": "bottom"
}

The three positions

Value Appearance
"bottom" Dialogue strip anchored to the bottom edge, with a gradient fading upward. This is the classic visual novel look.
"top" Dialogue strip anchored to the top edge, with a gradient fading downward.
"center" Dialogue box centered on screen. A semi-transparent dark overlay dims the scene behind it to keep the text readable.

CSS variables (center box)

The center panel has its own CSS variables you can override in public/custom.css:

Variable Default Description
--vn-dialogue-center-bg rgba(0, 0, 0, 0.82) Background of the centered dialogue panel
--vn-dialogue-center-max-width 1000px Maximum width of the centered panel
--vn-dialogue-center-radius 8px Border radius of the centered panel
--vn-dialogue-center-padding 1.25rem 2rem Inner padding of the centered panel
--vn-dialogue-bg-top linear-gradient(to bottom, rgba(0,0,0,0.9) 60%, transparent) Background gradient used when position is "top"

Per-Dialogue Background Override

You can swap the background image on any individual dialogue line without leaving the scene. This is useful for time-of-day changes, weather shifts, or revealing a new part of a location while keeping the same music and scene context.

Add a background field directly on a dialogue line with the filename of the image you want to display:

"library": {
  "background": "bg_library.png",
  "dialogues": [
    {
      "name": "Bob",
      "text": "The library! My favourite place."
    },
    {
      "name": "",
      "text": "The light shifted as dusk crept in.",
      "background": "bg_library_evening.png"
    },
    {
      "name": "Alice",
      "text": "I prefer whispering in here."
    }
  ]
}
  • The first dialogue uses the scene's background (bg_library.png).
  • The second dialogue overrides it with bg_library_evening.png.
  • The third dialogue has no background field, so it keeps showing bg_library_evening.png — the override persists until the scene ends or another override is set.

Note

The override only applies while that dialogue and any following dialogues (without their own override) are shown. When the player moves to a new scene, the scene's own background takes over again.

Tip

Combine this with a "transition": "fade-to-black" on the same dialogue for a smooth background swap:

{
  "name": "",
  "text": "Hours passed...",
  "background": "bg_library_night.png",
  "transition": "fade-to-black"
}

Background Music

Control music with the bgmFile field on a scene.

"bgmFile": "music_01.mp3"

Special values:

Value Effect
"filename.mp3" Play this file (stops any current music)
"continue" Keep whatever music is currently playing
"continue[filename.mp3]" Keep current music if it's already this file, otherwise play it
"reset" Restart the current music from the beginning
"none" Stop music
(omit the field) No music

The player can adjust or mute the volume from the top overlay bar during the game.

Scene and Dialogue Transitions

Transitions are visual effects that play when the story moves from one dialogue to the next, or when jumping to a new scene. You can set a global default in settings and override it per scene or per dialogue.

Global defaults (settings)

"settings": {
  "defaultSceneTransition": "fade",
  "defaultDialogueTransition": "none",
  "transitionDuration": 400
}

transitionDuration is the total duration of the animation in milliseconds (the engine splits it equally into an out-phase and an in-phase). The default is 400 ms.

Per-scene override

Add transition directly on a scene to override the default for that specific scene change:

"forest": {
  "background": "bg_forest.png",
  "transition": "slide-left",
  "dialogues": [...]
}

The transition plays when the player enters that scene.

Per-dialogue override

Add transition on any dialogue line to override the default for that specific step:

{
  "name": "",
  "text": "A long silence fell over the room...",
  "transition": "fade-to-black"
}

The transition plays when the player advances to that line (not when they leave it).

Available transitions

Dialogue transitions (transition on a dialogue line):

Value Effect
"none" No animation — instant change (default)
"fade" The dialogue text fades out, the new text fades in
"fade-to-black" The screen fades to black, then fades back in with the new text
"fade-to-white" The screen fades to white, then fades back in with the new text

Scene transitions (transition on a scene):

Value Effect
"none" No animation — instant change (default)
"fade" The scene fades out, the new scene fades in
"fade-to-black" The screen fades to black, then the new scene fades in
"fade-to-white" The screen fades to white, then the new scene fades in
"slide-left" The new scene slides in from the left (old scene exits to the right)
"slide-right" The new scene slides in from the right (old scene exits to the left)
"slide-top" The new scene slides in from the top (old scene exits downward)
"slide-bottom" The new scene slides in from the bottom (old scene exits upward)

Note

Slide transitions only apply to scene changes. If you set a slide on a dialogue transition field it will have no visible effect — use "fade" or a color fade for dialogue-level animations.

Priority order

When the engine decides which transition to play, it uses the first value found in this order:

  1. The transition field on the destination dialogue (highest priority)
  2. The transition field on the destination scene (when changing scenes)
  3. settings.defaultDialogueTransition or settings.defaultSceneTransition (global fallback)
  4. "none" — if nothing is set anywhere

Disabling transitions for a specific line

Set "transition": "none" on any dialogue or scene to force no animation, even when a default is configured globally:

{
  "name": "Alice",
  "text": "This line appears instantly.",
  "transition": "none"
}

CSS variable

Variable Default Description
--vn-transition-duration 200ms Duration of each half of the transition. Set automatically from transitionDuration in settings — change it there, not here.

Tip

A transitionDuration of 600 gives a cinematic feel; 200 keeps things snappy. The default 400 is a comfortable middle ground.

Text Formatting and Variables

Pauses

Use \. to insert a 1-second pause and \, for a 0.5-second pause while the text types out.

"text": "And then\\.. silence."

The \ acts as the pause marker. Each character after it is a pause type:

  • . → 1 second
  • , → 0.5 seconds

Note

You may have to put 2 backslashes (\\\) back to back, cause on most language, including JSON, the backslash is used as an exit character, as you can see on the example above.

Variables in Text

Use double curly braces {{ }} to insert character names or stored values into text dynamically.

Syntax Result
{{Alice}} Alice's name (respects defaultNameDisplay)
{{c!Alice}} Forces short name
{{C!Alice}} Forces full name
{{v!playerName}} The value stored in the playerName variable

Example:

{
  "name": "Alice",
  "text": "So, {{v!playerName}}, are you ready to meet {{C!Bob}}?"
}

If the player entered "Sam", this renders as: "So, Sam, are you ready to meet Bob Johnson?"

You can also use a variable as the speaker name:

{
  "name": "{{v!playerName}}",
  "text": "That's me!"
}

Text Markup (Bold, Italic, Colors, and More)

You can style your dialogue text using simple tags. Wrap the text you want to format between an opening tag and a closing tag. If you have ever used a forum, Discord, or a chat app, this will feel familiar.

Basic formatting
What you write What it does
[b]text[/b] Bold
[i]text[/i] Italic
[u]text[/u] Underline
[s]text[/s] Strikethrough
{
  "name": "Alice",
  "text": "This is [b]very important[/b], and [i]please[/i] don't forget it."
}
{
  "name": "",
  "text": "The [s]old world[/s] was gone. [b][i]Everything had changed.[/i][/b]"
}
Colors

Use [color=...]...[/color] to change the color of a word or phrase. Three formats are supported. Use whichever is easiest for you:

Format Example When to use
Color name [color=red]text[/color] Quick, simple colors
Hex code [color=#ff6600]text[/color] Exact brand or palette colors
RGB value [color=rgb(255, 100, 0)]text[/color] When you have RGB values from a color picker
{
  "name": "",
  "text": "The sky turned [color=red]crimson[/color] as the sun disappeared."
}
{
  "name": "Alice",
  "text": "I felt [color=#4488ff]calm[/color], yet [color=rgb(220, 50, 50)]afraid[/color]."
}

Tip

You can find hex and RGB values using any color picker tool, there are plenty of free ones online.

Line breaks

[br] inserts a line break inside a dialogue box. This is useful for poems, letters, or any text where you want to control where lines start.

{
  "name": "",
  "text": "Dear Alice,[br]I hope this letter finds you well.[br][br]Yours,[br]Bob"
}

Two [br] tags in a row create an empty line between paragraphs.

Custom styled blocks

If you know a little CSS, you can wrap text in a [class=...] tag to apply your own style. First, add the class to public/custom.css, then use it in your text.

In your story JSON:

{
  "name": "",
  "text": "She [class=highlight]screamed[/class] into the void."
}

In public/custom.css:

.highlight {
  background-color: yellow;
  color: black;
  padding: 0 4px;
  border-radius: 3px;
}

You can also assign multiple CSS classes by separating them with a space:

"text": "[class=big red-text]DANGER[/class]"

In public/custom.css:

.big {
  font-size: 1.5em;
}
.red-text {
  color: red;
}

Note

Class names may only contain letters, numbers, hyphens (-), underscores (_), and spaces (for multiple classes). Anything else will be ignored for safety.

Links

Since NamelessStory runs in a browser, you can make any word or phrase into a clickable link that opens a website. The link opens in a new tab and does not interrupt the game.

{
  "name": "",
  "text": "You can find the map on [link href=\"https://example.com/map\"]this page[/link]."
}

Note

Only http:// and https:// links are allowed. This prevents unsafe links from being embedded in story files.

Warning

When you put a link, make sure it's in between ". If you use ' instead, it will not work and the text will show the raw tag instead of a link.

Combining tags

Tags can be nested inside each other to combine effects. Close tags in the reverse order you opened them (innermost first):

{
  "name": "",
  "text": "[b][color=red]WARNING:[/color][/b] [i]Do not open the door.[/i]"
}
{
  "name": "Alice",
  "text": "I thought it was [b][i]impossible[/i][/b], yet here we are."
}

Tags also work alongside variables — you can color a character's name reference, for example:

{
  "name": "",
  "text": "[i]And so, [b]{{v!playerName}}[/b] set off on their journey...[/i]"
}
Quick reference
Tag Usage Notes
[b]...[/b] Bold text
[i]...[/i] Italic text
[u]...[/u] Underlined text
[s]...[/s] Strikethrough text
[color=VALUE]...[/color] Colored text Accepts name, #hex, rgb(…)
[class=NAME]...[/class] Span with a CSS class Letters, numbers, -, _, spaces only
[link href="URL"]...[/link] Clickable link (opens new tab) https:// or http:// only
[br] Line break Self-closing, no [/br] needed

Navigating Between Scenes

Use the next field on the last dialogue of a scene to jump to another scene. If you don't include next on the last dialogue, the story ends and credits roll.

{ "name": "Alice", "text": "See you later!", "next": "scene02" }

next values for dialogue:

Value Effect
"scene_id" Jump to the start of that scene
"scene_id:3" Jump to dialogue index 3 in that scene
"__end__" End the story and show credits
(omit) Go to the next dialogue. If it's the last one, end the story

Where to Put Your Files

public/
├── assets/        ← Images (backgrounds, character sprites)
│   ├── title_bg.png
│   ├── bg_forest.png
│   ├── alice_idle.png
│   └── ...
│
├── audio/         ← Music files
│   ├── music_01.mp3
│   ├── ambient.mp3
│   └── ...
│
└── story/         ← Story JSON files
    ├── story.sample.json
    └── my_story.json

In your JSON, always reference files by filename only (no path needed):

  • "background": "bg_forest.png" — not "assets/bg_forest.png"
  • "bgmFile": "music_01.mp3" — not "audio/music_01.mp3"

Supported formats:

  • Images: PNG, JPG, GIF, WebP, SVG
  • Audio: MP3 (recommended), WAV, OGG, M4A

Connecting Your Story to the App

Once your story JSON is ready, open src/App.tsx and update the scriptFile prop to point to your file (without the .json extension):

// src/App.tsx
<VNPlayer scriptFile="my_story" />

If your file is public/story/my_story.json, then scriptFile="my_story".

What You Can Customize

Without touching code (JSON only)

  • Story content, dialogue, branching paths
  • Character names, colors, sprite variants
  • Background images and music per scene
  • Title screen title, background, and button labels
  • Credits content and scroll speed
  • Text typing speed (global, per scene, per dialogue)
  • Player name collection and use throughout the story

By setting CSS variables (easiest)

Every visual property is exposed as a CSS custom property. Create a file at public/custom.css — it is loaded automatically — and set any variables you want to override inside :root. You do not need to touch any source file.

/* public/custom.css */
:root {
    --vn-font-family: 'Georgia', serif;           /* custom font for the whole engine */
    --vn-dialogue-bg: rgba(20, 5, 40, 0.9);
    --vn-dialogue-border: 2px solid rgba(180, 100, 255, 0.4);
    --vn-text-color: #f0d0ff;
    --vn-option-bg: linear-gradient(135deg, rgba(140, 60, 200, 0.4), rgba(100, 20, 160, 0.2));
}

The full list of available variables is documented inside public/custom.css. Transition speed is controlled by transitionDuration in your story settings (see Transitions).

By editing the module CSS files (full control)

Each component has its own style.module.css file with scoped styles. Editing these files gives you complete control over every visual detail without breaking functionality in other components (scoped styles cannot affect anything outside their own component).

File Controls
src/components/VisualNovelComponents/Dialogue/style.module.css Dialogue box, name label, text
src/components/VisualNovelComponents/VNTopOverlay/style.module.css Top bar, fade-in/out transition
src/components/VisualNovelComponents/VNBottomOverlay/style.module.css Bottom bar, overlay buttons
src/components/VisualNovelComponents/UserOption/style.module.css Choice buttons
src/components/VisualNovelComponents/UserInput/style.module.css Text input box
src/components/VisualNovelComponents/CharacterFullSprite/style.module.css Character sprite positioning
src/components/CreditsComponents/CreditsPage/style.module.css Credits scroll animation
src/components/VolumeSlider/style.module.css Volume control widget

By editing React components (advanced)

The engine is fully open source. Developers can extend or change any behavior by modifying the components in src/components/ or utilities in src/utils/.

Core components to know:

  • src/components/VNPlayer/ — loads the story and routes between pages
  • src/components/VisualNovelComponents/VisualNovel/ — the main game loop
  • src/components/VisualNovelComponents/Scene/ — renders the current dialogue
  • src/utils/typewriterUtils.ts — text animation and variable substitution logic

Saving and Loading

The engine has a built-in save system accessible during gameplay:

  • Auto-save to browser: Progress is saved to a browser cookie automatically. When the player reopens the game, they can click "Continue" to pick up where they left off.
  • Save to file: The bottom bar has a "Save" button that downloads a .json save file to the player's computer.
  • Load from file: On the title screen, the "Load Save File" button lets the player load a previously saved .json file.

Deploying Online for Free (GitHub + Vercel)

You can share your visual novel online for free using GitHub (to store your code) and Vercel (to host the website). No server needed.

Step 1 — Put your project on GitHub

  1. Create a free account at github.com if you don't have one.
  2. Create a new repository (click the + button → "New repository"). Give it a name, make it Public, and click "Create repository".
  3. Follow GitHub's instructions to push your local project. If you cloned NamelessStory, you'll want to change the remote origin to your own repo:
    git remote set-url origin https://github.com/YOUR_USERNAME/YOUR_REPO.git
    git add .
    git commit -m "My visual novel"
    git push -u origin main

Note

If you're not comfortable with Git, you can also drag and drop your files directly onto GitHub's web interface.

Step 2 — Deploy with Vercel

  1. Go to vercel.com and sign up (you can use your GitHub account).
  2. Click "Add New Project""Import Git Repository".
  3. Select your GitHub repository from the list.
  4. Vercel will automatically detect it's a Vite project. The default settings are correct — just click "Deploy".
  5. After a minute or two, Vercel gives you a public URL like https://my-story.vercel.app.

Step 3 — Update your story (auto-deploy)

Every time you push a new commit to GitHub, Vercel automatically rebuilds and redeploys your site. No manual steps needed.

Building manually

If you ever want to build the site yourself (e.g., to deploy elsewhere):

npm run build

This creates a dist/ folder with all the static files. Upload that folder to any static hosting service (GitHub Pages, Netlify, Cloudflare Pages, etc.).

Schema Validation Test Files

The public/story/tests/ folder contains three JSON files designed to verify that the engine's story parser and Zod schema validation work correctly. Each file deliberately breaks the schema in a different way, so you can confirm that the engine catches every class of error and reports it clearly.

Note

These files are intended for contributors and curious users who want to explore how the validation system works. Story creators do not need them.

Important

The tests/ folder is automatically removed from production builds. When you run npm run build, the engine deletes dist/story/tests/ after the bundle is written, so the files never reach your deployed site. You do not need to do anything, just leave the folder where it is.

How the validation works

When the engine loads a story file, it runs two checks in sequence:

  1. JSON parsing: the raw text must be valid JSON. If even one character is wrong (a missing comma, an unquoted key, a mismatched bracket), the engine stops immediately and reports a syntax error before doing anything else.
  2. Schema validation (Zod): once the JSON is parsed successfully, the engine checks every field against the story schema: required fields must be present, values must have the correct type, enums must use one of the allowed strings, arrays must not be empty, and all next targets must point to scenes that actually exist in the file. Every violation is collected and reported together, so you can fix them all at once.

The three test files

tests/invalid_syntax.json

This file contains intentional JSON syntax errors. The JSON parser will refuse to read it at all, so Zod validation never runs.

Errors included:

  • Missing comma between two top-level fields
  • Unquoted object key (textSpeed instead of "textSpeed")
  • Trailing comma inside an array
  • Missing colon after a character ID
  • Bare unquoted string used as a value (__end__ instead of "__end__")
  • Missing closing } for the root object

Expected engine behaviour: An immediate JSON parse error is shown. No story fields are validated.

tests/missing_field.json

This file is valid JSON, but it is missing several required fields that the schema demands.

Fields intentionally omitted:

  • settings.creditsPage — the entire credits page block is absent
  • settings.titlePage.background — required image filename
  • characters.alice.name and characters.alice.color — both required on every character
  • dialogues[0].text — required on every dialogue line
  • options[0].next — required on every choice option
  • options[1].text — required on every choice option
  • story.intro.background — required on every scene

Expected engine behaviour: The file parses as JSON without error, then Zod reports a list of every missing required field.

tests/wrong_type.json

This file is valid JSON with all fields present, but many values have the wrong type or an invalid value for their field.

Type violations included:

Field Expected Provided
settings.startingScene string 42 (number)
settings.textSpeed positive number "fast" (string)
settings.historyLimit positive integer 10.7 (float)
settings.defaultNameDisplay "short" or "full" "nickname" (invalid enum)
settings.defaultDialoguePosition "bottom", "top", or "center" "left" (invalid enum)
settings.transitionDuration non-negative number -500 (negative)
settings.titlePage.title non-empty string true (boolean)
settings.titlePage.background non-empty string "" (empty string)
settings.titlePage.showTitle boolean "yes" (string)
settings.creditsPage.scrollSpeedInPixelsPerSecond positive number -20 (negative)
settings.creditsPage.creditGroups non-empty array [] (empty array)
characters.alice.name string 123 (number)
characters.alice.color string false (boolean)
characters.alice.sprite object (key → filename) [...] (array)
story.intro.background string [...] (array)
story.intro.bgmLoop boolean "true" (string)
story.intro.transition valid transition enum "zoom" (invalid enum)
story.intro.dialogues array {...} (object)
dialogues[0].text string 9999 (number)
dialogues[0].textSpeed positive number -10 (negative)
dialogues[0].dialoguePosition "bottom", "top", or "center" "middle" (invalid enum)
options[0].text non-empty string "" (empty string)
options[0].next string true (boolean)
sprite.name non-empty string "" (empty string)
sprite.position string or {x, y} object 42 (number)
sprite.inDialogueBox boolean "no" (string)
input.value string 0 (number)

Expected engine behaviour: The file parses as JSON without error, then Zod reports every type violation in a single list.

How to try them yourself

  1. Open src/App.tsx and temporarily change the scriptFile prop to point to one of the test files:

    // src/App.tsx
    <VNPlayer scriptFile="tests/invalid_syntax" />
    // or
    <VNPlayer scriptFile="tests/missing_field" />
    // or
    <VNPlayer scriptFile="tests/wrong_type" />
  2. Start the development server if it is not already running:

    npm run dev
  3. Open http://localhost:5173 in your browser. Instead of the title screen, the engine will display the validation errors it found.

  4. When you are done, restore scriptFile to your actual story file.

Tip

Try fixing one of the errors in a test file, save it, and reload, the engine will re-validate and the fixed error will disappear from the list. This is a good way to understand exactly what each error message means.

Contributing

Contributions are welcome! If you find a bug, have a feature idea, or want to improve the documentation:

  1. Fork the repository on GitHub
  2. Create a new branch: git checkout -b my-feature
  3. Make your changes and commit them
  4. Open a Pull Request describing what you changed and why

Please keep the spirit of the project in mind: accessibility first. Changes should make it easier for non-programmers to create visual novels, not harder.

Disclaimer

NamelessStory is provided as is, without any warranty of any kind, express or implied. By using, modifying, or distributing this software, you agree to the following:

Third-party content. NamelessStory is a tool for creating stories. The authors of NamelessStory are not responsible for any content created by users of this engine, including but not limited to story scripts, images, audio, or any other assets. Story creators are solely responsible for ensuring their content complies with applicable laws and does not infringe on third-party rights.

Modified versions. Because this project is open source, anyone can fork it, modify it, and distribute their own version. The original authors of NamelessStory have no control over, and accept no liability for, any modified version distributed by third parties. If you download a visual novel built on a fork of this engine, you are doing so at your own risk.

Save files. The engine allows players to load .json save files from their computer. Save files are plain text data — they cannot contain executable code or traditional viruses. However, a save file from an unknown source could contain unexpected values that interact with a modified or poorly secured fork of the engine. Only load save files from sources you trust. The original authors of NamelessStory accept no liability for any issues arising from loading save files created by or obtained from third parties.

No liability. To the maximum extent permitted by applicable law, the authors and contributors of NamelessStory shall not be held liable for any direct, indirect, incidental, special, or consequential damages arising from the use or inability to use this software, even if advised of the possibility of such damages.

This disclaimer is in addition to, and does not replace, the warranty and liability exclusions already present in the MIT License.

Note

It may look scary, but don't worry, basically, don't do stupid thing and download stuff only from trusted sources.

License

NamelessStory is licensed under the MIT License. See the LICENSE file for more information.

About

Want to make a visual novel without coding? NamelessStory is a text-based, open-source engine that lets you focus on your story and characters. ~no programming needed.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Contributors

Languages