FolioTier 2

text-tags

Inline formatting + animation grammar that lives inside `say` and `narrate` string content.

Reference page. Text tags are inline grammar inside say / narrate string content, not a standalone verb. The parser shipped in Phase 3 Step 4 — see apps/web/src/lib/runtime/effects/text/parser.ts. The per-glyph renderer ({vibrate} / {wave}) shipped in Step 5; the static-tag styling ({b} / {i} / {color} / {size}) shipped in Step 6; and the special timing tags ({glitch} / {slow}) shipped in Step 7 — they drive a per-character reveal schedule at effects/text/reveal-schedule.ts. This page documents the closed grammar so the parser, the importer's deterministic pass, and the AI importer pass all share one target.

How tags work

A tag is delimited by { and }. Static tags wrap a span of text with a matching {/tag}; animated and special tags have the same shape. The grammar is closed — only the tags listed here are recognized. Unknown tags are a parse error at import time and an inline editor warning in the studio, both with the documented "unknown text tag" issue code; the migration report surfaces them under the same code.

say "Emma": "I {b}can't{/b} believe you {i}did{/i} that."

Nesting is allowed; mismatched closers reject at parse time. Tags that take a value use = ({color=#ff0066}…{/color}); tags without a value use the bare form ({b}…{/b}).

Static formatting

These render synchronously — the text appears with the formatting applied, no per-glyph animation. All four are present in the corpus (the-question + Acting Lessons combined: {i} × 317, {b} × 102, {color=} × 59, {size=} × 12).

| Tag | Value | Effect | |---|---|---| | {b}…{/b} | — | Bold weight. Maps to CSS font-weight: 700. | | {i}…{/i} | — | Italic style. Maps to CSS font-style: italic. | | {color=…}…{/color} | #rrggbb / #rgb hex or theme token (e.g. dialogue.accent) | Override the text color for the wrapped span. | | {size=…}…{/size} | Integer point size (8–96) or relative (+2, -4) | Override the font size. Relative values stack with the surrounding context size. |

Out-of-range values reject at parse time per G3.1 (same shared validator as the effects catalog).

I {b}can't{/b} believe you {i}did{/i} that.
I can't believe you did that.

{b} and {i} compose freely with surrounding plain text — same line, two static spans.

The {color=#ff5566}red light{/color} blinks once, then the {size=+6}whole{/size} room goes still.
The red light blinks once, then the whole room goes still.

{color} accepts hex literals or theme-token paths; {size} accepts absolute (8–96) or relative (±24) values.

Animated formatting

These render per-glyph — each character in the wrapped span is its own animated <span>. Per Phase 3 Step 5: soft cap ~80 glyphs per animated tag. Longer spans fall back to whole-block animation to avoid the per-glyph cost.

| Tag | Value | Effect | |---|---|---| | {vibrate}…{/vibrate} | — | Each glyph jitters in place at low amplitude. Use for nervousness, fear, instability. | | {wave}…{/wave} | — | Each glyph sine-waves on the y-axis with a per-glyph phase offset. Use for sing-song, dreaminess, levity. |

Both are Folio-native — not present in the Ren'Py corpus. They land with the per-glyph renderer in Phase 3 Step 5.

She watched {vibrate}everything{/vibrate} unravel.
She watched everything unravel.

{vibrate} jitters each glyph independently — a low-amplitude tremor that reads as nervousness.

They could hear the {wave}broadcast{/wave} from a mile out.
They could hear the broadcast from a mile out.

{wave} sine-waves each glyph on the y-axis with a left-to-right phase offset; the traveling motion is what separates it from {vibrate}.

Special timing

These affect how the line types out, not how individual glyphs look. Same closed-grammar shape; both Folio-native.

| Tag | Value | Effect | |---|---|---| | {glitch}…{/glitch} | — | Wrapped span types with intermittent garbled-character flashes that resolve to the real text. Use for corruption, malfunction, possession. | | {slow}…{/slow} | — | Wrapped span types at half the surrounding speed (or the project's text.slow-cps token, when set). Use for gravity, weariness, deliberation. |

{slow}Rain crawls{/slow} down the station glass.
Rain crawls down the station glass.

{slow} halves the typewriter speed inside the wrapped span. Click replay to watch the opening drag before the rest of the line snaps back to cadence.

The red {glitch}ON AIR{/glitch} light warms the room.
The red ON AIR light warms the room.

{glitch} garbles each non-whitespace char for ~150ms before settling on the real character. Whitespace is exempt so word-wrap boundaries stay clean.

What's deliberately out of v1

The corpus surfaces additional Ren'Py text tags that v1 does not adopt. Each is a deliberate scope call, not an oversight:

| Tag | Corpus presence | Why out of v1 | |---|---|---| | {w=…} / {w} | Heavy in Tier 3+ (297 + 57 in Eternum + What a Legend) | Pacing knob authors typically over-use. Phase 3's {slow} covers the narrative-pacing case; explicit per-line waits flow through the manual lane until a v1.x decision lands. | | {nw} | Heavy in Tier 3+ (252 hits) | "No wait — auto-advance" is a runtime mode, not a per-line tag in Folio. Authors set auto-advance on the player; the importer notes {nw} lines for re-checking after import. | | {cps=…} | Heavy in Tier 3+ (249 hits) | Project-wide typing speed is a theme token (text.cps), not an inline tag. The importer lifts {cps=N} blocks to per-line {slow} when the value is half the project default; otherwise the manual lane. | | {font=…} | 145 hits in What a Legend | Font swaps are a theme concern — namebox / dialogue / choice already get distinct font tokens. Inline font swap is a power tool that opens the typography surface wider than v1 wants. | | {image=…} | 321 hits in What a Legend | Inline emoji / icon insertion is a sandbox-VN affordance. Phase 6 Track A's Locations subsystem owns the hotspot/affordance equivalent. | | {a=…} / {outlinecolor=…} / {u} | Sparse | Ren'Py UI screen affordances or styling minutiae that don't belong in narrative content. Migration report surfaces them; manual lane absorbs. |

The closed-grammar discipline is the point. Adding a tag means deciding what authoring problem it solves and what the closed-form spelling is — not "well, Ren'Py has it."

Notes

A future tag lands via the same catalog-growth process documented in docs/folio-effects-catalog.md: matrix evidence → proposal → wiki page → Phase 3 module → importer pattern recognizer.

The importer's deterministic pass lowers the supported tags above verbatim. Anything else — including the Tier 3+ tags listed in the "deliberately out" table — flows through the migration report's manual lane with a stable per-tag issue code.

See also

  • say — dialogue verb; text-tag content lives inside its quoted string
  • narrate — narration verb; same tag grammar applies
  • @flash — stage-level color punctuation, distinct from inline {color=…} tags