How an overlay renders
From the browser source opening a URL to a follower alert animating on screen - the whole path, in order. Worth reading once, and worth having when something is not appearing and you need to know which step to suspect.
Overlabels is not magic, and knowing the order things happen in turns "my overlay is blank" from a mystery into a question with about four possible answers.
Boot: a static overlay
OBS browser source
|
v
/overlay/{slug}#token 1. page loads, token read from the fragment
|
v
Echo / Reverb connection 2. WebSocket opens
|
v
GET /api/overlay/render 3. template + live Twitch data
|
v
tags -> conditionals -> CSS 4. resolve, evaluate, inject
|
v
mounted, subscribed 5. listening on alerts.{twitch_id}
In detail:
- The page loads and reads the token. The fragment is pulled off
window.location.hashand checked for a length of 64. If it is missing or the wrong length, you get the "your overlay link is broken" screen instead of a silent blank page - that message exists because OBS cannot show you a console. - A WebSocket connection opens to Laravel Reverb, authenticated against your token. This happens early and independently, so a render failure and a connection failure are separately diagnosable.
- The renderer fetches your template and your live data in one call, using the token.
- Tag replacement runs. Every
[[[tag]]]is resolved in a single pass - Twitch values, control values, formatting pipes and all. This happens exactly once per render. - Expression controls are registered and evaluated client-side, so derived values cost no round trip.
- Conditional blocks are evaluated and branches that did not match are removed from the output entirely, rather than hidden with CSS.
- Your CSS is injected into the document head as a single
<style id="overlay-style">element. - The rendered HTML is mounted, and the overlay subscribes to
alerts.{your_twitch_id}for everything that happens next.
Tags are parsed once, and resolved values are never re-scanned for tags. This is deliberate: if a
control value containing [[[something]]] were reparsed, anyone who could set a control could inject
into your overlay. A value that looks like a tag renders as text.
Staying live
After the first render, three things update the overlay in place. None of them reload the page.
| Mechanism | What arrives | What happens |
|---|---|---|
| Control updates | control.updated |
The value updates in reactive state; expressions that read it re-evaluate |
| Twitch events | EventSub webhook, relayed | Aggregate tags like followers_total move |
| Alerts | A complete pre-rendered alert payload | Rendered over the static overlay, then auto-dismissed |
Only the affected nodes change. The rest of the overlay is untouched, which is why a counter ticking over does not restart a CSS animation running elsewhere on the page.
Alert render flow
Twitch EventSub webhook / external service webhook
|
v verified (HMAC-SHA256 for Twitch, per-service for the rest)
stored + deduplicated
|
v which template is mapped to this event type, for this user?
merged with current overlay data and rendered server-side
|
v broadcast on alerts.{twitch_id}
overlay checks targeting rules -> renders -> transition -> auto-dismiss
The alert arrives at your overlay as finished HTML. Your browser source is not fetching anything or
deciding anything; it is being handed a rendered payload and asked to display it for duration_ms.
Two consequences worth knowing:
- An alert needs a static overlay to render into. There is no DOM otherwise. If your alerts are not appearing, confirm a static overlay is actually running in that browser source. See Overlays vs Alerts.
- Targeting is checked at the overlay, not at the server. An alert with no targeting configured fires everywhere; one with targeting is ignored by overlays not on its list.
Overlay health
An OBS browser source is a hostile place to run a web page. It gets suspended, the machine sleeps, the network drops mid-stream, and nobody is watching a console. So the overlay defends itself:
- Reconnects with exponential backoff when the WebSocket drops
- Periodic health checks to notice a connection that is technically open but actually dead
- Visible error banners, because a console error in OBS reaches nobody
- Auto-reload as a last resort when recovery fails
The banner styles are defined in the page itself rather than in the Vue app, specifically so an error can be displayed even when the app failed to mount.
Why scripts are stripped
<script>, <iframe>, <embed> and friends are removed from all template content - HTML, CSS, head and
meta fields alike - before it is stored. This is enforced on the server, not just flagged in the editor.
What still works: external stylesheets, web fonts, icon libraries, CDN-hosted CSS, and every animation CSS can express. That covers the overwhelming majority of overlay design.
What does not: inline scripts, third-party embeds, and anything that needs to execute JavaScript inside the overlay.
Overlays are shared and copied between users by design. A template that could run arbitrary JavaScript would be running it with your overlay token in scope, on a machine that is live to an audience. Stripping scripts is what makes "copy this stranger's overlay" a safe thing to do.
If you need real JavaScript - a wheel, a canvas, a custom visualisation - the supported path is to host your own page and add it to OBS as its own browser source, reading your data over the API. Lists in realtime walks through exactly that.
Related
- Overlays vs Alerts - why alerts belong inside a static overlay
- Overlay Access Tokens - the credential used in step 1
- Testing your alerts - firing real events at this pipeline on demand