> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://help.tako.id/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# 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?

1. **Alert** (Doesn't include media like Voice Note or GIF)
2. **Media Share** (Only the message part is customizable; media/video and the progress bar aren't)
3. **Leaderboard**
4. **Timer** (Re-renders every second **only if** your code uses `{{timeLeft}}` or `{{timeLeftMs}}`. Use `{{timerEndAt}}` alone and run your own countdown in `<script>` instead: the overlay renders once and runs itself, no flicker)
5. **Soundboard**
6. **Milestone**
7. **Polling**
8. **Running Text**
9. **VIP Queue**
10. **Song Share** (Song info, queue, and visual player: progress bar, play/pause, cover art, all fully customizable with variables. The audio engine itself is still managed by Tako and can't be swapped or controlled from the overlay.)

---

## HTML Writing Tips

1. **Use standard HTML structure**: Always start with `<html>`, `<head>`, and `<body>`.
2. **Don't use `position: absolute` on 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 (it treats the body height as 0px when it uses absolute positioning).
3. **Use flexible sizes**: Use `px`, `rem`, or `em` for fonts/elements. **Don't use `vh` or `vw`**.
4. **Prefer `rem`**: it scales properly with the font-size setting in the overlay's dashboard settings.
5. Always test in the dashboard so it looks as expected.
6. **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, and 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 `/* ... */`. Safer still: skip comments entirely.
7. **Write JSON data (arrays/objects) literally, don't wrap it in quotes**: Write `const data = {{variableName}};`, NOT `const 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.
8. **Don't set a fixed `max-width` on 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 fixed `max-width` (e.g. `26rem`), the box gets capped there and leaves empty space if the real box is wider. Use `width: 100%` to fill the box size you've configured.
9. **Write raw numbers literally, wrap text in quotes**: Number variables (e.g. `{{timerEndAt}}`, `{{timerAddedMs}}`, `{{hiddenCount}}`) are written without quotes, like `const endAt = {{timerEndAt}};`. Text variables (e.g. `{{gifterName}}`, `{{timestamp}}`) are wrapped in quotes, like `const name = "{{gifterName}}";`.

---

## 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 |
| ---- |
| `{{amount}}` | `15000` | Donation amount (raw number). |
| `{{formattedAmount}}` | `IDR 15.000` | Formatted donation amount. |
| `{{message}}` | `Keep it up!` | Message from the donor. |
| `{{gifterName}}` | `BudiGaming` | Donor's name. |
| `{{gifterPicture}}` | https://example.com/profile.jpg | Donor's profile picture URL. |
| `{{gifterBadges}}` | `VIP,TopDonator` | Donor's badge names, comma-separated. |
| `{{isGifterVerified}}` | `true` or `false` | Donor's verification status (`true` if verified). |
| `{{mediaDuration}}` | `10` (seconds) | Media duration on alert/mediashare; if there's no media, it's the alert's duration instead. |
| `{{timestamp}}` | `Sep 11, 2026 12:29` | Gift time, formatted `dd MMM yyyy HH:mm` in your timezone. Same exact value the built-in themes print under the card. |
| `{{showTimestamp}}` | `true` or `false` | Status of the "Show Timestamp Below Overlay?" toggle in the dashboard. Custom HTML renders as-is, so if you want the timestamp to follow this toggle, check it yourself in `<script>` (see example code). |

⚠️ 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 |
| ---- |
| `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 |

**Example Code:**

```html
<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; }
    .stamp { margin-top: 0.35rem; text-align: right; font-size: 0.75rem; letter-spacing: 0.04em; color: #7a7160; }
  </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 class="stamp" id="stamp">{{timestamp}}</div>
  </div>

  <script>
    const picture = "{{gifterPicture}}".trim();
    const badgesRaw = "{{gifterBadges}}".trim();
    const isVerified = "{{isGifterVerified}}" === "true";
    const mediaDuration = "{{mediaDuration}}";
    const showTimestamp = "{{showTimestamp}}" === "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");
    const messageEl = document.getElementById("message");
    const mediaTag = document.getElementById("media-tag");
    const stampEl = document.getElementById("stamp");

    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";
    }

    if (!showTimestamp || !stampEl.textContent.trim() || stampEl.textContent.includes("{{")) {
      stampEl.style.display = "none";
    }
  </script>
</body>
</html>
```

💡 `{{timestamp}}` and `{{showTimestamp}}` work on both Alert and Media Share. `{{showTimestamp}}` follows whichever overlay is currently showing: if the gift is a mediashare, it reads the Media Share toggle, not Alert's.

---

### 2. Leaderboard

| Variable Name | Example Value | Description |
| ---- |
| `{{title}}` | `Top Donors This Month` | Leaderboard title from settings. |
| `{{rankings}}` | `[{"name":"BudiGaming","picture":"https://example.com/profile.jpg","badges":"VIP,TopDonator","amount":15000,"formattedAmount":"Rp15.000"}, ...]` | Ranking list (JSON): name, picture, badges (names only, comma-separated; **no icon URL**), donation amount, formatted amount. |

**Example Code:**

```html
<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 |
| ---- |
| `{{title}}` | `Around the Archipelago` | Milestone title from settings. |
| `{{currentAmount}}` | `75000` | Total raised across all steps (raw number). |
| `{{formattedCurrentAmount}}` | `IDR 75.000` | Total raised, formatted. |
| `{{targetAmount}}` | `100000` | Total target across all steps (raw number). |
| `{{formattedTargetAmount}}` | `IDR 100.000` | Total target, formatted. |
| `{{overallPercentage}}` | `75` | Overall progress percentage (0-100, can exceed 100 if overshot). |
| `{{stepProgress}}` | `25000` | Progress within the currently active step only (raw number). |
| `{{formattedStepProgress}}` | `IDR 25.000` | Active step's progress, formatted. |
| `{{stepTarget}}` | `50000` | Currently active step's target (raw number). |
| `{{formattedStepTarget}}` | `IDR 50.000` | Active step's target, formatted. |
| `{{percentage}}` | `50` | Active step's progress percentage (0-100). |
| `{{stepIndex}}` | `2` | Active step's order number (starts at 1). |
| `{{stepCount}}` | `4` | Total number of steps. |
| `{{stepTitle}}` | `Step 2: Bali` | Active step's title (empty if not set). |
| `{{steps}}` / `{{milestoneSteps}}` | `[{"title":"Step 1","target":25000,"currentAmount":25000,"percentage":100,"isActive":false}, ...]` | List of all steps (JSON), each item has `formattedTarget`/`formattedCurrentAmount`. |

**Example Code:**

```html
<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 |
| ---- |
| `{{title}}` | `Pick the Next Game!` | Poll title from settings. |
| `{{pollingOptions}}` | `[{"id":1,"name":"Game A","totalAmount":5000,"formattedTotalAmount":"Rp5.000"}, ...]` | Poll options list (JSON): ID, name, donation amount, formatted amount. |
| `{{startAt}}` | `2025-07-08` | Poll start date. |
| `{{startTime}}` | `09:30` | Poll start time. |
| `{{endAt}}` | `2025-07-08` | Poll end date. |
| `{{endTime}}` | `12:30` | Poll end time. |

**Example Code:**

```html
<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 |
| ---- |
| `{{texts}}` | `[{"title":"Welcome!","backgroundColor":"#333","textColor":"#FFF"}, ...]` | Extra text list (JSON): title, background color, text color. |
| `{{gifts}}` | `[{"gifterName":"BudiGaming","amount":15000,"formattedAmount":"Rp15.000"}, ...]` | Donation list (JSON): donor name, amount, formatted amount. |

**Example Code:**

```html
<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 |
| ---- |
| `{{amount}}` | `15000` | Donation amount (raw number). |
| `{{formattedAmount}}` | `Rp15.000` | Formatted donation amount. |
| `{{soundName}}` | `Explosion` | Name of the chosen sound effect. |
| `{{gifterName}}` | `BudiGaming` | Donor's name. |
| `{{gifterPicture}}` | https://example.com/profile.jpg | Donor's profile picture URL. |
| `{{gifterBadges}}` | `VIP,TopDonator` | Donor's badge names, comma-separated; **no icon URL** from the system. |
| `{{isGifterVerified}}` | `true` or `false` | Donor's verification status (`true` if verified). |

**Example Code:**

```html
<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 |
| ---- |
| `{{title}}` | `Event Starting!` | Timer title from settings. |
| `{{timeLeft}}` | `05:30` | Time left in `MM:SS` format (or `00:00` when expired). **Changes every second**, which causes the overlay to re-render every second. |
| `{{timeLeftMs}}` | `330000` | Time left in milliseconds (raw number). **Changes every second** too. |
| `{{timerEndAt}}` | `1757568540000` | When the timer expires, as an epoch millisecond timestamp (`0` = timer inactive / not running). This value **only changes when time is added/removed/reset**, so if your code uses only this (without `{{timeLeft}}`/`{{timeLeftMs}}`), the overlay renders once and you run your own countdown in JS, so your animation never resets every second. |
| `{{timerAddedMs}}` | `60000` | The most recent time change, in milliseconds. Negative = time removed. `0` if there's been no change since the overlay loaded. |
| `{{timerAddedSeconds}}` | `60` | Same as above, in seconds. |
| `{{timerAdded}}` | `+01:00` | Same, fully formatted with a plus/minus sign. Empty if there's been no change yet. |
| `{{isEnabled}}` | `true` or `false` | Timer status (`true` if active). |
| `{{options}}` | `[{"durationAdded":300,"minimumGiftAmount":10000}, ...]` | Timer options list (JSON): `durationAdded` = time added, in **seconds**, `minimumGiftAmount` = minimum donation amount to earn that addition. Example alongside: a Rp10,000 donation adds 300 seconds (5 minutes). |

💡 How time gets added: viewer donates → Tako computes it against the rules in "Time Addition Rules" → `{{timerEndAt}}` moves back by that amount → `{{timerAddedSeconds}}` & `{{timerAdded}}` fill in with the amount just added. So if the rule is Rp10,000 = 1 minute and someone donates Rp10,000, `{{timerAdded}}` becomes `+01:00`, perfect for a "time added!" animation.

**Example Code:**

```html
<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; }
    .added { height: 1.35rem; font-size: 1.0625rem; font-weight: 700; color: #2f7d4f; opacity: 0; transition: opacity 0.25s ease-out; }
    .added.minus { color: #9c3b2e; }
    .added.show { opacity: 1; }
  </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" id="countdown">--:--</div>
    <div class="added" id="added">{{timerAdded}}</div>
  </div>

  <script>
    const endAt = {{timerEndAt}};
    const addedMs = {{timerAddedMs}};
    const isEnabled = "{{isEnabled}}" === "true";

    const countdown = document.getElementById("countdown");
    const added = document.getElementById("added");

    document.getElementById("status-el").textContent = isEnabled ? "Active" : "Inactive";

    const pad = (n) => String(n).padStart(2, "0");

    const tick = () => {
      const totalSeconds = endAt === 0 ? 0 : Math.max(0, Math.round((endAt - Date.now()) / 1000));
      const hours = Math.floor(totalSeconds / 3600);
      const minutes = Math.floor((totalSeconds % 3600) / 60);
      const seconds = totalSeconds % 60;

      countdown.textContent = hours > 0 ? pad(hours) + ":" + pad(minutes) + ":" + pad(seconds) : pad(minutes) + ":" + pad(seconds);
    };

    tick();
    setInterval(tick, 250);

    if (addedMs !== 0) {
      if (addedMs < 0) added.classList.add("minus");
      added.classList.add("show");
      setTimeout(() => added.classList.remove("show"), 1800);
    }
  </script>
</body>
</html>
```

This example never touches `{{timeLeft}}` at all, so the overlay renders exactly once: a `setInterval` inside the iframe handles the countdown itself. Your animations/transitions stay safe from resetting every second. If you'd rather keep it simple and don't mind the overlay re-rendering every second, just swap `.countdown`'s content for `{{timeLeft}}` and drop the countdown script.

---

### 8. VIP Queue

| Variable Name | Example Value | Description |
| ---- |
| `{{title}}` | `VIP Queue` | Title from settings. |
| `{{totalEntries}}` | `7` | Total number of people in the queue. |
| `{{hiddenCount}}` | `2` | Number of entries not shown because they exceed the display limit (`displayedQueueLimit` in settings). |
| `{{showFields}}` | `"true"` or `"false"` (string) | Whether custom fields (e.g. address, note) are shown. |
| `{{visibleEntries}}` | `[{"index":1,"id":"1","name":"BudiGaming","picture":"https://example.com/profile.jpg","badges":"VIP,TopDonator","badgeItems":[{"url":"https://tako.id/images/badges/verified-creator.png","title":"Verified Creator"}],"isVerified":true,"amount":15000,"formattedAmount":"Rp15.000","fields":[{"label":"Address","value":"123 Merdeka St"}]}, ...]` | Queue list **already** cut down to the display limit (JSON), used for rendering. Each entry has `badgeItems`: an array of real badges (icon + title), used to render badges as **images**, not just text. |
| `{{entries}}` / `{{queue}}` | same structure as `visibleEntries` | List of **all** entries (JSON), not cut down to the limit. |
| `{{fields}}` | `[{"label":"Address","value":"123 Merdeka St"}, ...]` | All custom fields from every entry, flattened into one array (JSON). |

**Example Code:**

```html
<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}}` / `{{songTitle}}` | `Song Title` | Title of the song currently playing. |
| `{{songArtist}}` | `Artist Name` | Artist of the song currently playing. |
| `{{songCoverArtUrl}}` | https://example.com/cover.jpg | Cover art URL of the song currently playing. |
| `{{requesterName}}` | `BudiGaming` | Name of the person who requested the song. |
| `{{requesterPicture}}` | https://example.com/profile.jpg | Requester's profile picture URL. |
| `{{requesterBadges}}` | `VIP,TopDonator` | Requester's badge names (names only, comma-separated). **No icon URL** from the system for this "Now Playing" row. Fill in `BADGE_ICON_URLS` manually in the script if you want it shown as an image; the code below already has the 6 official Tako system badges filled in, see the table under Alert (§1). |
| `{{isRequesterVerified}}` | `true` or `false` | Requester's verification status. |
| `{{duration}}` | `215` (seconds) | Song duration (raw number). |
| `{{formattedDuration}}` | `03:35` | Formatted song duration. |
| `{{elapsed}}` | `42` (seconds) | Elapsed playback time (raw number). |
| `{{formattedElapsed}}` | `00:42` | Formatted elapsed playback time. |
| `{{progressPercent}}` | `19` | Song progress percentage (0-100), use it to build your own progress bar. |
| `{{isPlaying}}` | `true` or `false` | Whether the song is currently playing. |
| `{{isPaused}}` | `true` or `false` | Whether the song is currently paused. |
| `{{isInGap}}` | `true` or `false` | Whether it's currently in a gap between songs. |
| `{{gapRemaining}}` | `3` (seconds) | Time left in the gap between songs. |
| `{{gapText}}` | `Next Song At 00:03` | Status text for the gap between songs. |
| `{{error}}` | `Song failed to play` | Error message if any, empty if none. |
| `{{nowPlaying}}` | `{"id":"1","title":"Song Title","artist":"Artist Name","coverArtUrl":"...","duration":215,"formattedDuration":"03:35","sender":{"name":"BudiGaming"},...}` | Object (JSON) of the song currently playing, `null` if none. |
| `{{queue}}` / `{{queues}}` | `[{"id":"2","title":"Queued Song","artist":"Another Artist","duration":180,"senderName":"Another Supporter","senderBadges":"VIP,TopDonator","senderBadgeItems":[{"url":"https://tako.id/images/badges/verified-creator.png","title":"Verified Creator"}],...}, ...]` | List of upcoming songs in the queue (JSON). Each item has `senderBadgeItems`: an array of real badges (icon + title) for that song's requester, which genuinely can be rendered as an **image**. |

**Example Code:**

```html
<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

1. **Log into your Creator Dashboard**
2. **Find the "Overlays" menu on the left**
3. **Pick an overlay** (e.g. Alert)
4. **Under "Theme", switch to "Custom HTML"**.
5. Copy-paste the code you built into the textarea that appears.
6. **Check the preview** at the top.

${youtube}[How to Make Custom Overlay in Tako!](8PEOMWGBN4w)
