Docs: Templates and code

Templates and code

Every source has a template, CSS and, optionally, JS. You edit them in the editor's Code tab. The defaults come from the module, and you can rewrite any of them: a runner card that looks nothing like ours, a timer with your own markup, a panel built from scratch on a text source.

Templates

A template is HTML with Liquid tags. {{ }} prints a value, {% %} runs logic:

<div class="run">
  <div class="game">{{ run.game }}</div>
  {% if run.category != "" %}<div class="category">{{ run.category }}</div>{% endif %}
  <div class="estimate">{{ "Overlay.estimate" | t }} {{ run.estimateDisplay }}</div>
</div>

The template redraws whenever the live state changes. Only what changed in the page is touched, so animations and embeds don't restart.

Values are escaped, so a game called <b> shows as text. | raw turns that off for a value you trust.

The template can't add <script>, <style>, <form> or event attributes such as onclick; they are removed. Iframes are kept only for Twitch and YouTube players and vdo.ninja.

Variables

VariableContents
runthe current run: game, category, platform, estimate (ms), estimateDisplay, host, commentators, kind (run, or break, intermission, interview during a block)
upcomingnext runs and blocks: game, category, runners (names), kind, startsAt
runnersthe runners: name, pronouns, number (1 to 4), finish (ms, display, place, or empty)
runnerthe runner of this source, when it has a runner setting
timerdisplay, status (stopped, running, paused, finished), finishes, places
countdowndisplay, ended; empty when no countdown is set
tickerthe ticker messages
musicartist, title; empty when nothing plays
donationamount and total (formatted in the marathon currency)
modesingle, race or coop
marathonname, locale, timezone, currency
layoutthe layout tokens
settingsthis source's settings
blockinside a block: its variables with this use's values
nowthe server time in milliseconds

Some modules add their own: clock in the timer, remaining in the countdown, runs in next runs. The list under the template editor shows what the selected source has and inserts it at the cursor.

A race panel that lists every runner:

{% for runner in runners %}
  <div class="runner">
    <span class="name">{{ runner.name }}</span>
    {% if runner.finish %}<span class="time">{{ runner.finish.display }} · {{ runner.finish.place }}º</span>{% endif %}
  </div>
{% endfor %}

Filters

Besides the standard Liquid filters (upcase, join, size, date...):

FilterExampleResult
duration{{ run.estimate | duration }}1:23:45; "hms" always shows hours, "tenths" adds tenths
money{{ donation.amount | money }}R$ 1.234,56, in the marathon's currency and language
number{{ 12345 | number }}12.345
asset{{ settings.logo | asset }}the address of a library image
t{{ "Overlay.next" | t }}"Up next", in the marathon's overlay language

Animations

Add data-animate to an element and its text animates when it changes: the old text leaves, the new one comes in.

<div class="game" data-animate="flip">{{ run.game }}</div>

Effects: fade, up, down, left, right, flip, explode, implode. The element should hold only text and be a block or inline-block.

Numbers can count instead of swapping. data-countup="money" (or "number") with data-value counts from the old value to the new one in 1.5 s:

<span data-countup="money" data-value="{{ donation.amount }}">{{ donation.total }}</span>

When OBS stops drawing the source (hidden scene, background tab), changes apply at once, so nothing freezes halfway.

CSS

CSS in the CSS tab applies to that source only. Write .game { } and it can't touch a .game in another source or the page around the overlay.

Use the tokens to stay consistent with the layout:

.game {
  font: var(--layout-font-heading);
  color: var(--layout-color-accent);
}

A token named color-accent is var(--layout-color-accent). Block variables are var(--block-<name>). Shared classes and fonts belong in the layout CSS.

CSS with unbalanced braces is rejected, so a typo can't break the rest of the overlay. Overlays need OBS 31 or newer.

JS

JS never runs in the overlay page itself. A source with JS renders inside a sandboxed frame that can't read cookies, the output key or the rest of the overlay, and can't make network requests. The rendered template goes into #root, and your script hears every update:

speedthon.onState((ctx) => {
  const el = document.querySelector("#root .game");
  if (el) el.classList.toggle("long", ctx.run.game.length > 24);
});

speedthon.context always holds the latest state. Use JS for what templates and CSS can't do: measuring text, canvas drawing, effects timed by script. For anything else a template is simpler and faster.