StreamElements Overlay

Timer Overlay Setup

Step-by-step guide for installing a fully controlled StreamElements timer with buttons and animated 7TV text. First remove everything in HTML, CSS, JS, and Fields, and dont remove the Data fields.

Step 1 — HTML (Widget Code)

Paste this entire block into the HTML tab.

<!DOCTYPE html>
<html>
<head>
<style>
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700&display=swap');

:root {
  --text-color: {{textColor}};
  --font-size: {{fontSize}}px;
  --shadow-color: {{shadowColor}};
  --shadow-blur: {{shadowBlurRadius}}px;
}

html, body {
  margin: 0;
  padding: 0;
  background: transparent;
}

#timer {
  font-family: 'Nunito', sans-serif;
  font-size: calc(var(--font-size) * 1.15);
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1.05;
  user-select: none;
  text-align: center;

  color: var(--text-color);
  text-shadow:
    0 2px 0 rgba(0,0,0,0.4),
    0 0 var(--shadow-blur) var(--shadow-color);

  background-size: cover;
  background-position: center;
  background-repeat: repeat;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: currentColor;
}
</style>
</head>

<body>
  <div id="timer">00:00:00</div>

<script>
const pad2 = n => String(Math.floor(n)).padStart(2, '0');

function parseHHMMSS(s) {
  if (!s) return 0;
  const p = s.split(':').map(v => parseInt(v, 10) || 0);
  if (p.length === 3) return p[0]*3600 + p[1]*60 + p[2];
  if (p.length === 2) return p[0]*60 + p[1];
  return p[0];
}

function format(sec) {
  sec = Math.max(0, Math.floor(sec));
  return `${pad2(sec/3600)}:${pad2((sec%3600)/60)}:${pad2(sec%60)}`;
}

let elTimer;
let defaultStartSeconds = 0;
let endAtMs = 0;
let tick = null;
let paused = true;
let pausedAt = 0;

function render() {
  let secs;
  if (paused && !pausedAt) {
    secs = defaultStartSeconds;
  } else {
    secs = Math.max(0, Math.round((endAtMs - Date.now()) / 1000));
  }
  elTimer.textContent = format(secs);
}

function startTick() {
  clearInterval(tick);
  tick = setInterval(render, 250);
}

function startTimer() {
  endAtMs = Date.now() + defaultStartSeconds * 1000;
  paused = false;
  pausedAt = 0;
  startTick();
  render();
}

function pauseTimer() {
  if (!paused) {
    paused = true;
    pausedAt = Date.now();
    clearInterval(tick);
  } else {
    paused = false;
    endAtMs += Date.now() - pausedAt;
    pausedAt = 0;
    startTick();
  }
  render();
}

function resetTimer() {
  paused = true;
  pausedAt = 0;
  clearInterval(tick);
  render();
}

function applyGifFill(url) {
  if (url && url.trim() !== "") {
    elTimer.style.backgroundImage = `url("${url}")`;
    elTimer.style.color = "transparent";
    elTimer.style.webkitTextFillColor = "transparent";
  } else {
    elTimer.style.backgroundImage = "none";
    elTimer.style.color = getComputedStyle(document.documentElement)
      .getPropertyValue('--text-color');
    elTimer.style.webkitTextFillColor = "currentColor";
  }
}

window.addEventListener('onWidgetLoad', obj => {
  const f = obj.detail.fieldData;
  elTimer = document.getElementById('timer');
  defaultStartSeconds = parseHHMMSS(f.startTime || "01:00:00");
  applyGifFill(f.gifFill);
  render();
});

window.addEventListener('onWidgetFieldChanged', obj => {
  if (obj.detail.field === 'startTime') {
    defaultStartSeconds = parseHHMMSS(obj.detail.value);
    resetTimer();
  }
  if (obj.detail.field === 'gifFill') {
    applyGifFill(obj.detail.value);
  }
});

window.addEventListener('onEventReceived', obj => {
  const ev = obj.detail?.event;
  if (!ev) return;

  if (ev.listener === 'widget-button') {
    if (ev.field === 'startBtn') startTimer();
    if (ev.field === 'pauseBtn') pauseTimer();
    if (ev.field === 'resetBtn') resetTimer();
    return;
  }

  if (ev.listener !== 'message') return;

  const msg = ev.data.text.trim();
  const tags = ev.data.tags || {};
  const isBroadcaster = !!tags.badges?.broadcaster;
  const isMod = tags.mod === "1";

  if (isBroadcaster) {
    if (msg === "!start") startTimer();
    if (msg === "!pause") pauseTimer();
    if (msg === "!reset") resetTimer();
  }

  if (isMod && msg.startsWith("!paint")) {
    const url = msg.replace("!paint", "").trim();
    applyGifFill(url);
  }
});
</script>
</body>
</html>

Step 2 — Fields (JSON)

Paste this into the Fields tab.

{
  "startTime": {
    "type": "text",
    "label": "Starting Time (HH:MM:SS)",
    "value": "01:00:00"
  },
  "gifFill": {
    "type": "text",
    "label": "GIF Text Fill URL (optional)",
    "value": "",
    "placeholder": "https://example.com/text.gif"
  },
  "textColor": {
    "type": "colorpicker",
    "label": "Text Color",
    "value": "#FFD85E"
  },
  "fontSize": {
    "type": "number",
    "label": "Font Size",
    "value": 96,
    "min": 16,
    "max": 400
  },
  "shadowColor": {
    "type": "colorpicker",
    "label": "Shadow Color",
    "value": "#000000"
  },
  "shadowBlurRadius": {
    "type": "number",
    "label": "Shadow Blur",
    "value": 16,
    "min": 0,
    "max": 50
  },
  "startBtn": {
    "type": "button",
    "label": "Start Timer",
    "hidden": true
  },
  "pauseBtn": {
    "type": "button",
    "label": "Pause / Resume",
    "hidden": true
  },
  "resetBtn": {
    "type": "button",
    "label": "Reset Timer",
    "hidden": true
  }
}