Thryft Logo

Thryft

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.

Your Game Doesn't Need a Framework

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

6 Modules, Zero Dependencies

Load individually or use thryft.js for the combined bundle. Each module works standalone.

fetch.js

~4KB

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>

tooltip.js

~3KB

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>

timer.js

~3KB

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>

sound.js

~2KB

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();

modal.js

~3KB

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>

inventory.js

~5KB

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>

Get Started

npm

$ npm install thryft

CDN

<script src="https://unpkg.com/thryft/dist/thryft.min.js"></script>

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>

Built for Real Games

Extracted from patterns that proved themselves in production.

Dunmast

Naval idle MMO. Server-rendered with Express + EJS. Uses all six modules.

dunmast.com

Thrynd

Dwarf mining idle game. Same server-rendered stack. Uses fetch, tooltip, timer, inventory.

In development

Part of the Loxley Games open source toolkit