Browse the docs

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.

Guide 19 min read
.md
On this page
  1. What are Controls?
  2. Control Types in Detail
  3. Preset Controls (from integrations)
  4. Managing Controls
  5. Using Controls in Overlays
  6. The Control Panel
  7. Copying an Overlay with Controls
  8. Tips and Best Practices
  9. If you need more help

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:2 to get a clean 0.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: [[[if:c:alerts:muted]]]ALERTS ARE MUTED[[[endif]]] - it flips live when you hit the mute button on the Events page.

Browse every preset →

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.

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.

Related docs

Reading this as a machine? The same page as plain markdown: /help/controls.md