A lightweight Discord-themed UI library for zombs.io userscripts.
Drop it into any userscript to get a polished, tabbed settings panel that matches the game's aesthetic.
- Sidebar navigation with tabs, built-in icons, and badge counters
- Global fuzzy search bar across all settings (emoji-safe)
- Collapsible sections to organise dense tabs
- Toggles, sliders, buttons, button rows, dropdowns, text inputs, number inputs, filterable search lists
- Color pickers, keybind capture, radio groups, status tags, progress bars
- Version switcher with automatic header badge sync
- Top-center popup system — toasts (max 5 stacked), confirmation dialogs, input prompts
- Live popup handles — update text, swap type, or dismiss programmatically (e.g. countdowns)
- Web Cache API persistence — any component can save its state across page reloads via
opts.persist - Minimize / restore via header button or a configurable keyboard shortcut
- Zero dependencies — pure vanilla JS + CSS
Load as a @require in Tampermonkey / Violentmonkey:
// @require https://raw.githubusercontent.com/TropicalBanana2/ZOUI/refs/heads/main/zoui.jsOr copy zoui.js directly into the top of your userscript before any code that uses it.
const ui = new ZOUI(document.querySelector("#hud-menu-settings"), "My Script", "1.0.0");
ui.setToggleKey("Insert"); // Press Insert to minimize / restore
const general = ui.addTab("General");
ui.addHeader(general, "Combat");
ui.addToggle(general, "Auto Attack", false, enabled => { /* ... */ }, { persist: "myScript-autoAttack" });
ui.addSlider(general, "Attack Speed", 1, 10, 5, value => { /* ... */ });
// Popup — returns a live handle
const t = ui.toast("Wave incoming!", "warning", 0);
setTimeout(() => t.dismiss(), 5000);Any component that accepts an opts parameter can persist its value across page reloads using the Web Cache API.
Pass { persist: "uniqueKey" } as the last argument to any persistable add* method.
Keys must be unique within a script. A good pattern is "scriptName-settingName".
ui.addToggle(tab, "Night Mode", false, v => applyNightMode(v), { persist: "myScript-nightMode" });
ui.addSlider(tab, "Speed", 1, 10, 5, v => setSpeed(v), { persist: "myScript-speed" });
ui.addSelect(tab, "Difficulty", options, v => setDiff(v), { persist: "myScript-difficulty" });Persistence is handled internally by ZOUICache. You never need to interact with it directly — just pass the persist key.
Internal class used by ZOUI for persistence. Can also be used standalone.
const cache = new ZOUICache("my-namespace");
// Reads are synchronous (from in-memory mirror)
cache.get("speed", 5); // returns stored value or fallback
// Writes are synchronous to memory, async to CacheStorage
cache.set("speed", 8);
// Async deletion
await cache.delete("speed");
// Async full clear
await cache.clear();
// Promise that resolves when all stored values are hydrated into memory
await cache._ready;| Member | Description |
|---|---|
get(key, fallback) |
Sync read — returns stored value or fallback |
set(key, value) |
Sync write to memory + async write to CacheStorage |
delete(key) |
Remove a single key |
clear() |
Wipe the entire namespace |
_ready |
Promise that resolves when hydration from CacheStorage is complete |
new ZOUI(container, title?, version?)| Parameter | Type | Default | Description |
|---|---|---|---|
container |
Element |
— | DOM element to mount the UI into |
title |
string |
"ZOUI" |
Title shown in the header bar |
version |
string |
"1.0.0" |
Version string shown in the header badge |
ZOUI creates its own ZOUICache instance namespaced to "zoui-" + title. The last active tab is automatically persisted and restored on the next load.
Add a tab to the sidebar. Returns the tab name used as the first argument for all add* methods.
const tab = ui.addTab("Visuals");
const tab = ui.addTab("Combat", "⚔️"); // emoji icon
const tab = ui.addTab("Player", "<svg>...</svg>"); // inline SVG
const tab = ui.addTab("Items", "https://example.com/icon.png"); // image URLBuilt-in icons are provided automatically for tabs named: Player, Combat, Visuals, Misc.
Set a notification badge counter on a sidebar tab button. Pass null to remove the badge.
ui.setTabBadge("Alerts", 3); // shows a red "3" pill on the tab
ui.setTabBadge("Alerts", null); // removes the badgeAdd an uppercase section label.
ui.addHeader(tab, "Movement");Add a thin horizontal rule.
ui.addDivider(tab);Add a plain text block, or a styled blue info callout when tip is true.
ui.addText(tab, "Some descriptive text.");
ui.addText(tab, "This feature is experimental.", true);Add an on/off toggle row.
ui.addToggle(tab, "Show FPS", false, enabled => {
console.log("Toggle:", enabled);
});
// With persistence:
ui.addToggle(tab, "Night Mode", false, v => applyNightMode(v), { persist: "myScript-nightMode" });Add a range slider.
ui.addSlider(tab, "FOV", 60, 120, 90, value => {
console.log("FOV:", value);
});
// With persistence:
ui.addSlider(tab, "Speed", 1, 10, 5, v => setSpeed(v), { persist: "myScript-speed" });Add a single button. Pass true for secondary to use the ghost style.
ui.addButton(tab, "Reset Defaults", () => resetAll());
ui.addButton(tab, "Cancel", () => close(), true);Add multiple buttons in a horizontal row. Each button is individually searchable via the global search bar.
ui.addButtonRow(tab, [
["Build", () => buildBase()],
["Record", () => recordBase(), true], // secondary style
["Delete", () => deleteBase(), true],
]);Add a text input field.
ui.addTextbox(tab, "Username", "Enter name...", value => {
console.log("Input:", value);
}, "Player1");
// With persistence:
ui.addTextbox(tab, "API Key", "Enter key...", v => setKey(v), "", { persist: "myScript-apiKey" });Add a dropdown select. Returns a SelectController for dynamic option management.
const sel = ui.addSelect(tab, "Difficulty", [
{ value: "easy", label: "Easy" },
{ value: "medium", label: "Medium" },
{ value: "hard", label: "Hard" },
], value => {
console.log("Selected:", value);
});
// With persistence:
const sel = ui.addSelect(tab, "Mode", options, v => setMode(v), { persist: "myScript-mode" });SelectController:
| Method | Description |
|---|---|
sel.addOption(value, label) |
Append a new option, returns the <option> element |
sel.removeOption(value) |
Remove an option by value |
sel.clear() |
Remove all options |
sel.getValue() |
Return the currently selected value |
sel.setValue(value) |
Programmatically select an option |
sel.value |
Readable/writable shorthand for getValue / setValue |
sel.element |
The raw <select> DOM element |
Add a filterable list where items can be clicked to select them. Returns a SearchListController for dynamic item management.
const list = ui.addSearchList(tab, "Tower Type",
["Wall", "Arrow Tower", "Cannon Tower", "Magic Tower"],
item => console.log("Selected:", item)
);
// With persistence:
const list = ui.addSearchList(tab, "Preset", presets, v => loadPreset(v), { persist: "myScript-preset" });SearchListController:
| Method | Description |
|---|---|
list.addItem(label) |
Append a new item |
list.removeItem(label) |
Remove an item by label |
list.clear() |
Remove all items |
list.getValue() |
Return the currently selected label (or null) |
list.setValue(label) |
Programmatically select an item |
list.value |
Readable/writable shorthand for getValue / setValue |
Add a row of version pill buttons. Clicking a pill highlights it and updates the header badge. Each pill is individually searchable.
ui.addVersionSwitcher(tab, "Script Version",
["1.0.0", "2.0.0", "3.0.0"],
"3.0.0",
version => console.log("Switched to:", version)
);Add a collapsible section. Returns a collKey that can be passed as the tab argument to any add* method to place content inside the section.
const coll = ui.addCollapsible(tab, "Advanced Settings");
// Add components inside the collapsible — pass collKey as first arg
ui.addToggle(coll, "Debug Mode", false, v => console.log(v));
ui.addSlider(coll, "Timeout", 0, 60, 10, v => console.log(v));
// Open/close state persisted:
const coll = ui.addCollapsible(tab, "Combat", true, { persist: "myScript-collCombat" });| Parameter | Type | Default | Description |
|---|---|---|---|
tab |
string |
— | Parent tab name |
label |
string |
— | Heading text |
open |
boolean |
true |
Initially expanded |
opts |
object |
{} |
{ persist: "key" } to save open/close state |
Add a number input field. Values are automatically clamped to [min, max] and rounded to the nearest step on change.
ui.addNumberInput(tab, "Max Towers", 0, 200, 5, 50, value => {
console.log("Max:", value);
});
// With persistence:
ui.addNumberInput(tab, "Gold Target", 0, 100000, 100, 5000, v => setGoal(v),
{ persist: "myScript-goldTarget" });Add a progress bar. Returns a handle to update it programmatically.
const bar = ui.addProgressBar(tab, "Build Progress", 0, 10);
bar.setValue(5); // 50%
bar.setMax(20); // new maximum; value is clamped if needed| Return method | Description |
|---|---|
setValue(v) |
Update the current value (clamped to [0, max]) |
setMax(m) |
Update the maximum value |
Add a keybind capture row. Click the button then press any key to rebind.
ui.addKeybind(tab, "Toggle Menu", "Insert", key => {
console.log("New key:", key);
});
// With persistence:
ui.addKeybind(tab, "Quick Build", "F2", k => bindKey(k), { persist: "myScript-quickBuild" });Add a color picker row with a clickable swatch that opens the native color chooser.
ui.addColorPicker(tab, "Accent Color", "#5865f2", color => {
document.documentElement.style.setProperty("--accent", color);
});
// With persistence + controller:
const picker = ui.addColorPicker(tab, "Theme", "#5865f2", applyTheme, { persist: "myScript-theme" });
picker.getValue(); // "#5865f2"
picker.setValue("#ff0"); // programmatically update| Return method | Description |
|---|---|
getValue() |
Return the current color string |
setValue(color) |
Programmatically set the color (does not fire callback) |
Add a radio button group.
ui.addRadioGroup(tab, "Quality",
[
{ value: "low", label: "Low — best performance" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High — best visuals" },
],
"medium",
value => console.log("Quality:", value)
);
// With persistence:
ui.addRadioGroup(tab, "Mode", options, "normal", v => applyMode(v), { persist: "myScript-mode" });Add a static status chip / colored tag.
const status = ui.addTag(tab, "Connection", "Online", "#23a559");
const ver = ui.addTag(tab, "Script", "v3.0.0", "#5865f2");
const warn = ui.addTag(tab, "Wave", "Incoming!", "#f0b232");
// Update dynamically:
status.update("Offline");
status.setColor("#ed4245");| Return method | Description |
|---|---|
update(text) |
Change the chip's display text |
setColor(hex) |
Swap the accent colour (text, background tint, border) |
Programmatically update the version badge in the header.
ui.setVersion("2.1.0");Programmatically switch to a tab by name.
ui.switchTab("Visuals");Toggle the minimized state of the panel. When minimized the body and search bar collapse, leaving only the header visible.
ui.toggleMinimize();Bind a keyboard key that calls toggleMinimize() globally. Calling again with a new key replaces the previous binding.
ui.setToggleKey("Insert"); // default
ui.setToggleKey("F9");Convenience wrappers around ZOUIPopup — see the ZOUIPopup section below for full documentation.
Can be used directly without a ZOUI instance. The ZOUI class creates one internally as ui.popup and exposes toast, confirm, and input as thin wrappers.
// Standalone usage:
const popup = new ZOUIPopup();
popup.toast("Hello!", "success");
// Via ZOUI (equivalent):
ui.toast("Hello!", "success");
// Access the internal instance directly:
ui.popup.toast("Hello!", "success");All three methods return a PopupHandle for live updates.
Show a brief auto-dismissing notification at the top-center of the screen, over the game canvas.
A maximum of 5 live toasts are displayed at once — if 5 already exist, the oldest is dismissed before the new one appears.
| Parameter | Type | Default | Description |
|---|---|---|---|
message |
string |
— | Text to display |
type |
string |
"info" |
"info" · "success" · "warning" · "error" |
duration |
number |
3000 |
ms before auto-dismiss. Pass 0 to disable. |
ui.toast("Base saved!");
ui.toast("Build complete.", "success");
ui.toast("Gold Stash not found.", "error");
ui.toast("Wave incoming!", "warning", 5000);
// No auto-dismiss — controlled manually:
const t = ui.toast("Wave in 5s", "warning", 0);Show a top-center confirmation popup. Stays visible until dismissed.
Enter confirms · Escape cancels.
ui.confirm("Delete this base?",
() => console.log("Confirmed!"),
() => console.log("Cancelled.")
);Show a top-center input popup with a text field. The field is auto-focused.
Enter confirms · Escape cancels. onConfirm receives the input value as a string.
| Parameter | Type | Default | Description |
|---|---|---|---|
message |
string |
— | Label shown above the input field |
onConfirm |
function |
— | Called with (value: string) |
onCancel |
function |
null |
Called with no args on cancel |
placeholder |
string |
"" |
Input placeholder text |
defaultValue |
string |
"" |
Pre-filled value |
ui.input("Name your base:", name => saveBase(name), null, "Enter a name...");
// Pre-filled (rename flow):
ui.input("Rename base:", newName => rename(id, newName), null, "Enter a name...", currentName);Every popup method returns a live handle with three chainable methods:
| Method | Description |
|---|---|
handle.update(message) |
Rewrite the popup's message text in place |
handle.setType(type) |
Swap the accent colour and icon ("info" · "success" · "warning" · "error") |
handle.dismiss() |
Fade out and remove (safe to call multiple times) |
All methods return the handle so calls can be chained.
// Countdown toast:
let n = 5;
const h = ui.toast(`Wave in ${n}s`, "warning", 0);
const id = setInterval(() => {
n--;
if (n <= 0) {
clearInterval(id);
h.setType("error").update("Wave is here!");
setTimeout(() => h.dismiss(), 1500);
} else {
h.update(`Wave in ${n}s`);
}
}, 1000);
// Auto-cancelling confirm:
let sec = 10;
const c = ui.confirm(`Confirm? (${sec}s)`, onYes);
const id = setInterval(() => {
sec--;
if (sec <= 0) { clearInterval(id); c.dismiss(); }
else c.update(`Confirm? (${sec}s)`);
}, 1000);MIT — free to use, modify, and distribute.