How to Create Custom Overlay
Want your stream to look cooler with your own custom overlay? A custom overlay is like a blank canvas you build with HTML. Make special messages, fun animations, or branding that makes people instantly remember you!
What Overlay Types Can Be Customized?
- Alert (Doesn't include media like Voice Note or GIF)
- Media Share (Only the message part — media/video and progress bar can't be customized)
- Leaderboard
- Timer (Re-rendered every time the time changes)
- Soundboard
- Milestone
- Polling
- Running Text
- VIP Queue
- Song Share (Song info, queue, and visual player — progress bar, play/pause, cover art — fully customizable with variables. The audio engine itself is still managed by Tako, can't be swapped/controlled from the overlay.)
HTML Writing Tips
- Use standard HTML structure: Always start with
<html>,<head>, and<body>. - Don't use
position: absoluteon the body CSS: Make sure the body has a definite height — Tako's overlay system detects the body's size, and if it can't detect it, the overlay won't show (the system treats the body height as 0px when it uses absolute positioning). - Use flexible sizes: Use
px,rem, oremfor fonts/elements. Don't usevhorvw. - Prefer
rem— it scales properly with the font-size setting in the overlay's dashboard settings. - Always test in the dashboard so it looks as expected.
- Don't use
//comments inside<script>: Tako's overlay system strips every newline from your code before rendering. A//comment only stops at a newline — once the newline is gone, that comment can "swallow" the rest of the script, breaking logic below it with no visible error. If you need a comment, use/* ... */, or safer — skip comments entirely. - Write JSON data (arrays/objects) literally, don't wrap it in quotes: Write
const data = {{variableName}};— NOTconst data = JSON.parse('{{variableName}}'). The JSON content already uses double quotes ("), and wrapping it in quotes again will cut your JS string in the middle, breaking the script. - Don't set a fixed
max-widthon the main container: The overlay box size (width/height) is already set by you via OBS Browser Source or the dashboard preview. If your code uses a fixedmax-width(e.g.26rem), the box gets capped there and leaves empty space if the real box is wider. Usewidth: 100%to fill the box size you've configured.
Variables for Each Overlay
Every overlay has variables you can use to make the display dynamic without manual edits. Full list below, including which overlay supports what and usage examples:
⚠️ Make sure there's no space between the curly braces and the variable name!
1. Alert & Media Share
Variable Name | Example Value | Description |
|---|---|---|
|
| Donation amount (raw number). |
|
| Formatted donation amount. |
|
| Message from the donor. |
|
| Donor's name. |
| Donor's profile picture URL. | |
|
| Donor's badge names, comma-separated. |
|
| Donor's verification status ( |
|
| Media duration on alert/mediashare; if there's no media, it's the alert's duration instead. |
⚠️ Tako doesn’t send badge icon URLs. If you want the badge to appear as an image (not text/a tag), manually fill in the BADGE_ICON_URLS in the script below: key = the exact badge name as it appears in {{gifterBadges}}, value = the URL of that badge icon. Badges not on that list will automatically fall back to a regular text tag.
The code example below has been pre-filled with 6 official Tako system badges (name + URL):
Badge Name | Icon URL |
|---|---|
| |
| |
| |
| |
| |
|
Example Code:
<html>
<head>
<style>
body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
.ticket {
position: relative;
width: 100%;
box-sizing: border-box;
overflow: hidden;
background: #f2e9d3;
border: 2px solid #1c1712;
box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
padding: 0.9rem 1rem 0.9rem 1.5rem;
}
.tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
.grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
.gifter-row { display: flex; align-items: center; gap: 0.6rem; }
.avatar { width: 2.6rem; height: 2.6rem; border-radius: 50%; border: 2px solid #1c1712; object-fit: cover; flex-shrink: 0; display: none; }
.avatar-fallback { width: 2.6rem; height: 2.6rem; border-radius: 50%; border: 2px solid #1c1712; background: #e8dab8; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; }
.gifter-info { flex: 1; min-width: 0; }
.gifter-name { font-weight: 700; font-size: 1.0625rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.badges { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; margin-top: 0.2rem; }
.badge-tag { font-size: 0.75rem; border: 1px solid #7a7160; color: #7a7160; padding: 0.05rem 0.35rem; }
.badge-icon { height: 1rem; width: auto; display: block; }
.amount { font-weight: 700; font-size: 1.125rem; color: #b6842c; flex-shrink: 0; }
.perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
.message { font-size: 1rem; color: #4a4335; }
.media-tag { display: inline-block; margin-top: 0.4rem; font-size: 0.75rem; color: #7a7160; border: 1px solid #7a7160; padding: 0.05rem 0.35rem; }
</style>
</head>
<body>
<div class="ticket">
<div class="tab"></div>
<div class="grommet"></div>
<div class="gifter-row">
<img class="avatar" id="avatar" alt="">
<span class="avatar-fallback" id="avatar-fallback">🎁</span>
<div class="gifter-info">
<div class="gifter-name" id="gifter-name">{{gifterName}}</div>
<div class="badges" id="badges"></div>
</div>
<div class="amount">{{formattedAmount}}</div>
</div>
<div class="perf"></div>
<div class="message" id="message">{{message}}</div>
<div class="media-tag" id="media-tag"></div>
</div>
<script>
const picture = "{{gifterPicture}}".trim();
const badgesRaw = "{{gifterBadges}}".trim();
const isVerified = "{{isGifterVerified}}" === "true";
const mediaDuration = "{{mediaDuration}}";
const BADGE_ICON_URLS = {
"Tako Verified Partner": "https://tako.id/images/badges/partnered-creator.png",
"Tako Verified Creator": "https://tako.id/images/badges/verified-creator.png",
"Rich Rich Club": "https://tako.id/images/badges/king.png",
"Charity": "https://tako.id/images/badges/charity.png",
"VTuber": "https://tako.id/images/badges/vtuber.png",
"Tako Team": "https://tako.id/images/badges/admin.png"
};
const avatar = document.getElementById("avatar");
const avatarFallback = document.getElementById("avatar-fallback");
const gifterName = document.getElementById("gifter-name");
const badgesEl = document.getElementById("badges");
const messageEl = document.getElementById("message");
const mediaTag = document.getElementById("media-tag");
if (picture && !picture.includes("{{")) {
avatar.src = picture;
avatar.style.display = "block";
avatarFallback.style.display = "none";
avatar.onerror = () => {
avatar.style.display = "none";
avatarFallback.style.display = "flex";
};
}
if (isVerified) gifterName.appendChild(document.createTextNode(" ✓"));
if (badgesRaw && !badgesRaw.includes("{{")) {
badgesRaw.split(",").filter(Boolean).forEach((name) => {
const iconUrl = BADGE_ICON_URLS[name];
if (iconUrl) {
const icon = document.createElement("img");
icon.className = "badge-icon";
icon.src = iconUrl;
icon.alt = name;
icon.title = name;
badgesEl.appendChild(icon);
} else {
const tag = document.createElement("span");
tag.className = "badge-tag";
tag.textContent = name;
badgesEl.appendChild(tag);
}
});
}
if (!messageEl.textContent.trim() || messageEl.textContent.includes("{{")) {
messageEl.style.display = "none";
}
if (mediaDuration && !mediaDuration.includes("{{") && Number(mediaDuration) > 0) {
mediaTag.textContent = "⏱ " + mediaDuration + "s";
} else {
mediaTag.style.display = "none";
}
</script>
</body>
</html>
2. Leaderboard
Variable Name | Example Value | Description |
|---|---|---|
|
| Leaderboard title from settings. |
|
| Ranking list (JSON): name, picture, badges (names only, comma-separated — no icon URL), donation amount, formatted amount. |
Example Code:
<html>
<head>
<style>
body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
.ticket {
position: relative;
width: 100%;
box-sizing: border-box;
overflow: hidden;
background: #f2e9d3;
border: 2px solid #1c1712;
box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
padding: 0.9rem 1rem 0.9rem 1.5rem;
}
.tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
.grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
.title { font-size: 1.0625rem; font-weight: 700; }
.perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
.rank-row { display: flex; align-items: center; gap: 0.6rem; font-size: 1rem; padding: 0.35rem 0; border-bottom: 1px dashed #e8dab8; }
.rank-row:last-child { border-bottom: none; }
.rank-num { width: 1.6rem; font-weight: 700; color: #b6842c; flex-shrink: 0; }
.rank-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rank-badges { display: flex; align-items: center; gap: 0.2rem; flex-shrink: 0; }
.rank-badge-tag { font-size: 0.75rem; border: 1px solid #7a7160; color: #7a7160; padding: 0.05rem 0.35rem; }
.rank-badge-icon { height: 0.9375rem; width: auto; display: block; }
.rank-amount { font-weight: 700; color: #7a7160; flex-shrink: 0; }
</style>
</head>
<body>
<div class="ticket">
<div class="tab"></div>
<div class="grommet"></div>
<div class="title" id="lb-title">{{title}}</div>
<div class="perf"></div>
<div id="lb-list"></div>
</div>
<script>
const rankings = {{rankings}};
const list = document.getElementById("lb-list");
const BADGE_ICON_URLS = {
"Tako Verified Partner": "https://tako.id/images/badges/partnered-creator.png",
"Tako Verified Creator": "https://tako.id/images/badges/verified-creator.png",
"Rich Rich Club": "https://tako.id/images/badges/king.png",
"Charity": "https://tako.id/images/badges/charity.png",
"VTuber": "https://tako.id/images/badges/vtuber.png",
"Tako Team": "https://tako.id/images/badges/admin.png"
};
rankings.forEach((r, i) => {
const row = document.createElement("div");
row.className = "rank-row";
const num = document.createElement("span");
num.className = "rank-num";
num.textContent = "#" + (i + 1);
const name = document.createElement("span");
name.className = "rank-name";
name.textContent = r.name;
const badges = document.createElement("div");
badges.className = "rank-badges";
(r.badges || "").split(",").filter(Boolean).forEach((badgeName) => {
const iconUrl = BADGE_ICON_URLS[badgeName];
if (iconUrl) {
const icon = document.createElement("img");
icon.className = "rank-badge-icon";
icon.src = iconUrl;
icon.alt = badgeName;
icon.title = badgeName;
badges.appendChild(icon);
} else {
const tag = document.createElement("span");
tag.className = "rank-badge-tag";
tag.textContent = badgeName;
badges.appendChild(tag);
}
});
const amount = document.createElement("span");
amount.className = "rank-amount";
amount.textContent = r.formattedAmount;
row.appendChild(num);
row.appendChild(name);
row.appendChild(badges);
row.appendChild(amount);
list.appendChild(row);
});
</script>
</body>
</html>
3. Milestone
Variable Name | Example Value | Description |
|---|---|---|
|
| Milestone title from settings. |
|
| Total raised across all steps (raw number). |
|
| Total raised, formatted. |
|
| Total target across all steps (raw number). |
|
| Total target, formatted. |
|
| Overall progress percentage (0-100, can exceed 100 if overshot). |
|
| Progress within the currently active step only (raw number). |
|
| Active step's progress, formatted. |
|
| Currently active step's target (raw number). |
|
| Active step's target, formatted. |
|
| Active step's progress percentage (0-100). |
|
| Active step's order number (starts at 1). |
|
| Total number of steps. |
|
| Active step's title (empty if not set). |
|
| List of all steps (JSON), each item has |
Example Code:
<html>
<head>
<style>
body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
.ticket {
position: relative;
width: 100%;
box-sizing: border-box;
overflow: hidden;
background: #f2e9d3;
border: 2px solid #1c1712;
box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
padding: 0.9rem 1rem 0.9rem 1.5rem;
}
.tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
.grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
.title { font-size: 1.0625rem; font-weight: 700; }
.perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
.bar-bg { height: 0.7rem; background: #e8dab8; border: 1px solid #1c1712; }
.bar-fill { height: 100%; background: #b6842c; }
.amounts { display: flex; justify-content: space-between; font-size: 1rem; font-weight: 700; margin-top: 0.45rem; }
.meta { font-size: 0.8125rem; color: #7a7160; margin-top: 0.45rem; }
</style>
</head>
<body>
<div class="ticket">
<div class="tab"></div>
<div class="grommet"></div>
<div class="title" id="ms-title">{{title}}</div>
<div class="perf"></div>
<div class="bar-bg">
<div class="bar-fill" id="bar-fill" style="width: {{percentage}}%"></div>
</div>
<div class="amounts">
<span id="current">{{formattedStepProgress}}</span>
<span id="target">{{formattedStepTarget}}</span>
</div>
<div class="meta">Step {{stepIndex}} / {{stepCount}}</div>
</div>
</body>
</html>
4. Polling
Variable Name | Example Value | Description |
|---|---|---|
|
| Poll title from settings. |
|
| Poll options list (JSON): ID, name, donation amount, formatted amount. |
|
| Poll start date. |
|
| Poll start time. |
|
| Poll end date. |
|
| Poll end time. |
Example Code:
<html>
<head>
<style>
body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
.ticket {
position: relative;
width: 100%;
box-sizing: border-box;
overflow: hidden;
background: #f2e9d3;
border: 2px solid #1c1712;
box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
padding: 0.9rem 1rem 0.9rem 1.5rem;
}
.tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
.grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
.title { font-size: 1.0625rem; font-weight: 700; }
.perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
.poll-row { display: flex; align-items: center; gap: 0.6rem; font-size: 1rem; padding: 0.35rem 0; border-bottom: 1px dashed #e8dab8; }
.poll-row:last-child { border-bottom: none; }
.poll-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.poll-amount { font-weight: 700; color: #7a7160; flex-shrink: 0; }
.meta { font-size: 0.8125rem; color: #7a7160; margin-top: 0.55rem; }
</style>
</head>
<body>
<div class="ticket">
<div class="tab"></div>
<div class="grommet"></div>
<div class="title" id="poll-title">{{title}}</div>
<div class="perf"></div>
<div id="poll-list"></div>
<div class="meta">{{startAt}} {{startTime}} — {{endAt}} {{endTime}}</div>
</div>
<script>
const options = {{pollingOptions}};
const list = document.getElementById("poll-list");
options.forEach((o) => {
const row = document.createElement("div");
row.className = "poll-row";
const name = document.createElement("span");
name.className = "poll-name";
name.textContent = o.name;
const amount = document.createElement("span");
amount.className = "poll-amount";
amount.textContent = o.formattedTotalAmount;
row.appendChild(name);
row.appendChild(amount);
list.appendChild(row);
});
</script>
</body>
</html>
5. Running Text
Variable Name | Example Value | Description |
|---|---|---|
|
| Extra text list (JSON): title, background color, text color. |
|
| Donation list (JSON): donor name, amount, formatted amount. |
Example Code:
<html>
<head>
<style>
body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; overflow: hidden; }
.ticker {
position: relative;
width: 100%;
box-sizing: border-box;
background: #f2e9d3;
border-top: 2px solid #1c1712;
border-bottom: 2px solid #1c1712;
padding: 0.6rem 0 0.6rem 1.4rem;
overflow: hidden;
white-space: nowrap;
}
.tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
.grommet { position: absolute; left: 0.275rem; top: 50%; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, -50%); z-index: 2; }
.ticker-track { display: inline-flex; gap: 1.5rem; animation: scroll 18s linear infinite; }
.ticker-item { font-size: 1rem; font-weight: 700; flex-shrink: 0; }
.ticker-item.gift { color: #9c3b2e; }
@keyframes scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
</style>
</head>
<body>
<div class="ticker">
<div class="tab"></div>
<div class="grommet"></div>
<div class="ticker-track" id="track"></div>
</div>
<script>
const texts = {{texts}};
const gifts = {{gifts}};
const track = document.getElementById("track");
function buildItems() {
const frag = document.createDocumentFragment();
texts.forEach((t) => {
const item = document.createElement("span");
item.className = "ticker-item";
item.style.color = t.textColor || "#1c1712";
item.textContent = t.title;
frag.appendChild(item);
});
gifts.forEach((g) => {
const item = document.createElement("span");
item.className = "ticker-item gift";
item.textContent = g.gifterName + " — " + g.formattedAmount;
frag.appendChild(item);
});
return frag;
}
track.appendChild(buildItems());
track.appendChild(buildItems());
</script>
</body>
</html>
Appending buildItems() twice makes the scroll seamless (two copies of the content sit side by side, and translateX(-50%) stops exactly when the second copy reaches the first copy's position). This overlay has no 250ms progress-tick like Song Share, so the CSS animation loop is safe to run continuously without restarting.
6. Soundboard
Variable Name | Example Value | Description |
|---|---|---|
|
| Donation amount (raw number). |
|
| Formatted donation amount. |
|
| Name of the chosen sound effect. |
|
| Donor's name. |
| Donor's profile picture URL. | |
|
| Donor's badge names, comma-separated — no icon URL from the system. |
|
| Donor's verification status ( |
Example Code:
<html>
<head>
<style>
body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
.ticket {
position: relative;
width: 100%;
box-sizing: border-box;
overflow: hidden;
background: #f2e9d3;
border: 2px solid #1c1712;
box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
padding: 0.9rem 1rem 0.9rem 1.5rem;
}
.tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
.grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
.gifter-row { display: flex; align-items: center; gap: 0.6rem; }
.avatar { width: 2.6rem; height: 2.6rem; border-radius: 50%; border: 2px solid #1c1712; object-fit: cover; flex-shrink: 0; display: none; }
.avatar-fallback { width: 2.6rem; height: 2.6rem; border-radius: 50%; border: 2px solid #1c1712; background: #e8dab8; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; }
.gifter-info { flex: 1; min-width: 0; }
.gifter-name { font-weight: 700; font-size: 1.0625rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.badges { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; margin-top: 0.2rem; }
.badge-tag { font-size: 0.75rem; border: 1px solid #7a7160; color: #7a7160; padding: 0.05rem 0.35rem; }
.badge-icon { height: 1rem; width: auto; display: block; }
.amount { font-weight: 700; font-size: 1.125rem; color: #b6842c; flex-shrink: 0; }
.perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
.sound-name { font-size: 1.0625rem; font-weight: 700; }
</style>
</head>
<body>
<div class="ticket">
<div class="tab"></div>
<div class="grommet"></div>
<div class="gifter-row">
<img class="avatar" id="avatar" alt="">
<span class="avatar-fallback" id="avatar-fallback">🔊</span>
<div class="gifter-info">
<div class="gifter-name" id="gifter-name">{{gifterName}}</div>
<div class="badges" id="badges"></div>
</div>
<div class="amount">{{formattedAmount}}</div>
</div>
<div class="perf"></div>
<div class="sound-name">🔈 {{soundName}}</div>
</div>
<script>
const picture = "{{gifterPicture}}".trim();
const badgesRaw = "{{gifterBadges}}".trim();
const isVerified = "{{isGifterVerified}}" === "true";
const BADGE_ICON_URLS = {
"Tako Verified Partner": "https://tako.id/images/badges/partnered-creator.png",
"Tako Verified Creator": "https://tako.id/images/badges/verified-creator.png",
"Rich Rich Club": "https://tako.id/images/badges/king.png",
"Charity": "https://tako.id/images/badges/charity.png",
"VTuber": "https://tako.id/images/badges/vtuber.png",
"Tako Team": "https://tako.id/images/badges/admin.png"
};
const avatar = document.getElementById("avatar");
const avatarFallback = document.getElementById("avatar-fallback");
const gifterName = document.getElementById("gifter-name");
const badgesEl = document.getElementById("badges");
if (picture && !picture.includes("{{")) {
avatar.src = picture;
avatar.style.display = "block";
avatarFallback.style.display = "none";
avatar.onerror = () => {
avatar.style.display = "none";
avatarFallback.style.display = "flex";
};
}
if (isVerified) gifterName.appendChild(document.createTextNode(" ✓"));
if (badgesRaw && !badgesRaw.includes("{{")) {
badgesRaw.split(",").filter(Boolean).forEach((name) => {
const iconUrl = BADGE_ICON_URLS[name];
if (iconUrl) {
const icon = document.createElement("img");
icon.className = "badge-icon";
icon.src = iconUrl;
icon.alt = name;
icon.title = name;
badgesEl.appendChild(icon);
} else {
const tag = document.createElement("span");
tag.className = "badge-tag";
tag.textContent = name;
badgesEl.appendChild(tag);
}
});
}
</script>
</body>
</html>
7. Timer
Variable Name | Example Value | Description |
|---|---|---|
|
| Timer title from settings. |
|
| Time left in |
|
| Timer status ( |
|
| Timer options list (JSON): added duration (ms), minimum donation amount to add time. |
Example Code:
<html>
<head>
<style>
body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
.ticket {
position: relative;
width: 100%;
box-sizing: border-box;
overflow: hidden;
background: #f2e9d3;
border: 2px solid #1c1712;
box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
padding: 0.9rem 1rem 0.9rem 1.5rem;
text-align: center;
}
.tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
.grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
.row-head { display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; text-align: left; }
.title { flex: 1; min-width: 0; font-size: 1.0625rem; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.status { flex-shrink: 0; font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: #7a7160; border: 1px solid #7a7160; padding: 0.1rem 0.4rem; }
.perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
.countdown { font-size: 2.25rem; font-weight: 700; letter-spacing: 0.05em; }
</style>
</head>
<body>
<div class="ticket">
<div class="tab"></div>
<div class="grommet"></div>
<div class="row-head">
<span class="title" id="timer-title">{{title}}</span>
<span class="status" id="status-el">Active</span>
</div>
<div class="perf"></div>
<div class="countdown">{{timeLeft}}</div>
</div>
<script>
const isEnabled = "{{isEnabled}}" === "true";
document.getElementById("status-el").textContent = isEnabled ? "Active" : "Inactive";
</script>
</body>
</html>
8. VIP Queue
Variable Name | Example Value | Description |
|---|---|---|
|
| Title from settings. |
|
| Total number of people in the queue. |
|
| Number of entries not shown because they exceed the display limit ( |
|
| Whether custom fields (e.g. address, note) are shown. |
|
| Queue list already cut down to the display limit (JSON) — used for rendering. Each entry has |
| same structure as | List of all entries (JSON), not cut down to the limit. |
|
| All custom fields from every entry, flattened into one array (JSON). |
Example Code:
<html>
<head>
<style>
body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
.ticket {
position: relative;
width: 100%;
box-sizing: border-box;
overflow: hidden;
background: #f2e9d3;
border: 2px solid #1c1712;
box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
padding: 0.9rem 1rem 0.9rem 1.5rem;
}
.tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
.grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
.row-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.title { font-size: 1.0625rem; font-weight: 700; }
.count-badge { font-size: 0.875rem; font-weight: 700; color: #b6842c; border: 1px solid #7a7160; padding: 0.05rem 0.45rem; border-radius: 999px; flex-shrink: 0; }
.perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
.queue-list { display: flex; flex-direction: column; gap: 0.5rem; }
.queue-row { display: flex; align-items: center; gap: 0.5rem; }
.queue-num { width: 1.5rem; height: 1.5rem; border-radius: 50%; background: #e8dab8; border: 1px solid #1c1712; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 700; }
.queue-avatar { width: 1.6rem; height: 1.6rem; border-radius: 50%; border: 1px solid #1c1712; object-fit: cover; flex-shrink: 0; display: none; }
.queue-name { flex: 1; min-width: 0; font-size: 1rem; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.queue-badges { display: flex; align-items: center; gap: 0.2rem; flex-shrink: 0; }
.queue-badge-icon { height: 1.0625rem; width: auto; display: block; }
.queue-amount { font-size: 0.875rem; font-weight: 700; color: #7a7160; flex-shrink: 0; }
.queue-fields { font-size: 0.75rem; color: #7a7160; padding-left: 2.1rem; }
.queue-empty { font-size: 1rem; font-style: italic; color: #7a7160; }
.hidden-note { font-size: 0.8125rem; text-align: center; color: #7a7160; font-weight: 700; margin-top: 0.3rem; }
</style>
</head>
<body>
<div class="ticket">
<div class="tab"></div>
<div class="grommet"></div>
<div class="row-head">
<div class="title" id="queue-title">{{title}}</div>
<div class="count-badge" id="count-badge">{{totalEntries}}</div>
</div>
<div class="perf"></div>
<div class="queue-list" id="queue-list"></div>
<div class="hidden-note" id="hidden-note"></div>
</div>
<script>
const entries = {{visibleEntries}};
const hiddenCount = {{hiddenCount}};
const showFields = "{{showFields}}" === "true";
const list = document.getElementById("queue-list");
const hiddenNote = document.getElementById("hidden-note");
if (entries.length === 0) {
const empty = document.createElement("p");
empty.className = "queue-empty";
empty.textContent = "No one in the queue yet.";
list.appendChild(empty);
}
entries.forEach((entry) => {
const row = document.createElement("div");
row.className = "queue-row";
const num = document.createElement("div");
num.className = "queue-num";
num.textContent = entry.index;
const avatar = document.createElement("img");
avatar.className = "queue-avatar";
if (entry.picture) {
avatar.src = entry.picture;
avatar.style.display = "block";
avatar.onerror = () => { avatar.style.display = "none"; };
}
const name = document.createElement("span");
name.className = "queue-name";
name.textContent = entry.name + (entry.isVerified ? " ✓" : "");
const badgesEl = document.createElement("div");
badgesEl.className = "queue-badges";
(entry.badgeItems || []).forEach((badge) => {
if (!badge.url) return;
const icon = document.createElement("img");
icon.className = "queue-badge-icon";
icon.src = badge.url;
icon.alt = badge.title || "";
icon.title = badge.title || "";
badgesEl.appendChild(icon);
});
const amount = document.createElement("span");
amount.className = "queue-amount";
amount.textContent = entry.formattedAmount;
row.appendChild(num);
row.appendChild(avatar);
row.appendChild(name);
row.appendChild(badgesEl);
row.appendChild(amount);
list.appendChild(row);
if (showFields && entry.fields && entry.fields.length > 0) {
const fieldsRow = document.createElement("div");
fieldsRow.className = "queue-fields";
fieldsRow.textContent = entry.fields.map((f) => f.label + ": " + (f.value || "-")).join(" • ");
list.appendChild(fieldsRow);
}
});
if (hiddenCount > 0) {
hiddenNote.textContent = "+" + hiddenCount + " more waiting";
}
</script>
</body>
</html>
9. Song Share
Variable Name | Example Value | Description |
|---|---|---|
|
| Title of the song currently playing. |
|
| Artist of the song currently playing. |
| Cover art URL of the song currently playing. | |
|
| Name of the person who requested the song. |
| Requester's profile picture URL. | |
|
| Requester's badge names (names only, comma-separated) — no icon URL from the system for this "Now Playing" row. Fill in |
|
| Requester's verification status. |
|
| Song duration (raw number). |
|
| Formatted song duration. |
|
| Elapsed playback time (raw number). |
|
| Formatted elapsed playback time. |
|
| Song progress percentage (0-100) — use it to build your own progress bar. |
|
| Whether the song is currently playing. |
|
| Whether the song is currently paused. |
|
| Whether it's currently in a gap between songs. |
|
| Time left in the gap between songs. |
|
| Status text for the gap between songs. |
|
| Error message if any, empty if none. |
|
| Object (JSON) of the song currently playing, |
|
| List of upcoming songs in the queue (JSON). Each item has |
Example Code:
<html>
<head>
<style>
body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
.ticket {
position: relative;
width: 100%;
box-sizing: border-box;
overflow: hidden;
background: #f2e9d3;
border: 2px solid #1c1712;
box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
padding: 0.9rem 1rem 0.9rem 1.5rem;
}
.tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
.grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
.row-head { display: flex; justify-content: flex-end; margin-bottom: 0.5rem; }
.status { font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: #7a7160; border: 1px solid #7a7160; padding: 0.1rem 0.4rem; }
.error-banner { font-size: 0.8125rem; background: #9c3b2e; color: #f2e9d3; padding: 0.35rem 0.5rem; margin-bottom: 0.5rem; display: none; }
.gap-banner { font-size: 0.9375rem; color: #7a7160; padding: 0.4rem 0; display: none; }
.now-playing { display: flex; gap: 0.75rem; }
.cover { position: relative; width: 3.4rem; height: 3.4rem; flex-shrink: 0; background: #e8dab8; border: 2px solid #1c1712; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; color: #7a7160; }
.cover img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: none; }
.info { flex: 1; min-width: 0; }
.title { font-size: 1.0625rem; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.title.idle { font-weight: 400; color: #7a7160; }
.artist { font-size: 0.9375rem; color: #7a7160; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 0.35rem; }
.requester { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; font-size: 0.8125rem; color: #7a7160; margin-bottom: 0.45rem; }
.requester img { width: 1.1rem; height: 1.1rem; border-radius: 50%; flex-shrink: 0; display: none; }
.requester .badge-tag { font-size: 0.75rem; border: 1px solid #7a7160; padding: 0.05rem 0.35rem; }
.requester .badge-icon { height: 0.9375rem; width: auto; display: block; }
.progress-row { display: flex; align-items: center; gap: 0.5rem; }
.bar-bg { flex: 1; height: 0.45rem; background: #e8dab8; border: 1px solid #1c1712; }
.bar-fill { height: 100%; background: #b6842c; }
.time { font-size: 0.8125rem; color: #7a7160; flex-shrink: 0; }
.perf { border-bottom: 1px dashed #1c1712; margin: 0.65rem 0 0.5rem; }
.setlist-label { font-size: 0.75rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: #7a7160; margin-bottom: 0.4rem; }
.setlist { display: none; }
.setlist-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9375rem; padding: 0.25rem 0; }
.setlist-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.setlist-badges { display: flex; align-items: center; gap: 0.2rem; flex-shrink: 0; }
.setlist-badge-icon { height: 0.9375rem; width: auto; display: block; }
.setlist-time { color: #7a7160; flex-shrink: 0; }
</style>
</head>
<body>
<div class="ticket">
<div class="tab"></div>
<div class="grommet"></div>
<div class="row-head">
<span class="status" id="status-el">Now Playing</span>
</div>
<div class="error-banner" id="error-banner"></div>
<div class="gap-banner" id="gap-banner"></div>
<div id="player-body">
<div class="now-playing">
<div class="cover" id="cover">
<span id="cover-fallback">♪</span>
<img id="cover-img" alt="">
</div>
<div class="info">
<div class="title" id="title-el">{{songTitle}}</div>
<div class="artist" id="artist-el">{{songArtist}}</div>
<div class="requester" id="requester-row">
<img id="requester-pic" alt="">
<span id="requester-name">{{requesterName}}</span>
<span id="requester-verified"></span>
<span id="requester-badges"></span>
</div>
<div class="progress-row" id="progress-row">
<div class="bar-bg">
<div class="bar-fill" style="width: {{progressPercent}}%"></div>
</div>
<div class="time" id="time-el" data-elapsed="{{elapsed}}" data-duration="{{duration}}">{{formattedElapsed}} / {{formattedDuration}}</div>
</div>
</div>
</div>
</div>
<div id="setlist-wrap">
<div class="perf" id="setlist-perf" style="display:none"></div>
<div class="setlist" id="setlist">
<div class="setlist-label" id="setlist-label">Up Next</div>
<div id="setlist-body"></div>
</div>
</div>
</div>
<script>
const isPlaying = "{{isPlaying}}" === "true";
const isPaused = "{{isPaused}}" === "true";
const isInGap = "{{isInGap}}" === "true";
const gapText = "{{gapText}}";
const errorText = "{{error}}";
const coverUrl = "{{songCoverArtUrl}}".trim();
const requesterPicture = "{{requesterPicture}}".trim();
const requesterBadges = "{{requesterBadges}}".trim();
const isRequesterVerified = "{{isRequesterVerified}}" === "true";
const BADGE_ICON_URLS = {
"Tako Verified Partner": "https://tako.id/images/badges/partnered-creator.png",
"Tako Verified Creator": "https://tako.id/images/badges/verified-creator.png",
"Rich Rich Club": "https://tako.id/images/badges/king.png",
"Charity": "https://tako.id/images/badges/charity.png",
"VTuber": "https://tako.id/images/badges/vtuber.png",
"Tako Team": "https://tako.id/images/badges/admin.png"
};
const titleEl = document.getElementById("title-el");
const artistEl = document.getElementById("artist-el");
const coverImg = document.getElementById("cover-img");
const coverFallback = document.getElementById("cover-fallback");
const statusEl = document.getElementById("status-el");
const errorBanner = document.getElementById("error-banner");
const gapBanner = document.getElementById("gap-banner");
const playerBody = document.getElementById("player-body");
const requesterRow = document.getElementById("requester-row");
const requesterPic = document.getElementById("requester-pic");
const requesterVerified = document.getElementById("requester-verified");
const requesterBadgesEl = document.getElementById("requester-badges");
const progressRow = document.getElementById("progress-row");
const setlistPerf = document.getElementById("setlist-perf");
const setlist = document.getElementById("setlist");
const setlistLabel = document.getElementById("setlist-label");
const setlistBody = document.getElementById("setlist-body");
const titleText = titleEl.textContent.trim();
const isIdle = !titleText || titleText === "{{" + "songTitle" + "}}";
if (errorText && !errorText.includes("{{")) {
errorBanner.textContent = "⚠ " + errorText;
errorBanner.style.display = "block";
}
if (isInGap) {
playerBody.style.display = "none";
gapBanner.textContent = gapText || "Preparing next song...";
gapBanner.style.display = "block";
statusEl.textContent = "Gap";
} else if (isIdle) {
titleEl.textContent = "No song playing yet";
titleEl.classList.add("idle");
artistEl.textContent = "Send a gift to request a song";
requesterRow.style.display = "none";
progressRow.style.display = "none";
coverFallback.style.display = "flex";
statusEl.textContent = "Idle";
} else {
statusEl.textContent = isPaused ? "Paused" : isPlaying ? "Now Playing" : "Buffering";
if (coverUrl && !coverUrl.includes("{{")) {
coverImg.src = coverUrl;
coverImg.style.display = "block";
coverFallback.style.display = "none";
coverImg.onerror = () => {
coverImg.style.display = "none";
coverFallback.style.display = "flex";
};
}
if (requesterPicture && !requesterPicture.includes("{{")) {
requesterPic.src = requesterPicture;
requesterPic.style.display = "block";
}
if (isRequesterVerified) requesterVerified.textContent = "✓";
if (requesterBadges) {
requesterBadges.split(",").filter(Boolean).forEach((name) => {
const iconUrl = BADGE_ICON_URLS[name];
if (iconUrl) {
const icon = document.createElement("img");
icon.className = "badge-icon";
icon.src = iconUrl;
icon.alt = name;
icon.title = name;
requesterBadgesEl.appendChild(icon);
} else {
const tag = document.createElement("span");
tag.className = "badge-tag";
tag.textContent = name;
requesterBadgesEl.appendChild(tag);
}
});
}
}
try {
const queueData = {{queue}};
if (Array.isArray(queueData) && queueData.length > 0) {
setlistPerf.style.display = "block";
setlist.style.display = "block";
setlistLabel.textContent = "Up Next (" + queueData.length + ")";
queueData.forEach((song, i) => {
const row = document.createElement("div");
row.className = "setlist-row";
const name = document.createElement("span");
name.className = "setlist-name";
name.textContent = (i + 1) + ". " + song.title + " — " + song.artist;
const badgesEl = document.createElement("div");
badgesEl.className = "setlist-badges";
(song.senderBadgeItems || []).forEach((badge) => {
if (!badge.url) return;
const icon = document.createElement("img");
icon.className = "setlist-badge-icon";
icon.src = badge.url;
icon.alt = badge.title || "";
icon.title = badge.title || "";
badgesEl.appendChild(icon);
});
const duration = document.createElement("span");
duration.className = "setlist-time";
duration.textContent = song.formattedDuration;
row.appendChild(name);
row.appendChild(badgesEl);
row.appendChild(duration);
setlistBody.appendChild(row);
});
}
} catch (e) {}
</script>
</body>
</html>
Additional Info You Might See
- "⚠ This is a test message": Shows when the overlay is being tested or previewed.
- "⛔ Sent by Admin": Shows when a gift is sent by Tako's team/admin.
How to Implement/Build a Custom Overlay
- Log into your Creator Dashboard
- Find the "Overlays" menu on the left
- Pick an overlay (e.g. Alert)
- Under "Theme", switch to "Custom HTML".
- Copy-paste the code you built into the textarea that appears.
- Check the preview at the top.
Updated on: 04/08/2026
Thank you!
