Variables, macros, widgets, links, audio, and controls.
Two ways to tell
one story.
Switch between a scrolling transcript and page-by-page passages without changing your authoring syntax.
Get started
Append passages or replace the current page.
Independent left and right UI bars.
Install Duplex
Twine 2 Desktop
- Extract the package somewhere permanent.
- Open Story Formats → Add a New Format.
- Enter the full file URL to
format.js. - Select Duplex 1.3.1.
file:///home/you/Twine/duplex-format/format.js
Browser Twine may block local file URLs. Use Twine Desktop or host the format online.
Tweego
project/ ├── storyformats/ │ └── duplex-format/ │ └── format.js └── story.twee
tweego --list-formats tweego -f duplex-1.3.1 -o story.html story.twee
StoryData
:: StoryData
{
"ifid": "YOUR-STORY-IFID",
"format": "Duplex",
"format-version": "1.3.1",
"start": "Start"
}Choose the display mode
Append mode
New passages appear beneath earlier passages as a transcript.
:: StoryInit <<set $passageMode = "append">>
Replace mode
Navigation clears the previous passage before showing the next.
:: StoryInit <<set $passageMode = "replace">>
Story JavaScript may set Config.displayMode. This takes priority over story variables.
Config.displayMode = "append"; // or "replace"
Core syntax
[[Passage]]Basic passage link.[[Label->Passage]]Labeled passage link.[[Label|Passage]]Alternate labeled-link form.$nameStory variable; may print naked._nameTemporary passage variable.<<set>>Assign or modify a variable.<<print>> / <<=>>Evaluate and print.<<unset>>Delete variables or properties.<<set $name = "Nest">> <<set $visits to 0>> <<set $visits += 1>> Hello, $name. Visit <<= $visits>>. <<unset $temporaryChoice>>
Operators
is, is not, isnot, eq, neq, gt, gte, lt, lte, and, or, and not, plus ordinary JavaScript operators.
Logic and scripting
If / elseif / else
<<if $keys > 1>> Several keys. <<elseif $keys is 1>> One key. <<else>> No keys. <</if>>
Switch / case / default
<<switch $weather>> <<case "sunny">> The path glows. <<case "rain" "storm">> Water darkens the trail. <<default>> Uncertain skies. <</switch>>
<<run expression>>Execute code without output.<<script>>…<</script>>Execute JavaScript.<<script TwineScript>>Execute translated Twine-style code.<<run $score += 10>>
<<script>>
State.variables.seenBird = true;
output.append("Recorded.");
<</script>>Interactive macros
<<button>>Button that executes its body and may navigate.<<link>>Text link that executes its body and may navigate.<<linkappend>>Single-use link that appends its body.<<linkprepend>>Single-use link that prepends its body.<<linkreplace>>Single-use link replaced by its body.<<checkbox>>Checkbox bound to a story variable.<<radiobutton>>Radio control bound to a story variable.<<cycle>>Link that cycles through <<option>> values.<<listbox>>Select menu using <<option>> or <<optionsfrom>>.<<numberbox>>Numeric input; optional Enter-key passage.<<textbox>>Single-line input; optional Enter-key passage.<<textarea>>Multi-line text input.<<button "Take feather">> <<set $hasFeather = true>> <</button>> <<cycle "$weather" autoselect>> <<option "Clear" "clear">> <<option "Rain" "rain">> <</cycle>> <<textbox "$name" "Nest">>
Cycles and listboxes support <<option>> and <<optionsfrom>>.
Audio macros
<<cacheaudio>>Register a track and its sources.<<audio>>Control tracks or groups.<<createaudiogroup>>Collect tracks under a :group ID.<<createplaylist>>Build a playlist.<<masteraudio>>Control master audio.<<playlist>>Control a playlist.<<removeaudiogroup>>Remove a custom group.<<removeplaylist>>Remove a playlist.<<waitforaudio>>Begin loading registered audio.Setup
<<cacheaudio "forest" "audio/forest.mp3" "audio/forest.ogg">> <<cacheaudio "owl" "audio/owl.mp3">> <<createaudiogroup ":ambience">> <<track "forest">> <<track "owl">> <</createaudiogroup>>
Actions
playpausestoploadunloadmuteunmuteloopunloopvolume leveltime secondsfadeinfadeoutfadeto levelfadeoverto seconds levelgoto passage<<audio "forest" volume 0.5 loop play>> <<audio ":ambience" fadeout>> <<playlist "night" shuffle play>> <<masteraudio volume 0.8>>
Native browser autoplay restrictions apply. Audio playback state is not stored in saves.
UI and sidebars
Left bar
StoryCaptionStoryMenuStoryLeftBar
Right bar
StoryRightBar- Current passage
- Current display mode
UIBarL.stow(); UIBarL.unstow(); UIBarL.hide(); UIBarL.show(); UIBarR.stow(); UIBarR.unstow();
Both bars implement destroy(), hide(), show(), isHidden(), isStowed(), stow(), unstow(), and update().
Aliases: UIBar, UIBarRight, UIBars.left, and UIBars.right.
UI API
UI.alert(message)UI.restart()UI.saves()UI.settings()UI.update()UI.jumpto()UI.share()Duplex includes one lightweight browser save, not SugarCube’s complete multi-slot Save API.
Widgets
Define custom macros in passages with the widget tag. They load before StoryInit.
Ordinary widget
:: Bird Widgets [widget] <<widget "birdname">> The bird is <<= _args[0]>>. <</widget>> <<birdname "Common nighthawk">>
Container widget
:: Bird Widgets [widget] <<widget "birdnote" container>> <aside><<= _contents>></aside> <</widget>> <<birdnote>>Active at dusk.<</birdnote>>
_args[0]First parsed argument._args.rawRaw argument string._args.fullFull argument compatibility value._args.nameInvoked widget name._contentsContainer widget contents.$argsDeprecated compatibility alias.JavaScript API
Duplex.go(title)Navigate to a passage.Duplex.back()Restore the previous history moment.Duplex.restart()Reset and return to the start.Duplex.mode()Return append or replace.Duplex.tags(title?)Return a passage’s tags.Duplex.hasTag(title?, tag)Test one passage tag.Duplex.audioNative audio registry.Duplex.UIDialog and UI API.Duplex.UIBarL / UIBarRSidebar controllers.Duplex.widgetsRegistered widgets.Globals include State, setup, Config, SimpleAudio, UI, UIBarL, and UIBarR.
Reference and limits
Special passages
StoryInitStartup initialization.StoryCaptionLeft-bar caption.StoryMenuLeft-bar menu.StoryLeftBarCustom left content.StoryRightBarCustom right content.Included facilities
- Story JavaScript and Stylesheet
widget-tagged passages- Back and Restart
- Single browser save
- Responsive dual sidebars
- Twine 2 and Tweego packaging
Duplex is not SugarCube or Harlowe. It implements a selected SugarCube-like authoring surface, not every macro, API, save feature, edge case, or third-party extension.
Copyright © 2026 Neston Linden Rainer