======================================================================== CLOUD BUNNY CHAT · FREE StreamElements custom widget by LumentoStudio The easy way: install it in one click from lumentostudio.com/free/. By hand: create a Custom Widget and paste each block into the matching tab (HTML / CSS / JS / Fields). Widget size in the overlay: 440 x 620 px (use the Size setting to scale). ======================================================================== === HTML ===
=== CSS === /* ── Cloud Bunny · free chat box by LumentoStudio ───────────────────── Cream cloud bubbles with puffy tops, sky-blue name tags. Roles by shape: host = bunny ears + crown, mod = shield, VIP = gem, sub = heart, viewer = plain outlined tag. Cloud cream 60 · sky blue 30 · berry pink 10, plum ink. Colours marked (field) are overwritten from the widget settings. */ html, body { margin: 0; padding: 0; background: transparent; overflow: hidden; } .cb { /* colour (field) */ --cloud: #fffaf2; --ink: #463a5c; --sky-deep: #4e8fd0; --berry: #ff7aa2; /* fixed */ --vip: #8d6be0; --sub: #f0884e; --edge-shadow: #1d2b4a40; --font-name: Coiny, "Arial Rounded MT Bold", sans-serif; --font-msg: Mali, "Comic Sans MS", sans-serif; --ease-pop: cubic-bezier(.34, 1.7, .5, 1); --ease-drift: cubic-bezier(.45, 0, .55, 1); --size: 1; zoom: var(--size); box-sizing: border-box; position: absolute; left: 0; top: 0; width: calc(100vw / var(--size)); height: calc(100vh / var(--size)); display: flex; flex-direction: column; justify-content: flex-end; align-items: flex-start; gap: 16px; padding: 26px 10px 28px; overflow: hidden; -webkit-mask-image: linear-gradient(#0000 0, #000 64px); mask-image: linear-gradient(#0000 0, #000 64px); } .cb * { box-sizing: border-box; } .cb-msg { position: relative; flex: none; max-width: 100%; padding-top: 17px; transform-origin: 12% 100%; } .cb-msg.in { animation: cbPop .62s var(--ease-pop); } .cb-msg.out { animation: cbDrift .9s var(--ease-drift) forwards; } @keyframes cbPop { 0% { transform: scale(.6) translateY(14px); opacity: .55 } 60% { transform: scale(1.04, .97) } 100% { transform: none; opacity: 1 } } @keyframes cbDrift { to { transform: translateY(-26px) scale(.96); opacity: 0 } } /* name tag: solid pill, tilted, sits on the cloud's edge */ .cb-tag { position: absolute; top: 0; left: 14px; z-index: 2; display: inline-flex; align-items: center; gap: 6px; max-width: calc(100% - 28px); padding: 4px 11px 3px 9px; border-radius: 999px; background: var(--role, var(--sky-deep)); color: #fff; font: 15px/1.15 var(--font-name); letter-spacing: .01em; transform: rotate(-3deg); box-shadow: 0 2px 0 #0002; white-space: nowrap; } .cb-tag b { font-weight: 400; overflow: hidden; text-overflow: ellipsis; } .cb-tag svg { width: 15px; height: 15px; fill: #fff; flex: none; } .cb-msg.viewer .cb-tag { background: var(--cloud); color: var(--ink); box-shadow: 0 0 0 2px var(--sky-deep); } .cb-msg.host { --role: var(--berry); } .cb-msg.mod { --role: var(--sky-deep); } .cb-msg.vip { --role: var(--vip); } .cb-msg.sub { --role: var(--sub); } .cb.no-roles .cb-tag svg { display: none; } .cb.no-roles .cb-msg:not(.viewer) .cb-tag { background: var(--sky-deep); } /* cloud bubble: cream body + three puffs breaking the top edge */ .cb-bubble { position: relative; z-index: 1; min-width: 168px; background: var(--cloud); color: var(--ink); border-radius: 22px 22px 22px 8px; padding: 16px 18px 12px; font: 600 16.5px/1.4 var(--font-msg); box-shadow: 0 2px 0 var(--sky-deep), 0 8px 18px var(--edge-shadow); overflow-wrap: anywhere; } .cb-bubble::before { content: ""; position: absolute; right: 18px; top: -15px; width: 104px; height: 24px; background: radial-gradient(circle at 78px 24px, var(--cloud) 19.5px, #0000 20px), radial-gradient(circle at 46px 24px, var(--cloud) 14.5px, #0000 15px), radial-gradient(circle at 20px 24px, var(--cloud) 10.5px, #0000 11px); filter: drop-shadow(0 -1px 0 #1d2b4a1c); } .cb-msg.alt .cb-bubble::before { transform: scaleX(-1); } .cb-bubble img { height: 1.6em; width: auto; vertical-align: -.45em; margin: 0 1px; } .cb-msg.host .cb-bubble::before { display: none; } /* host: bunny ears behind the cloud */ .cb-ears { position: absolute; right: 30px; top: -22px; width: 60px; height: 52px; z-index: 0; } .cb-ears i { position: absolute; bottom: 0; width: 23px; height: 50px; border-radius: 50% 50% 45% 45% / 62% 62% 38% 38%; background: var(--cloud); box-shadow: 0 2px 6px var(--edge-shadow); transform-origin: 50% 100%; } .cb-ears i::after { content: ""; position: absolute; inset: 7px 6px 12px; border-radius: inherit; background: var(--berry); opacity: .8; } .cb-ears i:first-child { left: 2px; transform: rotate(-12deg); } .cb-ears i:last-child { right: 2px; transform: rotate(14deg); animation: cbEar 3.4s var(--ease-drift) infinite; } .cb-msg.in .cb-ears i:first-child { animation: cbEarIn .7s var(--ease-pop); } @keyframes cbEar { 0%, 80%, 100% { transform: rotate(14deg) } 88% { transform: rotate(30deg) } } @keyframes cbEarIn { from { transform: rotate(-12deg) scaleY(.3) } } .cb.no-ears .cb-ears { display: none; } .cb.no-ears .cb-msg.host .cb-bubble::before { display: block; } /* the small credit, a tiny cloud tag at the bottom (can be switched off) */ .cb-credit { position: absolute; right: 12px; bottom: 0; font: 600 10px/1 var(--font-msg); letter-spacing: .03em; color: var(--ink); background: var(--cloud); padding: 3px 8px 4px; border-radius: 8px 8px 0 0; opacity: .82; } .cb.no-credit .cb-credit { display: none; } === JS === /* Cloud Bunny · free chat box by LumentoStudio — platform-free render engine. CloudChat.init(rootEl, { max, lifetime, roles, ears, credit, size, colors: { cloud, ink, accent, host } }) CloudChat.add({ id, user, name, role: "host"|"mod"|"vip"|"sub"|"viewer", text, emotes: [{ name, url }] }) CloudChat.removeId(msgId) · CloudChat.removeUser(userId) · CloudChat.demo() */ (function (root) { const ICON = { host: '', mod: '', vip: '', sub: '' }; const esc = (s) => String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); let box, opt = { max: 6, lifetime: 0, roles: true, ears: true }, count = 0; // message text → safe HTML, emote names replaced by images (whole words only) function textHTML(text, emotes) { const map = {}; (emotes || []).forEach((e) => { if (e && e.name && /^https:\/\//.test(e.url || "")) map[e.name] = e.url; }); return String(text).split(/(\s+)/).map((w) => map[w] ? `${esc(w)}` : esc(w)).join(""); } function leave(el) { if (!el || el.classList.contains("out")) return; el.classList.remove("in"); el.classList.add("out"); setTimeout(() => el.remove(), 900); } const CloudChat = { textHTML, init(rootEl, o) { opt = Object.assign(opt, o || {}); box = rootEl; box.classList.add("cb"); box.innerHTML = '
widget by LumentoStudio
'; const c = opt.colors || {}, s = box.style; if (c.cloud) s.setProperty("--cloud", c.cloud); if (c.ink) s.setProperty("--ink", c.ink); if (c.accent) s.setProperty("--sky-deep", c.accent); if (c.host) s.setProperty("--berry", c.host); s.setProperty("--size", Math.min(3, Math.max(.4, +opt.size || 1))); box.classList.toggle("no-roles", opt.roles === false); box.classList.toggle("no-ears", opt.ears === false); box.classList.toggle("no-credit", opt.credit === false); }, add(m) { if (!box || !m || !String(m.text || "").trim()) return; const role = ICON[m.role] || m.role === "viewer" ? m.role : "viewer"; const el = document.createElement("div"); el.className = `cb-msg ${role} in` + (count++ % 2 ? " alt" : ""); if (m.id) el.dataset.id = m.id; if (m.user) el.dataset.user = m.user; el.innerHTML = (role === "host" ? '
' : "") + `${ICON[role] ? `` : ""}${esc(m.name || "")}` + `
${textHTML(m.text, m.emotes)}
`; box.insertBefore(el, box.querySelector(".cb-credit")); // resting state is fully visible; the class only layers the entrance on top setTimeout(() => el.classList.remove("in"), 700); if (+opt.lifetime > 0) setTimeout(() => leave(el), opt.lifetime * 1000); const live = box.querySelectorAll(".cb-msg:not(.out)"); for (let i = 0; i < live.length - Math.max(1, +opt.max || 6); i++) leave(live[i]); }, removeId(id) { if (box && id) box.querySelectorAll(".cb-msg").forEach((el) => { if (el.dataset.id === String(id)) el.remove(); }); }, removeUser(u) { if (box && u) box.querySelectorAll(".cb-msg").forEach((el) => { if (el.dataset.user === String(u)) el.remove(); }); }, demo() { const D = [ ["viewer", "pebble_tea", "just made tea, perfect timing 🍵"], ["mod", "NoodleMod", "remember to be kind in chat! ❤️"], ["host", "MochiMallow", "hiii welcome in everyone ☁️ cozy farming stream today"], ["sub", "sunnybun", "the bunny ears on your messages 😂 so cute"], ["vip", "LunaDrops", "your farm is looking so pretty today 🔥"], ["viewer", "quietfox", "first time here, love the vibes 👍"], ["host", "MochiMallow", "welcome quietfox!! grab a blanket"], ["sub", "berrybiscuit", "can we name the new chicken Biscuit"], ["viewer", "mint_cloud", "the rain sounds in the background ❤️"], ["mod", "NoodleMod", "the schedule is in the panels below 👍"] ]; let i = 0; const next = () => { const [role, name, text] = D[i++ % D.length]; CloudChat.add({ role, name, text }); }; for (let k = 0; k < 4; k++) next(); return setInterval(next, 2400); } }; root.CloudChat = CloudChat; })(window); /* ── StreamElements adapter ── roles from data.badges */ (function () { let F = {}; const ignored = new Set(); const roleOf = (badges) => { const b = (badges || []).map((x) => x.type); if (b.includes("broadcaster")) return "host"; if (b.includes("moderator")) return "mod"; if (b.includes("vip")) return "vip"; if (b.includes("subscriber") || b.includes("founder")) return "sub"; return "viewer"; }; window.addEventListener("onWidgetLoad", (obj) => { F = obj.detail.fieldData || {}; String(F.ignoredUsers || "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean).forEach((u) => ignored.add(u)); CloudChat.init(document.getElementById("chat"), { max: +F.maxMessages || 6, lifetime: +F.hideAfter || 0, size: +F.size || 1, roles: F.showRoles !== false, ears: F.showEars !== false, credit: F.showCredit !== false, colors: { cloud: F.bubbleColor, ink: F.textColor, accent: F.accentColor, host: F.hostColor } }); }); window.addEventListener("onEventReceived", (obj) => { const { listener, event } = obj.detail || {}; if (listener === "delete-message") { CloudChat.removeId(event && event.msgId); return; } if (listener === "delete-messages") { CloudChat.removeUser(event && event.userId); return; } if (listener !== "message" || !event || !event.data) return; const d = event.data; if (ignored.has(String(d.nick || "").toLowerCase())) return; if (F.hideCommands !== false && String(d.text || "").trim().startsWith("!")) return; const emotes = (d.emotes || []).map((e) => ({ name: e.name, url: e.urls && (e.urls["2"] || e.urls["1"]) })); CloudChat.add({ id: d.msgId, user: d.userId, name: d.displayName || d.nick || "", role: roleOf(d.badges), text: d.text || "", emotes }); }); })(); === FIELDS === { "maxMessages": { "type": "number", "label": "Messages on screen", "value": 6, "min": 1, "max": 20, "step": 1, "group": "Chat" }, "hideAfter": { "type": "number", "label": "Hide messages after (seconds, 0 = never)", "value": 0, "min": 0, "step": 1, "group": "Chat" }, "hideCommands": { "type": "checkbox", "label": "Hide messages starting with !", "value": true, "group": "Chat" }, "ignoredUsers": { "type": "text", "label": "Hide these users (comma separated)", "value": "streamelements, nightbot", "group": "Chat" }, "showRoles": { "type": "checkbox", "label": "Show role icons (host, mod, VIP, sub)", "value": true, "group": "Look" }, "showEars": { "type": "checkbox", "label": "Bunny ears on your own messages", "value": true, "group": "Look" }, "bubbleColor": { "type": "colorpicker", "label": "Bubble colour", "value": "#fffaf2", "group": "Look" }, "textColor": { "type": "colorpicker", "label": "Text colour", "value": "#463a5c", "group": "Look" }, "accentColor": { "type": "colorpicker", "label": "Name tag and mod colour", "value": "#4e8fd0", "group": "Look" }, "hostColor": { "type": "colorpicker", "label": "Your name tag colour", "value": "#ff7aa2", "group": "Look" }, "size": { "type": "slider", "label": "Size", "value": 1, "min": 0.6, "max": 2, "step": 0.05, "group": "Look" }, "showCredit": { "type": "checkbox", "label": "Show the small 'widget by LumentoStudio' credit", "value": true, "group": "Look" } }