

Game UI toolkit for server-rendered browser games
Tooltips, timers, inventory grids, sound, modals, and a DOM-diffing fetch engine: everything a game UI needs, nothing it doesn't.
No framework. No build step. No virtual DOM. Just <script> tags and data attributes.
Thryft Hold is a small idle game in plain HTML. The stores tick, build buttons light up when you can afford them, the longship counts down home and its cargo lands in a drag-and-drop hold: all eight modules, wired with data attributes and one short script. It keeps working while you're away.
Open it on its own · read the game script · or the stand-in server that renders its HTML
Frameworks ship hundreds of kilobytes of runtime, require build pipelines, and force you to manage state for what should be simple UI updates. Server-rendered games deserve leaner tools.
The heavy way
<!-- With a framework -->
<!-- 1. Install 200KB+ of runtime
2. Set up a build pipeline
3. Manage component state
4. Wire up reactivity
5. Debug hydration mismatches
6. Ship a bundle to the client -->
<!-- All to update a number on screen. -->The thrifty way
<!-- With Thryft -->
<div data-fetch="/api/resources"
data-interval="30000"
data-swap="inner">
<span class="gold">1,234</span>
<span class="wood">567</span>
</div>
<!-- "1,234" → "1,235" patches one text node.
No build step. No state management.
Just HTML and a 4KB script. -->~20KB
Full bundle
6
Standalone modules
0
Dependencies
0
Build step required
Load individually or use thryft.js for the combined bundle. Each module works standalone.
Data-bound HTML templates. Define <template> elements with data-bind attributes, stamp instances with one call.
<template id="tmpl-user-card">
<div class="user-card">
<img data-bind="src:avatar" />
<strong data-bind="text:name"></strong>
</div>
</template>
<script>
var card = Thryft.tmpl('tmpl-user-card', {
avatar: '/img/rob.png',
name: 'Rob'
});
document.body.appendChild(card);
</script>DOM-diffing HTML swap engine. Fetches server-rendered HTML and patches only what changed: no flicker, no animation interruption.
<!-- Poll every 30s, diff changes into DOM -->
<div data-fetch="/api/resources"
data-interval="30000"
data-swap="inner">
...server-rendered content...
</div>
<!-- Load once on page load -->
<div data-fetch="/api/activity"
data-trigger="load"
data-swap="inner">
</div>
<!-- Click to load into a target -->
<button data-fetch="/api/inventory"
data-target="#modal"
data-swap="inner">
Open Inventory
</button>Viewport-aware tooltip positioning. Anchored above/below triggers or following the cursor. Dynamic content via callbacks.
<!-- Anchored tooltip -->
<span class="has-tooltip">
Iron Sword
<div class="tooltip-box">
<strong>Iron Sword</strong>
<p>A sturdy blade. +12 Attack.</p>
</div>
</span>
<!-- Cursor-following with dynamic fill -->
<span data-tooltip="ship-tip"
data-tooltip-fill="fillShipInfo">
HMS Victory
</span>
<div id="ship-tip" class="tooltip-box">...</div>Countdown timers, progress bar fills, and resource interpolation. Ticks values up smoothly between server polls.
<!-- Countdown timer -->
<span data-timer
data-completes-at="2026-08-21T14:30:00Z">
</span>
<!-- Auto-reload + sound on completion -->
<span data-timer
data-completes-at="..."
data-reload-on-complete
data-sound="bell">
</span>
<!-- Progress bar fill -->
<div data-timer-fill
data-started-at="..."
data-completes-at="...">
</div>
<!-- Resource ticking between polls -->
<div data-timer-resource
data-current="1234" data-rate="50"
data-cap="5000">
<span data-timer-resource-value></span>
</div>Audio preloading on first interaction, playback API, and localStorage mute toggle. Integrates with timer.js completions.
// Register sounds once
Thryft.sound.register({
bell: { src: '/audio/bell.mp3', vol: 0.4 },
fanfare: { src: '/audio/fanfare.mp3', vol: 0.5 },
cannon: { src: '/audio/cannon.mp3', vol: 0.6 },
});
// Play
Thryft.sound.play('bell');
// Mute toggle (backed by localStorage)
Thryft.sound.toggle();
Thryft.sound.isEnabled();Lightbox/overlay system with stacking, Escape to close, click-outside dismiss, and scroll lock. Fetch content from server.
<!-- Fetch + open -->
<button data-modal-fetch="/api/news"
data-modal-target="#modal">
News
</button>
<!-- Toggle existing content -->
<button data-modal-toggle="#settings">
Settings
</button>
<!-- Close button -->
<button data-modal-close>×</button>
<!-- Modal structure -->
<div id="modal" class="thryft-modal">
<div class="thryft-modal-content">
...
</div>
</div>Drag-to-reorder grid with right-click context menu, touch support, and server-side reorder persistence.
<div data-inventory
data-reorder-url="/api/inventory/reorder">
<div class="inv-cell"
data-item-key="iron_ore"
data-name="Iron Ore"
data-quantity="24"
data-max-stack="99"
data-actions="track,open"
draggable="true">
<img src="/icons/iron_ore.png">
</div>
<div class="inv-cell"></div>
</div>
<script>
document.addEventListener(
'thryft:inventory-action', (e) => {
// { action: 'open', key: 'chest', cell }
});
</script>One script tag (served from GitHub by jsDelivr)
<script src="https://cdn.jsdelivr.net/gh/loxleygames/thryft@v0.2.0/dist/thryft.min.js"></script>With a bundler
Or just download and self-host
<script src="/js/thryft.js"></script>
<!-- Or load only what you need -->
<script src="/js/thryft/fetch.js"></script>
<script src="/js/thryft/timer.js"></script>
<script src="/js/thryft/sound.js"></script>Extracted from patterns that proved themselves in production.
Naval idle MMO. Server-rendered with Express + EJS. Uses all six modules.
dunmast.com
Dwarf mining idle game. Same server-rendered stack. Uses fetch, tooltip, timer, inventory.
In development
Part of the Loxley Games open source toolkit