Getting started
The HTML & CSS tabs draw your widget. The box is transparent and floats over the video — never paint a full opaque background.
In the JS tab, subscribe with TE.on('gift', fn). window.TE is already loaded — no imports, no setup.
Use Simulate or the Fire buttons to preview offline, then Add to overlay — or let Claude build it via MCP.
var box = document.getElementById('box');
TE.on('gift', function (ev) {
box.textContent = ev.user.name + ' sent ' + ev.gift.name + '!';
box.classList.remove('pop'); void box.offsetWidth; box.classList.add('pop');
});Connect Claude (MCP)
Build and edit these widgets straight from Claude Desktop or Claude Code. Generate a token, add the connector, and Claude can call these tools on your account:
get_docsThe full authoring guide + every trigger and action (Claude reads this first).list_widgetsList your saved widgets with their ids.get_widgetFetch one widget’s html / css / js.create_widgetCreate a new HTML widget from name + html/css/js.update_widgetEdit an existing widget.delete_widgetDelete a widget by id.list_templatesList the built-in templates to copy from.get_templateRead a template’s html/css/js.create_from_templateCopy a template into a new widget.list_community_templatesBrowse widgets published by the community.install_community_templateInstall a private, editable fork of a community template.The TE SDK
Everything a widget does goes through the global window.TE object.
TE.on(type, fn)Subscribe to a live event. `type` is any trigger below; `fn(ev)` runs each time it happens.
TE.on('*', fn)Catch every event. `fn(ev, type)` receives the payload and the event name.
TE.off(type, fn)Remove a previously registered handler.
TE.onGift(name, fn)Fire only for gifts whose name matches (case-insensitive substring). Omit `name` to catch every gift.
TE.onSticker(idOrUrl, fn)Fire only for one specific subscriber emote / sticker — matched by its TikTok id, image URL, or source.
TE.rules.allowUser / allowGiftReusable eligibility filters for roles, allow/deny lists, gift name and minimum coin value.
TE.metricsThe latest stream snapshot object (viewers, likes, coins, followers, topGifters…). Also delivered via TE.on('metrics', fn).
TE.defineSettings([...])Declare streamer-tweakable controls; returns the current values (defaults merged with the streamer's choices).
TE.settingsThe current settings values object (same shape defineSettings returned).
TE.on('settings', fn)Runs when the streamer changes a setting live — re-render with the new values.
TE.state.get/set/incrementPromise-based atomic values for totals and shared game state.
TE.collection.*Server-owned join-once participants and score tables: join, increment, list, count, remove and clear.
TE.queue.*Bounded server-owned FIFO queues for media, requests and viewer-triggered actions.
TE.shared.*Opt several widgets into one named channel-state namespace; unrelated widgets remain isolated.
TE.random.draw(name, opts)Securely draw one or more winners from a collection and record the result.
TE.cooldown.claim(scope, user, ms)Atomically claim a global or per-user cooldown; returns claimed and retryAfterMs.
TE.timer.*Start, pause, reset and read a persisted wall-clock timer that survives reloads.
TE.points.trySpend(user, amount, reason)Check and debit points in one transaction; always inspect result.ok before running a paid interaction.
Triggers
Live events you can listen to. Every user contains { id, name, username, avatar, roles }.
TE.on('reward')A reward attempt completed. status is redeemed or insufficient; successful events are already charged.
{ status, user, reward: { id, name, cost, currency, trigger }, balance, missing, message|null }TE.on('gift')A viewer sent a gift. coins = total for the combo; streakEnd marks the combo finished.
{ user, gift: { name, image|null, coins, repeat, combo, streakEnd } }TE.on('follow')A viewer followed the account.
{ user }TE.on('subscribe')A viewer subscribed. months = how many months in a row.
{ user, months }TE.on('share')A viewer shared the live.
{ user }TE.on('chat')A chat message. emotes = inline subscriber-emote image URLs in this message.
{ user, comment, emotes:[url,…] }TE.on('like')A viewer sent likes. count = this burst; total = running total.
{ user, count, total }TE.on('join')A viewer entered the room. isTop = a top-gifter joined.
{ user, isTop }TE.on('sticker')A subscriber emote or on-screen sticker sent live. url = the emote image; id = its TikTok id.
{ user|null, id|null, url, source }TE.on('metrics')The latest stream snapshot. TE.metrics holds it; the event fires whenever it updates.
{ live, viewers, likes, followers, coins, topGifters:[…], … }TE.on('milestone')A round-number milestone was crossed (likes, coins, followers, subs).
{ metric, value, label }TE.on('poll')TikTok's native live poll — real votes, start to finish.
{ state:'start'|'update'|'end', title, options:[{text,votes}], endsAt }TE.on('battle')LinkMic battle updates: battle cards, fan tickets, army sizes.
{ card|null, tickets|null, armies|null, battleId|null }TE.on('rank')Hourly-ranking position and rank-up moments.
{ rank|null, from|null, to|null, countdown|null }TE.on('envelope')A red envelope / treasure box dropped in the live.
{ }TE.on('pinned')The host pinned a comment.
{ text }TE.on('deleted')A moderator removed a chat message (id matches the earlier chat event).
{ id }TE.on('streamState')The stream went live or offline.
{ live }TE.on('apiEvent')A custom event posted through the Event API. Listen to apiEvent or directly to its custom name.
{ name, data }TE.on('nowPlaying')The streamer's current Spotify track PLUS the up-next queue. Fires on connect and whenever the song, play-state, progress or queue changes. playback is null when nothing is playing; queue is the upcoming tracks.
{ connected, playback: { isPlaying, progressMs, durationMs, title, artists:[…], album, artwork|null } | null, queue:[{ title, artist, artwork|null, durationMs }] }TE.onGift('Rose', function (ev) { /* … */ });
TE.onSticker('<sticker id>', function (ev) { /* … */ });On the Stickers page, hover any emote → Trigger auto-builds a widget wired to that exact emote.
Actions
What your widget can do in response — plain browser APIs, ready to paste.
Pop an image on screen — the gift/emote artwork, an uploaded asset, or any URL. Auto-removes after a few seconds.
// Show an image, then fade it out
TE.on('gift', function (ev) {
var img = document.createElement('img');
img.src = ev.gift.image; // ← any image URL works
img.style.cssText = 'position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);max-width:60%';
document.body.appendChild(img);
setTimeout(function () { img.remove(); }, 4000); // ← how long it stays
});Play audio on an event. Expose a sound setting so the streamer picks their own file — no code editing.
// Play a sound the streamer chose in settings
var s = TE.defineSettings([{ key: 'sfx', label: 'Alert sound', type: 'sound', default: '' }]);
TE.on('settings', function (ns) { s = ns; });
TE.on('gift', function (ev) {
if (s.sfx) { var a = new Audio(s.sfx); a.volume = 0.8; a.play(); }
});Write dynamic text and re-trigger a CSS animation by toggling a class.
// Announce the event with a CSS pop animation
var box = document.getElementById('box'); // your element in the HTML tab
TE.on('follow', function (ev) {
box.textContent = ev.user.name + ' followed!';
box.classList.remove('pop'); void box.offsetWidth; box.classList.add('pop');
});Bind an on-screen number to a live metric — viewers, likes, coins, followers — updated automatically.
// Keep a number in sync with the live stream
TE.on('metrics', function (m) {
document.getElementById('count').textContent = m.viewers.toLocaleString();
});Speak an event with the browser’s text-to-speech. Great for gift or follow shout-outs.
// Text-to-speech shout-out
TE.on('gift', function (ev) {
var u = new SpeechSynthesisUtterance(ev.user.name + ' sent ' + ev.gift.name);
speechSynthesis.speak(u);
});Simple synchronous persistence for visual state. Use the transactional runtime for entrants, purchases and shared scores.
// Persistent state: survives OBS reloads
var total = TE.store.get('total', 0); // read (with default)
TE.on('gift', function (ev) {
total += ev.gift.coins;
TE.store.set('total', total); // write (auto-saved)
render();
});Exact-match chat commands plus atomic collections make safe entries, votes and spins possible.
// One server-authoritative entry per viewer
TE.onCommand('!join', function (c) {
TE.collection.join('entrants', c.user.id, c.user).then(function (result) {
if (result.joined) renderCount(result.count);
});
});Request overlay alerts, sounds, TTS, counters, subathon time or OBS scene switches directly from a widget.
// One call, one stream action
TE.act({ id: 'tts', message: 'New high score!' });
TE.act({ id: 'obsScene', scene: 'Hype cam' }); // needs the Desktop BridgeTransactionally check, spend or award loyalty points and receive the resulting balance.
// Only spin after the points were really debited
TE.onCommand('!spin', function (ev) {
TE.points.trySpend(ev.user, 50, 'Wheel spin').then(function (result) {
if (result.ok) spin();
else showMissing(result.missing);
});
});Atomic state, collections, secure random draws, cooldowns and persistent timers — safe across duplicate browser sources.
// Fair draw from a server-owned entrant collection
TE.random.draw('entrants', { count: 1 }).then(function (result) {
if (result.ok) reveal(result.winners[0].value);
});Expose text, colors, numbers, toggles — the streamer tweaks them live in the overlay editor, no code needed.
// No-code controls for whoever uses the widget
var s = TE.defineSettings([
{ key: 'title', label: 'Title', type: 'text', default: 'Goal' },
{ key: 'accent', label: 'Color', type: 'color', default: '#FF2E4D' },
]);
document.body.style.setProperty('--accent', s.accent);
TE.on('settings', function (ns) { s = ns; /* re-render with new values */ });Streamer settings
Declare controls with TE.defineSettings([...]) and whoever uses the widget tweaks them live in the overlay editor — no code. Each field:
A placed widget with one or more 'button' fields exposes those actions in its settings inside the Overlay Editor. The Widget Builder displays the same buttons for safe preview testing while coding.
'text'Single-line text inputstring'number'Number inputnumber'color'Color pickerhex string, e.g. "#FF2E4D"'select'Dropdown (needs options: [...])one of the options'toggle'On/off switchboolean'range'Slider (min / max / step)number'button'Declarative action buttonincrement, set or toggle — validated JSON, never dashboard code'sound'Picker over the streamer's uploaded sounds + Uploadthe chosen file URL ("" = none)'image'Picker over the streamer's uploaded images + Uploadthe chosen file URL ("" = none)var s = TE.defineSettings([
{ key: 'title', label: 'Title', type: 'text', default: 'Follower goal' },
{ key: 'target', label: 'Goal', type: 'number', default: 100 },
{ key: 'accent', label: 'Color', type: 'color', default: '#FF2E4D' },
{ key: 'reset', label: 'Reset', type: 'button', default: 0,
action: { type: 'increment', step: 1 }, confirm: true, tone: 'danger' },
]);
// re-render when the streamer changes something live
var lastReset = s.reset;
TE.on('settings', function (ns) {
s = ns;
if (ns.reset !== lastReset) { lastReset = ns.reset; resetCounter(); }
render();
});Control definitions are validated data. Button action accepts only increment, set or toggle. HTML, callback code and arbitrary URLs are rejected or ignored by the dashboard renderer.
Sandbox & limits
Widgets run in an isolated iframe with a strict content-security policy, so a broken or hostile widget can never touch your account or the rest of the page.
- Remote images & GIFs (TikTok gift art, avatars, any URL)
- Google Fonts + your own @font-face
- Audio via new Audio(url)
- Requests to TokElements' own APIs (e.g. /api/files/…)
- CSS animations, SVG, canvas, Web Audio
- External <script> tags / CDNs
- fetch / XHR / WebSocket to other hosts
- Cookies, localStorage, parent-page access
- Loading npm packages
Examples
Complete, copy-paste widgets. Each preview is live — demo events fire so you can watch it react.
var img = document.getElementById('emote');
// fires for every emote — use TE.onSticker('<id>', …) for one specific one
TE.on('sticker', function (ev) {
img.src = ev.url;
img.classList.remove('show'); void img.offsetWidth; img.classList.add('show');
});var target = 200; // ← your goal
TE.on('metrics', function (m) {
var pct = Math.min(100, m.follows / target * 100);
document.getElementById('fill').style.width = pct + '%';
document.getElementById('txt').textContent = m.follows + ' / ' + target;
});