========================================================================
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)).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] ? `${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"
}
}