Manifest ReferenceSpeech Bubbles

Speech Bubbles

Reference for SpeechBubble, BalloonConfig, TailConfig, HideBorderConfig, and BoundingBox — the 10 balloon types and the styling cascade.

Speech bubbles carry dialogue, thoughts, and narration inside a panel. Each bubble is a SpeechBubble object in a panel's speechBubbles array. Bubbles are positioned with normalized coordinates, hold localized text, and are styled through a four-level balloon configuration cascade rendered by the player's ComicBalloon SVG engine (see Balloon Renderer).

SpeechBubble

Defined as $defs/SpeechBubble. Lives in chapters[].panels.<panelId>.speechBubbles[] (and in variant overrides).

PropertyTypeRequiredDescription
idIdentifierYesUnique bubble ID within the panel.
textLocalizedStringYesBubble text keyed by BCP-47 locale, e.g. { "en-US": "...", "de-DE": "..." }. See Localization.
shapeBoundingBoxYesPosition and size of the bubble in normalized panel coordinates (0–1).
characterIdIdentifierNoSpeaker; references a character in meta.characters. Pulls in that character's balloon overrides.
audioAssetIdIdentifierNoVoice-over audio asset for this bubble (asset catalog ID).
styleRefIdentifierNoSchema 1.3+. Name of a reusable preset in settings.typography.balloonPresets. Unknown names are ignored.
balloonConfigBalloonConfigOverrideNoPer-bubble style overrides, merged last (onto the styleRef preset, character, or work-level defaults).
visibleIfJsonLogicNoStory-logic visibility condition, evaluated against variables. Do not use it to honor the reader's speech preference — that gating is implicit since schema 1.3 (see below).
tailobjectNoEditor-computed balloon tail geometry (tip/anchor points, width). Free-form.
tailStylestringNoEditor tail style preset (e.g. "normal").
textStyleobjectNoEditor text styling (font, color, alignment, padding). Free-form.
dataobjectNoEditor-computed shape geometry (e.g. ellipse cx/cy/rx/ry). Free-form.
styleobjectNoEditor bubble style (font, colors, stroke). Free-form.
lockedbooleanNoEditor state: bubble locked from selection/edits.
visiblebooleanNoEditor state: bubble visibility toggle.

The tail, tailStyle, textStyle, data, style, locked, and visible properties are authoring aids written by the CMS editor. Players should render bubbles from shape + the merged balloon configuration; runtime styling belongs in balloonConfig, not style.

BoundingBox

Axis-aligned bounding box using normalized coordinates (0–1), relative to the panel.

PropertyTypeRequiredDescription
xnumber (0–1)YesLeft edge (0 = left, 1 = right).
ynumber (0–1)YesTop edge (0 = top, 1 = bottom).
wnumber (0–1)YesWidth as a fraction of the panel.
hnumber (0–1)YesHeight as a fraction of the panel.

The coordinates are panel-relative: 0/0 is the panel artwork's top-left corner, 1/1 its bottom-right. Players map the box onto the rendered panel container, so a bubble keeps its position on the artwork regardless of the device format. (The CMS editor serializes bubbles in exactly this space — a caption glued to the panel's top border has y: 0 in every format.)

For display, the box acts as the position anchor: the reference player centers the balloon on the box center, keeps border-flush edges glued to the panel border, and sizes the balloon from its measured text at a per-screen reading scale — lettering stays comfortably readable on every screen instead of scaling with the panel.

BoundingBox sets additionalProperties: false — a bubble shape must contain only x, y, w, h. A type property (as used by the hotspot Shape union) is not valid here.

The implicit speech toggle

Since schema 1.3, every speech bubble is inherently subject to the reader's global speech toggle. The toggle's initial state comes from settings.ui.speechDefault (default: on), and the player ANDs it on top of any visibleIf — a bubble renders only when both the toggle is on and its visibleIf (if any) is truthy.

Do not write visibleIf: { "var": "prefs.speech" } (or similar) on bubbles to honor the speech preference. That pre-1.3 boilerplate is obsolete — and it was a bug waiting to happen: forgetting it on a single bubble made that bubble immune to the toggle. visibleIf is reserved for actual story logic, like sample 09's clue bubbles that appear once { "var": "band.clue.vent" } is set.

The styling cascade

Balloon styling merges four levels, most specific wins:

  1. Work defaults — a complete BalloonConfig at settings.typography.balloon_config (see Settings). Anything unset falls back to the schema defaults below.
  2. Character override — a partial BalloonConfigOverride on the character (meta.characters[].balloonConfig), merged onto the work defaults for every bubble spoken by that character.
  3. Named preset (schema 1.3+) — the BalloonConfigOverride from settings.typography.balloonPresets that the bubble references via styleRef. Lets many bubbles share one editable definition instead of repeating identical overrides.
  4. Per-bubble override — a partial BalloonConfigOverride on the bubble itself, merged last. With a preset in play this is only needed for true one-offs (e.g. a single whisper on top of a shared tail preset).

The merge is field-by-field, and the nested tail and hideBorder objects merge field-by-field too (a character can set tail.curve without losing the work-level tail.length). The player implements this in mergeBalloonConfig.

BalloonConfig

Complete balloon styling configuration, used at the work level as defaults ($defs/BalloonConfig).

PropertyTypeDefaultDescription
balloonTypeenum"normal"Visual type of the balloon shape — see the 10 types below.
cornerRadiusnumber (0–1)0.5Shape roundness (0 = rectangle, 1 = ellipse).
maxWidthnumber (40–800)120Maximum balloon width in pixels (balloon auto-sizes to text).
maxHeightnumber (30–600)80Maximum balloon height in pixels.
fontFamilystring"'Ames Italic', sans-serif"CSS font-family for balloon text.
fontSizenumber (4–200)12Font size in pixels.
strokeWidthnumber (0–20)2Balloon border stroke width in pixels.
strokeColorhex color"#000000"Balloon border stroke color.
fillColorhex color"#ffffff"Balloon fill/background color.
textColorhex color"#000000"Lettering color (1.7+), e.g. amber on-screen text on a dark fill.
tailTailConfig—Tail (pointer from balloon toward the speaker).
hideBorderHideBorderConfig—Border-hiding segment for layered balloon effects.

The 10 balloon types

balloonType accepts exactly these enum values:

TypeRendering
normalStandard balloon; roundness controlled by cornerRadius.
rectangleRectangular balloon (corner radius forced to 0, keeping a subtle 8 px rounding).
narratorTrue sharp-cornered rectangle for narration caption boxes — unlike rectangle, the corners have no rounding at all. Additive extension, mid-1.3.
cutTopBalloon with a cut/flattened top edge.
cutTopRightCut top with the cut biased to the right.
cutTopLeftCut top with the cut biased to the left.
thoughtCloud shape with a bumpy outline; the tail is rendered as a trail of small ellipses.
shoutJagged/burst edges (spiked outline) for shouting.
whisperStandard shape with a dashed border.
connectorOpen-tailed balloon used to connect/chain balloons.

BalloonConfigOverride

Partial styling for character-level or bubble-level overrides ($defs/BalloonConfigOverride). It has the same fields as BalloonConfig, all optional and without defaults — only fields you specify override the parent level. Its nested tail and hideBorder objects are likewise partial:

  • tail override fields: enabled, position, length, curve, curveAmount
  • hideBorder override fields: enabled, angle, arc

TailConfig

The balloon tail — the pointer from the balloon toward the speaker ($defs/TailConfig).

PropertyTypeDefaultDescription
enabledbooleantrueWhether the tail is visible.
positionnumber (0–359)180Tail direction in degrees (compass: 0 = top, 90 = right, 180 = bottom, 270 = left).
lengthnumber (0–500)45Tail length in pixels.
curve"straight" | "left" | "right""straight"Tail curve direction.
curveAmountnumber (0–1)0.4Curve intensity (0 = straight, 1 = maximum curve).

HideBorderConfig

Hides a segment of the balloon border, useful when stacking balloons so they appear joined ($defs/HideBorderConfig).

PropertyTypeDefaultDescription
enabledbooleanfalseWhether border hiding is active.
anglenumber (0–359)0Center angle of the hidden border segment in degrees (compass: 0 = top).
arcnumber (10–180)60Width of the hidden segment in degrees.

Example

A work-level default, a named preset, a character override, and a bubble combining all of them:

{
  "settings": {
    "typography": {
      "balloon_config": {
        "balloonType": "normal",
        "cornerRadius": 0.5,
        "fontFamily": "'Ames Italic', sans-serif",
        "fontSize": 12,
        "strokeWidth": 2,
        "strokeColor": "#000000",
        "fillColor": "#ffffff",
        "tail": { "enabled": true, "position": 180, "length": 45 }
      },
      "balloonPresets": {
        "speech-down-right": { "tail": { "position": 150, "length": 50 } }
      }
    }
  },
  "meta": {
    "id": "work-demo",
    "title": { "en-US": "Demo" },
    "locales": ["en-US", "de-DE"],
    "default_locale": "en-US",
    "characters": [
      {
        "id": "char-villain",
        "name": { "en-US": "The Villain" },
        "balloonConfig": {
          "balloonType": "shout",
          "fillColor": "#ffe0e0",
          "tail": { "curve": "right", "curveAmount": 0.6 }
        }
      }
    ]
  },
  "chapters": [
    {
      "id": "ch-1",
      "panels": {
        "p1": {
          "speechBubbles": [
            {
              "id": "sb-1",
              "characterId": "char-villain",
              "text": {
                "en-US": "You will never escape!",
                "de-DE": "Du wirst niemals entkommen!"
              },
              "shape": { "x": 0.55, "y": 0.1, "w": 0.35, "h": 0.2 },
              "styleRef": "speech-down-right",
              "balloonConfig": { "fontSize": 16 }
            }
          ]
        }
      },
      "graph": { "entry": "p1", "edges": [{ "from": "p1", "to": "p1" }] }
    }
  ],
  "panelwave": {
    "version": "1.3.0",
    "schema": "https://panelwave.org/schema/1.0/panelwave.schema.json"
  }
}

The bubble renders as a shout balloon (character), light red (character), 16 px text (bubble), with the preset's tail direction/length on top of the character's right curve — and it disappears automatically whenever the reader turns speech off. No visibleIf needed for that.