How to Use Controls
Learn how to create, manage, and use Controls in your Twitch overlays. Counters, timers, toggles, and more - all updated live during your stream.
On this page
You can also use Controls in CSS. This opens up possibilities for dynamic styling, which is incredibly powerful.
What are Controls?
A Control is a named value that lives on your overlay or alert template. You define its key, type, and
optional label, and then reference it in your overlay HTML with the [[[c:key]]] syntax. During your
stream, you update its value from the Control Panel and the change appears in OBS within a few
seconds.
Controls are overlay-scoped: each overlay has its own set. They are never shared between overlays unless you explicitly import them when copying.
| Type | What it is |
|---|---|
text |
Free-form text. Displayed as-is in your overlay. |
number |
A numeric value with optional min, max, and step. |
counter |
A whole-number counter with +/-/Reset buttons. Great for wins, rounds played, donations. |
timer |
A stopwatch, countdown, or count-to-date. Ticks in real time on the overlay. |
boolean |
An on/off toggle. Outputs 1 or 0. |
datetime |
A fixed date and time value. |
expression |
A formula that derives its value from other controls. Evaluated live on the overlay. |
list writer |
Records another control's value to a List every time it changes. Works with any control type, including Expressions. |
Control Types in Detail
text
Free-form text displayed as-is in your overlay. HTML is stripped for safety, so you can't accidentally inject markup through a Control Panel update.
Text controls are the most versatile type. Use them for player names, status messages, song titles, or anything that doesn't need numeric logic.
<div>
Now playing: <span>[[[c:song_title]]]</span>
</div>
Text controls also work well for storing URLs (image sources, links, etc.) that you want to swap out without editing overlay code.
number
A numeric value with optional min, max, and step constraints. Saved and displayed as a plain number. You type the value directly in the Control Panel.
Numbers are great for goal amounts, percentages, scores, or any value where you want to set it to a specific number rather than increment/decrement.
<div>
<progress value="[[[c:goal_current]]]" max="[[[c:goal_target]]]"></progress>
[[[c:goal_current]]] / [[[c:goal_target]]]
</div>
Random mode: Number controls can be set to "random mode" in the config. When enabled, the overlay generates a random integer between min and max on a configurable interval. Useful for slot machines, randomized choices, or whack-a-mole style games.
counter
A whole-number counter with +, -, and Reset buttons in the Control Panel. Each press fires immediately - no save button needed. Counters are the fastest way to track things that change during a stream.
Configure a step size (default 1), min/max bounds, and a reset value (default 0).
<div>
Wins: <span>[[[c:wins]]]</span>
</div>
<!-- Combine with conditionals for reactive messaging -->
[[[if:c:wins >= 10]]]
<div>On a tear tonight!</div>
[[[endif]]]
Random mode: Like Number controls, Counters also support random mode for generating random values on an interval.
timer
A live-ticking timer that runs on the overlay in real time. Control it from the Control Panel with Start, Stop, and Reset buttons. Timer controls support three modes:
| Mode | Behaviour |
|---|---|
| Count up | Counts upward from zero. A classic stopwatch. |
| Countdown | Counts down from a base duration you set (in seconds). Stops at zero. |
| Count to | Counts down to a specific date and time. Always ticking - no start/stop needed. |
The raw output is seconds. Use formatting pipes to display it as a clock:
<!-- Shows "02:34:15" -->
<span>[[[c:stream_timer|duration:hh:mm:ss]]]</span>
<!-- Shows "4:15" (minutes and seconds only) -->
<span>[[[c:round_timer|duration:mm:ss]]]</span>
<!-- Auto-format picks the smartest display -->
<span>[[[c:stream_timer|duration]]]</span>
Detecting if a timer is running
Every timer exposes a companion :running value that outputs 1 when the timer is active or 0 when it
is stopped. "Count to" timers are always considered running since they tick continuously.
Use it as a tag or in conditionals to show or hide content based on timer state:
<!-- Show a pulsing dot when the timer is live -->
[[[if:c:stream_timer:running]]]
<span></span> LIVE
[[[else]]]
<span>PAUSED</span>
[[[endif]]]
<!-- Only show the timer block while it's running -->
[[[if:c:round_timer:running]]]
<div>
<span>[[[c:round_timer|duration:mm:ss]]]</span>
</div>
[[[endif]]]
The :running value updates instantly when you press Start or Stop in the Control Panel.
boolean
An on/off toggle switch. Stores 1 (on) or 0 (off). In the Control Panel, it shows as a simple toggle
you can flip instantly.
Booleans are ideal for conditionally showing or hiding entire sections of your overlay without touching the code.
<!-- Toggle a sponsor banner on/off -->
[[[if:c:show_sponsor]]]
<div>
<img src="[[[c:sponsor_logo]]]" />
</div>
[[[endif]]]
<!-- Toggle between two layouts -->
[[[if:c:compact_mode]]]
<div>...</div>
[[[else]]]
<div>...</div>
[[[endif]]]
datetime
A fixed date and time value, set from a datetime picker in the Control Panel. Useful for "next stream starts at" displays, event countdowns, or logging purposes.
<div>
Next stream: <span>[[[c:next_stream|date:short]]]</span>
</div>
Use formatting pipes like |date:short or |date:long to format the output. If you
need a live countdown to a date, use a Timer in "count to" mode instead.
expression
A formula that derives its value from other controls. Expressions are evaluated live on the overlay with zero latency - no server round-trip needed. You cannot edit an expression's value directly; it's always computed from its formula.
Reference other controls using c.key syntax inside the formula. For service-managed controls, use
c.kofi.total_received (dots instead of colons).
// Simple math
Expression: c.wins / (c.wins + c.losses) * 100
<div>Win rate: [[[c:win_rate|round]]]%</div>
// Ternary / if-else logic
Expression: c.wins >= 10 ? "on_fire" : "warming_up"
<div>Mood: [[[c:mood]]]</div>
// Cross-service total
Expression: c.streamlabs.total_received + c.kofi.total_received
<div>Total donations: $[[[c:total_donations|round]]]</div>
// Latest donor across all services
Expression: latest(
c.streamlabs.donations_received_at, c.streamlabs.latest_donor_name,
c.kofi.donations_received_at, c.kofi.latest_donor_name
)
<div>Latest donor: [[[c:latest_donor]]]</div>
// Seconds since last donation
Expression: now() - max(c.kofi.donations_received_at, c.streamlabs.donations_received_at)
<div>Last donation: [[[c:since_last_donation|duration:mm:ss]]] ago</div>
Expressions support standard math operators (+ - * / %), comparisons, ternary operators, and
parentheses. Circular dependencies (A depends on B, B depends on A) are detected and blocked when you
save.
Available functions
latest() oldest() argmax() argmin() - accept pairs of value, label arguments. Return the
label paired with the highest (latest / argmax) or lowest (oldest / argmin) value. Works with
numbers and timestamps. First pair wins on ties.
max() min() sum() avg() abs() round() floor() ceil() - standard math functions.
max, min, sum, and avg accept multiple arguments. round takes an optional decimals count:
round(0.1 + 0.2, 2) returns the string "0.30" (trailing zero preserved, matching the
|round:2 pipe). Because the 2-arg form returns a string, put it at the end of the
expression; further math after it concatenates instead of adding.
sin() cos() fract() mod() PI - animation-friendly helpers. sin and cos take radians.
fract(x) returns the fractional part (x - floor(x), so fract(-0.3) === 0.7). mod(a, b) is
floor-based modulo (GLSL-style), so mod(-1, 5) === 4. Use the % operator if you want JS remainder
(-1 % 5 === -1). PI is a bare identifier - use PI, not PI().
Heads up on float precision: fract(10.2) evaluates to 0.19999999999999993, not 0.2. That's expected
- it's how IEEE 754 doubles work, same in every language (JavaScript, GLSL, Python, C). For animation
math the trailing noise is invisible. For text display, pipe through a formatter like
|round:2to get a clean0.2.
now() - returns the current timestamp in seconds. Useful for calculating time since an event, e.g.
now() - c.kofi.donations_received_at.
list writer
A side-effect control with one job: every time the source control's value changes, append the new value to a target List. Unlike every other control type, a list writer has no value of its own and renders nothing in your overlay - the row exists purely as a binding between a source control and a list.
The source can be any control type: a counter you bump from chat, a service-managed control (Ko-fi donor name, StreamLabs tip amount, Twitch cheer count), or even an Expression Control. Yes, even an Expression Control - the server evaluates the same formula your overlay does, then writes the result to the list. So an expression that combines values from multiple services can have its history persisted automatically.
// Bind a chat-driven counter to a log list
Source: c.wins
Target: wins_log
// Mod types !inc wins 1 - the new value gets appended to wins_log automatically
// Persist the result of an expression that aggregates across services
Expression: latest(
c.kofi.donations_received_at, c.kofi.latest_donor_name,
c.streamlabs.donations_received_at, c.streamlabs.latest_donor_name
)
Source: c.latest_donor (the expression above)
Target: donor_history
Why this exists. Controls hold the current value of something. The moment a counter bumps or a
donation lands, the previous value is gone. A list writer keeps the trail. Once values are in a list,
every existing list action works on them:
[[[c:list:donor_history:count]]], foreach iteration, :last for the most recent, :random for a
shout-out picker, and so on.
Capping. Set max_items on the target list (from the Lists dashboard) for a rolling window. List
writers FIFO-drop the oldest entry when the cap is hit, so "last 10 donors" is just max_items = 10 on
the list. Without a cap, the list grows unbounded.
Curation. The target list is editable from the Lists dashboard exactly like any other List. Delete entries you don't want, rename items, clear the whole thing - the writer keeps feeding new values either way. If a donor you'd rather not advertise lands in the history, just remove the row.
Disabled lists. Disabling a list (from the Lists dashboard) silently skips appends. The writer doesn't error; the value just doesn't land. Re-enable the list to resume.
Preset Controls (from integrations)
Everything above describes controls you create and update by hand. There is a second family of controls that Overlabels creates and updates for you: preset controls. When you connect an integration - Twitch, a donation service, or the Overlabels GPS app - that service can feed live values straight into your overlays.
How they differ from the controls above
- Auto-managed value. You never type their value in the Control Panel. The connected service updates it whenever an event lands - a donation, a cheer, a GPS ping. The Control Panel shows them as read-only.
- Namespaced tag. Reference them with a source-qualified tag:
[[[c:source:key]]], e.g.[[[c:kofi:latest_donor_name]]]or[[[c:gps:speed]]]. The extra segment keeps two services from colliding on the same key. - Shared across every overlay. Unlike the overlay-scoped controls above, preset controls are user-scoped. Add one on any static template and it becomes available in all of your overlays automatically - you don't add it per overlay.
- Only when connected. A service's presets only appear once you've connected that integration. Twitch is the exception - its per-stream counters are available as soon as you sign in.
Adding a preset control
Open a static template, click Add control, and pick a preset from the Stream Controls dropdown at the top of the modal. The key, type, and label are filled in for you - you can override the label. Presets you've already added are hidden from the list so you can't add the same one twice.
Prefer to skip the modal? Every preset tag is copy-to-clipboard on the Integration presets reference - paste the tag straight into your overlay HTML and it resolves the same way.
The shared donation family
Every donation service (Ko-fi, StreamLabs, Fourthwall, Buy Me a Coffee, Throne) exposes the same six-key shape, so you can swap services - or combine them - without relearning the keys:
donations_received // counter, bumps per donation
latest_donor_name
latest_donation_amount
latest_donation_message
latest_donation_currency
total_received // running total this session
Because the keys line up, an Expression like c.kofi.total_received + c.streamlabs.total_received totals
donations across services. Some services add extras on top: Throne carries an item name, thumbnail URL,
and surprise-gift flag; Buy Me a Coffee adds the latest support type.
Available integrations
| Integration | What it feeds |
|---|---|
| Twitch | Per-stream counters (follows, subs, raids, cheers, bits) that reset when you go live. Available the moment you connect Twitch. |
| Ko-fi | Donation, subscription, and shop-sale data from your connected Ko-fi account. |
| StreamLabs | Live donation data delivered through the StreamLabs listener. |
| Fourthwall | Donation and tip data for creators using Fourthwall for merch and supporter tiers. |
| Buy Me a Coffee | Supporter and membership data, including the latest support type. |
| Throne | Gift data plus Throne-only extras: item name, product thumbnail URL, and a surprise-gift flag. |
| Overlabels GPS | Live location data from the Overlabels GPS app: speed, coordinates, distance, battery, and per-session aggregates. |
| Overlabels Alerts | The global alert mute state, no integration required. Show a banner while muted: - it flips live when you hit the mute button on the Events page. |
Managing Controls
Controls live on the Controls tab of your overlay's detail page. You must be the overlay owner to manage them.
Creating a Control
Click Add control in the Controls tab to open the creation modal.
- Key - a lowercase slug used in template tags, e.g.
wins,goal_amount. Must start with a letter and contain only lowercase letters, digits, and underscores. The key is permanent and cannot be changed after creation. - Label - an optional human-readable name displayed in the Control Panel, e.g. "Death Counter". If omitted, the key is used.
- Type - one of: text, number, counter, timer, boolean, datetime, expression.
- Sort order - controls the display order in the Control Panel. Lower numbers appear first.
- Type-specific config - Number and counter controls accept min, max, step, and reset value. Timer controls accept a mode (count up, countdown, or count to date/time). Expression controls require a formula.
Editing a Control
Click the pencil icon on any control row in the Controls tab. You can update the label, sort order, and type-specific configuration. The key and type cannot be changed after creation to protect references already used in your overlay HTML.
Deleting a Control
Click the trash icon on a control row and confirm the prompt. Deletion is permanent. Any [[[c:key]]]
references left in your overlay will render as blank after deletion - no errors, just empty space.
Copying the Snippet
Each row in the Controls table shows a copy button with the ready-to-paste snippet [[[c:key]]]. Click
it to copy the snippet to your clipboard so you can paste it directly into your overlay editor.
Using Controls in Overlays
Once a control exists, reference its current value anywhere in your overlay or alert overlay HTML using
the [[[c:key]]] syntax.
Displaying a Value
Place the tag wherever you want the value to appear. At render time the overlay substitutes the current value.
<div>
Wins: <span>[[[c:wins]]]</span>
</div>
The overlay updates in real time whenever the value changes, no page reload required.
Conditionals with Control Values
Control values participate fully in the conditional engine. Use them exactly as you would any other template variable.
[[[if:c:wins >= 10]]]
<div>On a tear tonight!</div>
[[[elseif:c:wins >= 5]]]
<div>Building momentum.</div>
[[[else]]]
<div>Just getting started.</div>
[[[endif]]]
<!-- Show a goal bar only when goal is set -->
[[[if:c:goal_label]]]
<div>
<span>[[[c:goal_label]]]</span>
<progress value="[[[c:goal_current]]]" max="[[[c:goal_target]]]"></progress>
</div>
[[[endif]]]
String comparison, numeric comparison, boolean truthiness... All operators work the same way as with Twitch data tags. See the Syntax Help page for the full comparison reference.
Controls in CSS
Just like Twitch data tags, control tags can appear inside <style> blocks, which opens up dynamic
styling.
<style>
.goal-fill {
[[[if:c:goal_pct >= 100]]]
background: #22c55e; /* green when complete */
[[[else]]]
background: #3b82f6;
[[[endif]]]
}
</style>
Controls in Alerts
Alerts also support control tags. This lets an alert read the current state of your overlay to decide what to display.
<!-- Alert for a sub that mentions the current death count -->
<div>
[[[event.user_name]]] just subscribed!
[[[if:c:wins > 0]]]
<span>(and yes, [[[c:wins]]] wins so far)</span>
[[[endif]]]
</div>
The Control Panel
The Control Panel is a live dashboard for updating control values during your stream. It lives on the Control Panel tab of your overlay's detail page. Open it in a browser window before going live and keep it on a second monitor or phone.
How each type works
| Type | In the panel |
|---|---|
| Text & Number | Type a new value into the input field and click Save. The overlay updates immediately. Number controls respect the min, max, and step you configured. |
| Counter | Three buttons: − decrements by one step, + increments by one step, and Reset returns the counter to its configured reset value (default 0). Each press fires immediately, no save button needed. |
| Timer | Start begins counting (count up or countdown, depending on your config). The display ticks in the Control Panel and in the overlay simultaneously. Stop pauses at the current time. Reset returns to zero (or the base duration for countdowns). "Count to" timers show the target datetime and tick automatically - no start/stop needed. |
| Boolean | A single toggle switch. Flip it on or off - the value updates immediately. Pairs well with conditionals to show/hide overlay sections. |
| Datetime | Pick a date and time from the datetime picker and click Save. Useful for "Next stream: [[[c:next_stream]]]" display text. |
| Expression | Expressions have no input in the Control Panel - their value is always derived from the formula. The panel shows the current expression and its live-evaluated result. |
Real-time updates
Every Control Panel action broadcasts the new value over your live channel. Any open overlay that references the changed control re-renders that value in real time - typically in under a second. No refresh required in OBS.
Access
The Control Panel is available only to the overlay owner and requires a logged-in session. Your viewers or collaborators cannot accidentally change your values - there is no public endpoint for mutations.
Copying an Overlay with Controls
When you copy a public overlay that has Controls attached, Overlabels walks you through the Import Wizard before navigating to your new copy.
The Import Wizard
The wizard shows a table of every control from the source overlay. For each one you can choose:
| Choice | What happens |
|---|---|
| Create | Recreate this control in your copy with the same type and config. You can edit the key before confirming if you want to rename it. |
| Skip | Leave this control out of your copy. Any overlay tags referencing it will render blank until you add a matching control yourself. |
What gets copied
- Copied: key, label, type, configuration (min/max/mode/base duration, etc.), and sort order.
- Also copied: the current value. Although this may bring in some stale data when you copy the overlay to your account, it does allow for sharing fully pre-configured overlay templates.
- New IDs: each created control gets a brand-new database ID. Changes you make to your copy's controls never affect the original template.
Skipping the wizard
Clicking Skip all, take me to the copy skips import entirely and takes you straight to your new overlay. Your copy will have zero controls at that point. You can always add controls manually from the Controls tab later, as long as you give them the same keys that your overlay HTML references.
Tips and Best Practices
Choose descriptive keys
Keys are effectively permanent, so name them like Future You is tired and mildly annoyed. Use boss_wins
instead of d.
Use sort order on purpose
The Control Panel displays controls by sort order. Put the values you touch most during a stream at the
top using sort orders like 0, 1, 2, and so on. That way the important stuff stays within easy reach
when things get hectic.
Use counters for values that change often
If you are tracking something numeric that changes during the stream, use a counter instead of a text
control. Hitting + or - is much faster and safer under pressure than manually typing a new number
every time.
Good examples:
- boss fights cleared
- rounds won
- times chat bullied you into a bad idea 🤣
Controls are more powerful with conditionals
A Control does not have to be shown as raw text. You can use it inside [[[if:c:wins >= 10]]] logic to
change content, styling, or full layout states.
[[[if:c:wins >= 10]]]
<div>
this run is incredible
</div>
[[[endif]]]
Use functions instead of nested ternaries
When comparing values across multiple services, avoid chaining ? : operators. Use latest(),
oldest(), max(), or min() instead - they scale to any number of services without nesting.
Instead of this:
c.streamlabs.donations_received_at > c.kofi.donations_received_at
? c.streamlabs.latest_donor_name
: c.kofi.latest_donor_name
Do this:
latest(
c.streamlabs.donations_received_at, c.streamlabs.latest_donor_name,
c.kofi.donations_received_at, c.kofi.latest_donor_name
)
Note which timestamp is being raced: donations_received_at, not latest_donor_name_at. _at moves
when a control changes, so a donor tipping twice in a row never moves the name's timestamp. The
counter goes up on every donation regardless of who sent it, so it is the one that answers "when did
this service last hear from anyone". Latest donator from any source
walks through it.
Use now() to track time since an event
Combine now() with max() and _at timestamps to show how long ago something happened - across any
number of services.
Example - seconds since the latest donation:
now() - max(
c.kofi.donations_received_at,
c.streamlabs.donations_received_at,
c.fourthwall.donations_received_at
)
Pair it with the duration pipe formatter in your template to display it as
[[[c:since_last_donation|duration:mm:ss]]].
Values are sanitized
HTML is stripped from text values before storage. You can't accidentally inject markup through a Control Panel update.
URLs and asset links work well in Controls
Controls can store URLs just fine, which makes them useful for reusable assets like:
- image URLs
- CSS file URLs
- profile links
- external media links
That lets you define a value once and reuse it throughout your overlay without hardcoding the same URL in multiple places.
Controls can also be used in alerts
Controls created on a static overlay can also parse inside alerts rendered on that same static overlay.
So if your underlying static overlay has a [[[c:myname]]] Control, your alerts can use that value too.
The important rule is scope: the Control must exist on the static overlay that the alert is rendered through. If it does not exist there, it will not parse.
If you need more help
You can always reach out on [email protected] or open a new issue on Github.