Skip to content

Widgets

Widgets are reusable content blocks defined in your story and invoked by name.

Defining a Widget

Use the {widget} macro to define a widget. The first argument is the widget's name (quoted string):

:: StoryInit
{widget "StatusBar"}
  Health: {$health} | Mana: {$mana}
{/widget}

{widget "Separator"}
  <hr>
{/widget}

Widgets can be defined in two places:

  • StoryInit — the most common location. Widgets defined here are available everywhere.
  • Any passage tagged widget — these passages are automatically processed at startup, so their widgets are also available from the start.
:: MyWidgets [widget]
{widget "StatusBar"}
  Health: {$health} | Mana: {$mana}
{/widget}

{widget "Separator"}
  <hr>
{/widget}

Using [widget]-tagged passages lets you organize widget definitions into dedicated passages and keep StoryInit focused on variable setup and initialization logic.

Using a Widget

Invoke a widget by using its name as a macro:

{StatusBar}

Some passage content...

{Separator}

More content.

Widget names are case-insensitive: {statusbar}, {StatusBar}, and {STATUSBAR} all work.

Arguments

Widgets can accept arguments, making them more flexible. Declare parameter names after the widget name using @ prefixed local variables:

:: StoryInit
{widget "StatLine" @label @value @max}
  **{@label}:** {@value} / {@max}
{/widget}

Pass arguments when invoking the widget. Arguments that are standalone values — quoted strings, variables, numbers, booleans — can be separated by spaces:

:: Start
{StatLine "Health" $health $max_health}
{StatLine "Mana" $mana $max_mana}
{choice "Go to the mines" "mining-bay"}
{Portrait $pcPortrait "PC"}

Comma-separated arguments also work and are required when arguments contain operators or other expressions:

{StatLine "Damage", $strength * 2, 100}

Parameters are block-scoped to the widget body using the @ namespace — they never conflict with $ story variables or _ temporary variables. If fewer arguments are passed than parameters declared, the extra parameters are undefined.

Block Widgets (Wrapping Content)

Widgets can wrap body content using the special {@children} placeholder. When {@children} appears in a widget's body, the widget becomes a block widget — it must be invoked with a closing tag, and the content between the tags replaces {@children}.

Defining a Block Widget

Use {@children} to mark where the wrapped content renders:

:: UIWidgets [widget]
{widget "Card" @title}
  <div class="card">
    <h2>{@title}</h2>
    {@children}
  </div>
{/widget}

{widget "Alert" @type}
  <div class="alert alert-{@type}">
    {@children}
  </div>
{/widget}

Using a Block Widget

Invoke with a closing tag — the content between the tags becomes {@children}:

:: Start
{Card "Character Stats"}
  {StatLine "Health", $health, $max_health}
  {StatLine "Mana", $mana, $max_mana}
{/Card}

{Alert "warning"}
  You are running low on health!
{/Alert}

Multiple Slots

If {@children} appears more than once in a widget body, the invocation content renders in each location (mirroring):

{widget "SplitView"}
  <div class="preview">{@children}</div>
  <div class="detail">{@children}</div>
{/widget}

Nesting

Block widgets can be nested inside each other:

{Card "Inventory"}
  {Alert "info"}
    You have {$inventory.length} items.
  {/Alert}
{/Card}

How Detection Works

The presence of {@children} in the widget body is the signal — no extra syntax is needed in the definition header. Widgets without {@children} remain self-closing and work exactly as before.

Notes

  • @children is a reserved name and cannot be used as a widget parameter name.
  • Block widgets must always be invoked with a closing tag (e.g., {Card "x"}{/Card}).
  • Markdown is processed independently in the widget body and invocation children — markdown syntax cannot span across the {@children} boundary.
  • Invocation children inherit the widget's locals context, so they can access widget parameters like {@title}.

How Widgets Work

When you define a widget, its body is stored as an AST (parsed content). When you invoke it, that AST is rendered in place. This means:

  • Widgets see the current values of all variables at the time they are rendered
  • Widgets re-render when variables they reference change
  • Widgets can contain any markup: links, macros, HTML, other widgets
  • Arguments are evaluated at invocation time and scoped to the widget body

Example

A simple widget without arguments:

:: StoryInit
{widget "HealthBar"}
  {if $health > 50}
    {.green print $health}
  {elseif $health > 0}
    {.yellow print $health}
  {else}
    {.red print "DEAD"}
  {/if}
{/widget}

:: Start
HP: {HealthBar}

You stand at the entrance to the dungeon.
[[Enter the dungeon|Dungeon]]

:: Dungeon
HP: {HealthBar}

{set $health = $health - 20}
A trap! You take damage.

A parameterized widget for reusable UI:

:: StoryInit
{widget "ResourceBar" @label @current @maximum}
  <div class="resource-bar">
    <span class="resource-label">{@label}</span>
    {meter @current @maximum}
  </div>
{/widget}

:: Start
{ResourceBar "HP", $health, $max_health}
{ResourceBar "MP", $mana, $max_mana}
{ResourceBar "Stamina", $stamina, $max_stamina}

Released under the Unlicense.