Skip to content

Safe viewer text

Everything a viewer writes (their name, their message, a cheer’s text) is controlled by someone else. If you draw it as HTML, someone can send <img src=x onerror=…> and end up inside your overlay. That is why the validator does not allow innerHTML (js_forbidden_html) and, in the run test, it sends a message with HTML and checks that it does not turn into elements (runtime_injection).

// Wrong: the message HTML gets interpreted
box.innerHTML = `<b>${user}</b>: ${message}`;
script.js
const app = document.querySelector("#app");
Widget.on("Twitch.ChatMessage", ({ user, message }) => {
const line = document.createElement("div");
const name = document.createElement("b");
Widget.dom.text(name, user);
const text = document.createElement("span");
Widget.dom.text(text, message);
line.append(name, ": ", text);
app.replaceChildren(line);
});
manifest.json
{
"events": ["Twitch.ChatMessage"],
"strings": { "es": {}, "en": {} },
"menu": { "sections": [] }
}
index.html
<div id="app"></div>
  • createElement and append: you build the structure yourself, element by element. What you add as text is never HTML.
  • Widget.dom.text: sets a value with textContent. Use it for names, amounts and anything that comes from the event.
  • Widget.dom.renderMessage: for a message with emotes. It takes message.parts and returns a fragment with emotes as images and the rest as text.
  • Widget.dom.image: an image only if its URL is from an allowed CDN (emotes, badges, cheers); otherwise it returns null. Check the result before adding it.

The widget’s fixed texts (“Thanks for the follow”) go in strings and are read with Widget.t. If you mix in variables, pass them as the second argument: Widget.t("follow", { name: user }). The value is substituted as text, and the result is drawn with Widget.dom.text.