minecraft-inventory
v0.1.6
Published
A flexible, scalable React library for rendering Minecraft inventory GUIs. Designed for integration into real Minecraft clients (e.g., mineflayer bots, web clients) without using CSS `transform: scale` — all sizing is driven by CSS custom properties.
Readme
minecraft-inventory
A flexible, scalable React library for rendering Minecraft inventory GUIs. Designed for integration into real Minecraft clients (e.g., mineflayer bots, web clients) without using CSS transform: scale — all sizing is driven by CSS custom properties.
Features
- All standard inventory types (chest, furnace, crafting table, villager trades, enchanting table, brewing stand, anvil, smithing table, horse, beacon, and more)
- CSS variable–based scaling — no layout jank, no
transform: scale <img>-rendered item textures with automaticitems/→blocks/fallback (via PrismarineJS asset mirror by default)- Tooltips that follow the cursor, matching the original Minecraft style
- Full keyboard support: number keys (1-9) to swap hotbar, Q to drop, double-click to collect, scroll wheel to pick/place
- Mobile support: tap to open a context menu (take all / half / custom amount / drop)
- Optional JEI (item browser) panel on the left or right
- Bot connector layer — plugs into a mineflayer bot or any custom server connection
- Demo connector with action logging for local development
- Extendable registry — add new inventory types in one place
Quick Start (React)
import React from 'react'
import { createRoot } from 'react-dom/client'
import { InventoryGUI, createDemoConnector } from 'minecraft-inventory'
const connector = createDemoConnector({
windowType: 'chest',
windowTitle: 'My Chest',
slots: [
{ index: 0, item: { type: 265, name: 'iron_ingot', count: 32, displayName: 'Iron Ingot' } },
// ...more slots
],
})
function App() {
return (
<InventoryGUI
type="chest"
connector={connector}
scale={2}
showJEI
jeiItems={[
{ type: 1, name: 'stone', displayName: 'Stone', count: 1 },
{ type: 4, name: 'cobblestone', displayName: 'Cobblestone', count: 1 },
]}
/>
)
}
// Mount into the DOM
createRoot(document.getElementById('root')!).render(<App />)Quick Start (No React — programmatic)
If you don't use React, use the mountInventory helper:
<div id="inventory"></div>
<script type="module">
import { mountInventory, createDemoConnector } from 'minecraft-inventory'
const connector = createDemoConnector({
windowType: 'chest',
windowTitle: 'My Chest',
slots: [
{ index: 0, item: { type: 265, name: 'iron_ingot', count: 32, displayName: 'Iron Ingot' } },
],
})
const inv = mountInventory(document.getElementById('inventory'), {
type: 'chest',
connector,
scale: 2,
})
// Update props later
inv.update({ scale: 3 })
// Destroy when done
inv.destroy()
</script>Installation
npm install minecraft-inventory
# or
pnpm add minecraft-inventoryCore API
<InventoryGUI> — All-in-one component
<InventoryGUI
type="chest" // Inventory type name (see registry)
title="My Chest" // Override window title
slots={[...]} // Optional: provide slots directly
properties={{ litTime: 100 }} // Optional: progress bar data
connector={connector} // Optional: bot connector
scale={2} // Scale multiplier (default: 2)
showJEI={false} // Show item browser panel
jeiItems={[...]} // Items to show in JEI
jeiPosition="right" // 'left' | 'right'
textureBaseUrl="/assets/mc" // Override texture base URL
enableKeyboardShortcuts={true}
onClose={() => {}}
/><InventoryOverlay> — Full-screen overlay with backdrop
Renders the inventory centered on screen with a semi-transparent backdrop. Clicking outside drops the held item or closes the window.
import { InventoryOverlay, InventoryProvider, ScaleProvider, TextureProvider } from 'minecraft-inventory'
<TextureProvider>
<ScaleProvider scale={2}>
<InventoryProvider connector={connector}>
<InventoryOverlay
type="chest"
showBackdrop // default: true (50% black)
backdropColor="rgba(0,0,0,0.5)"
showJEI
jeiItems={items}
jeiPosition="right"
onClose={() => console.log('closed')}
>
{/* Extra children (e.g. notes panel) are rendered inside the centered anchor */}
</InventoryOverlay>
</InventoryProvider>
</ScaleProvider>
</TextureProvider>Manual composition
For full control, use the provider components and compose manually:
import {
TextureProvider, ScaleProvider, InventoryProvider,
InventoryWindow, Hotbar, HUD, CursorItem, JEI,
} from 'minecraft-inventory'
function MyInventory({ connector }) {
return (
<TextureProvider baseUrl="https://example.com/assets">
<ScaleProvider scale={2}>
<InventoryProvider connector={connector}>
<div style={{ display: 'flex', gap: 8 }}>
<InventoryWindow type="furnace" />
<JEI items={allItems} position="right" />
</div>
<Hotbar />
<HUD />
<CursorItem />
</InventoryProvider>
</ScaleProvider>
</TextureProvider>
)
}Scaling
Scale is driven entirely by CSS custom properties — no transform: scale. Changing the scale prop recalculates all size tokens:
| Variable | Default (scale=2) |
|---|---|
| --mc-scale | 2 |
| --mc-slot-size | 36px (18 × scale) |
| --mc-font-size | 14px (7 × scale) |
| --mc-pixel | 2px |
// Changing scale re-renders the whole GUI at new dimensions
<InventoryGUI type="chest" scale={3} />Textures
By default, item textures are fetched from the PrismarineJS Minecraft assets mirror on GitHub:
https://raw.githubusercontent.com/PrismarineJS/minecraft-assets/master/data/1.21.4/item/{name}.pngProvide name on each ItemStack to use the correct asset:
{ type: 276, name: 'diamond_sword', count: 1, displayName: 'Diamond Sword' }To use local textures (e.g., from a resource pack server):
<InventoryGUI
type="chest"
textureBaseUrl="/assets/minecraft"
// → /assets/minecraft/item/diamond_sword.png
/>For full control, pass a custom texture config overriding any or all URL resolvers:
import { TextureProvider } from 'minecraft-inventory'
// Option A — simple base URL (all textures served from your CDN/server)
<TextureProvider baseUrl="https://yourserver.com/mc-assets/1.21.4">
{/* item textures: /mc-assets/1.21.4/items/{name}.png */}
{/* block textures: /mc-assets/1.21.4/blocks/{name}.png */}
{/* GUI textures: /mc-assets/1.21.4/textures/{path}.png */}
<InventoryWindow type="chest" />
</TextureProvider>
// Option B — replace individual resolvers (config is merged with defaults)
<TextureProvider config={{
getItemTextureUrl: ({ type, name }) =>
`https://cdn.example.com/items/${name ?? type}.png`,
getBlockTextureUrl: ({ type, name }) =>
`https://cdn.example.com/blocks/${name ?? type}.png`,
// version param lets you vary GUI textures per container (1.16.4, 1.21.4, etc.)
getGuiTextureUrl: (path, version = '1.16.4') =>
`https://cdn.example.com/gui/${version}/${path}.png`,
}}>
<InventoryWindow type="furnace" />
</TextureProvider>
// Option C — completely static local assets (no CDN)
<TextureProvider config={{
baseUrl: '/assets',
getItemTextureUrl: ({ name, type }) => `/assets/items/${name ?? type}.png`,
getBlockTextureUrl: ({ name, type }) => `/assets/blocks/${name ?? type}.png`,
getGuiTextureUrl: (path) => `/assets/gui/${path}.png`,
}}>
<InventoryWindow type="chest" />
</TextureProvider>Bundled GUI textures (mc-assets) — Use the optional mc-assets package to bundle container backgrounds locally and avoid remote requests. Generate the texture map from your inventory registry, then pass the config to TextureProvider:
pnpm add mc-assets # optional peer dependencyimport { TextureProvider, InventoryOverlay } from 'minecraft-inventory'
import { localBundledTexturesConfig } from './bundledTexturesConfig'
// GUI container backgrounds (chest, furnace, etc.) load from bundled mc-assets;
// item/block textures still use the default remote URLs unless you override them.
<TextureProvider config={localBundledTexturesConfig}>
<InventoryOverlay type="chest" ... />
</TextureProvider>The generated file (src/generated/localTextures.ts) exports bundledTextureMap, allTexturePaths, and allContainerPaths. Use createBundledTexturesConfig({ remoteFallback: true, bundledTextureMap? }) to get a config with getGuiTextureUrl, setOverride(path, image), clearOverrides(), setRemoteFallback(enabled), and resetRenderedSlots(). Pass short paths to setOverride (e.g. gui/sprites/container/anvil/text_field_disabled.png) — the version prefix exists only for the texture import generator. Call resetRenderedSlots() after multiple setOverride calls to invalidate cached textures so slots re-request them. localBundledTexturesConfig is the default instance. Re-run pnpm gen:textures after changing inventory types.
Connector
The connector is the bridge between the GUI and a Minecraft server or bot.
Mineflayer connector
Use createMineflayerConnector(bot) to plug the GUI into a mineflayer bot. The connector turns slot clicks, drags, and drops into bot.clickWindow() (and plugin APIs for villager trades, enchantment table, anvil, beacon), and subscribes to mineflayer inventory events so the UI stays in sync.
Example — overlay when a container opens:
import React, { useState, useEffect } from 'react'
import { createRoot } from 'react-dom/client'
import mineflayer from 'mineflayer'
import {
InventoryProvider,
ScaleProvider,
TextureProvider,
InventoryOverlay,
createMineflayerConnector,
} from 'minecraft-inventory'
const bot = mineflayer.createBot({
host: 'localhost',
port: 25565,
username: 'InventoryViewer',
})
function App() {
const [connector, setConnector] = useState(null)
const [windowType, setWindowType] = useState(null)
const [open, setOpen] = useState(false)
useEffect(() => {
const onOpen = () => {
setConnector(createMineflayerConnector(bot))
setWindowType(bot.currentWindow?.type ?? 'generic_9x1')
setOpen(true)
}
const onClose = () => {
setOpen(false)
setConnector(null)
}
bot.on('windowOpen', onOpen)
bot.on('windowClose', onClose)
return () => {
bot.removeListener('windowOpen', onOpen)
bot.removeListener('windowClose', onClose)
}
}, [])
if (!open || !connector || !windowType) return null
return (
<TextureProvider>
<ScaleProvider scale={2}>
<InventoryProvider connector={connector}>
<InventoryOverlay
type={windowType}
onClose={() => bot.closeWindow(bot.currentWindow)}
showJEI
jeiItems={[]}
/>
</InventoryProvider>
</ScaleProvider>
</TextureProvider>
)
}
createRoot(document.getElementById('root')).render(<App />)Hotbar “open inventory” button: If you render a hotbar with the container option (e.g. mobile open-inventory button), the connector handles the open-inventory action: it calls openPlayerInventory(), which opens the player inventory GUI, or the ridden entity’s inventory (e.g. llama) when mounted. No extra wiring needed once the connector is passed to InventoryProvider.
Demo connector (for local testing)
import { createDemoConnector } from 'minecraft-inventory'
const connector = createDemoConnector({
windowType: 'crafting_table',
windowTitle: 'Crafting',
slots: [...],
onAction: (entry) => {
console.log(entry.description, entry.action)
},
})
// Update slots programmatically:
connector.updateSlots(newSlots)
connector.setHeldItem({ type: 264, name: 'diamond', count: 1 })
connector.openWindow('furnace', 'Furnace', furnaceSlots)
connector.closeWindowExternal()Custom connector
Implement the InventoryConnector interface for any other backend:
import type { InventoryConnector } from 'minecraft-inventory'
const myConnector: InventoryConnector = {
getWindowState: () => ({ windowId: 1, type: 'chest', slots: [...], heldItem: null }),
getPlayerState: () => null,
sendAction: async (action) => {
// send action to server
console.log('action', action)
},
closeWindow: () => {},
subscribe: (listener) => {
// call listener({ type: 'windowUpdate', state }) on changes
return () => {} // cleanup
},
}Keyboard Shortcuts (Desktop)
| Key | Action |
|---|---|
| Left click | Pick up all / place all |
| Right click | Pick up half / place one |
| Shift + Left click | Transfer to/from container |
| Double click | Collect all of same item type |
| 1–9 (while hovering) | Swap slot with hotbar slot N |
| Q (while hovering) | Drop one item |
| Ctrl+Q | Drop entire stack |
| Scroll up | Pick up one more (right-click equivalent) |
| Scroll down | Put one back |
| Esc | Close window (onClose callback) |
Mobile Support
On touch devices, tapping a slot with no held item opens a context menu instead of immediately picking up the item. The menu appears to the side in landscape or below in portrait.
Context menu options:
- Take All — picks up the entire stack
- Take Half — picks up half
- Take Amount… —
window.promptto enter a custom quantity - Drop — drops the stack from the slot
- Cancel
When you have a held item, tapping a slot places/transfers it (same as left-click on desktop).
JEI — Item Browser
import { JEI } from 'minecraft-inventory'
const items = [
{ type: 264, name: 'diamond', displayName: 'Diamond' },
{ type: 265, name: 'iron_ingot', displayName: 'Iron Ingot' },
// ...
]
<JEI
items={items}
position="right" // 'left' | 'right'
onItemClick={(item) => {}} // left-click handler
onItemMiddleClick={(item) => {}} // middle-click handler
/>Use the search bar to filter by display name or item name. Scroll wheel or arrow buttons to paginate.
Adding New Inventory Types
All inventory types live in a single registry file:
src/registry/inventories.ts
Add a new entry to the inventoryDefinitions object:
export const inventoryDefinitions: Record<string, InventoryTypeDefinition> = {
// ... existing types ...
my_custom_chest: {
name: 'my_custom_chest',
title: 'Custom Chest',
backgroundTexture: 'gui/container/my_custom_chest', // path under textures/
backgroundWidth: 176,
backgroundHeight: 166,
includesPlayerInventory: true,
includesHotbar: true,
slots: [
// Custom container slots (indices 0-N)
...gridSlots(0, 9, 3, 8, 18, 'container'),
// Player inventory (adjust indices to match server protocol)
...playerInv(84, 27, 54),
],
},
}Slot index conventions:
- Container slots always start at
0 - Player inventory and hotbar indices vary by window type (match the server protocol)
- Use
playerInv(yPos, invStartIndex, hotbarStartIndex)for the standard 3×9 + hotbar layout
Progress bars (for furnaces, brewing stands, etc.):
progressBars: [
{
id: 'cook_arrow',
x: 79, y: 34, width: 24, height: 16,
direction: 'right', // 'right' | 'up' | 'down' | 'left'
textureX: 176, textureY: 14, // source coords in background texture
getValue: (props) => props.cookingProgress ?? 0,
getMax: (props) => props.totalCookTime || 200,
},
],Properties (data slots from server):
properties: {
cookingProgress: { dataSlot: 2, description: 'Cook progress (0-200)' },
totalCookTime: { dataSlot: 3, description: 'Total cook time' },
},Registering at runtime (optional, for plugins/mods):
import { registerInventoryType } from 'minecraft-inventory'
registerInventoryType({
name: 'my_mod_inventory',
title: 'Mod Inventory',
backgroundTexture: 'gui/container/my_mod',
backgroundWidth: 176,
backgroundHeight: 166,
slots: [...],
})Then use it anywhere:
<InventoryGUI type="my_mod_inventory" connector={connector} />ItemStack Type
interface ItemStack {
type: number // Numeric item ID
name?: string // Snake_case name, e.g. 'diamond_sword' (used for texture URL)
count: number
metadata?: number
nbt?: Record<string, unknown>
displayName?: string
enchantments?: Array<{ name: string; level: number }>
lore?: string[]
durability?: number // Current durability value
maxDurability?: number // Max durability (renders bar when < max)
textureKey?: string // Override texture path for getItemTextureUrl
texture?: string | HTMLImageElement // Direct texture (bypasses URL lookup)
blockTexture?: BlockTextureRender // Isometric block icon from face slices
debugKey?: string
}Texture overrides (mineflayer itemMapper):
texture: string— URL or data URL, fetched and cached.texture: HTMLImageElement— Preloaded image, used directly.blockTexture: { source, top, left, right }— Composite an isometric block icon from three face slices. Each face hasslice: [x, y, w, h]in source texture pixels. Uses a pool of aux canvases (not recreated per slot).
Project Structure
src/
index.tsx # Library entry point / exports
types.ts # Core TypeScript types
registry/
index.ts # registerInventoryType / getInventoryType
inventories.ts # All built-in inventory type definitions ← ADD NEW TYPES HERE
components/
InventoryWindow/ # Main window renderer
Slot/ # Individual slot (click/drag/keyboard/mobile)
ItemCanvas/ # Canvas-based item texture rendering
Tooltip/ # Cursor-following item tooltip
JEI/ # Item browser panel
Hotbar/ # Hotbar with active slot indicator
HUD/ # XP bar
CursorItem/ # Floating item following mouse cursor
context/
InventoryContext.tsx # Shared state (held item, hover, drag)
ScaleContext.tsx # CSS variable scale provider
TextureContext.tsx # Texture URL resolver
connector/
types.ts # InventoryConnector interface
mineflayer.ts # Mineflayer bot adapter
demo.ts # Demo connector with action log
hooks/
useMobile.ts
useKeyboardShortcuts.ts
styles/
tokens.css # CSS custom property definitions
playground/
src/
App.tsx # Interactive playground / demo
mockItems.ts # Sample items for testingContributing
Running the playground
pnpm install
pnpm dev
# Opens at http://localhost:3200Adding a new inventory type
- Open
src/registry/inventories.ts - Add a new key to
inventoryDefinitions(see template above) - Test it in the playground by adding it to
INVENTORY_TYPESinplayground/src/App.tsx - Submit a PR with the new type and sample mock slots in
playground/src/mockItems.ts
Slot index reference
Each Minecraft window type has a fixed slot layout defined by the server protocol. The indices must match prismarine-windows / mineflayer slot numbering. Cross-reference with:
prismarine-windowsfor JS slot maps- Minecraft source:
net/minecraft/world/inventory/for the canonical layout
Memory leak note (Tooltip / MessageFormattedString)
If opening tooltips increases memory without returning to baseline, see docs/MEMORY_LEAK_ANALYSIS.md. The likely cause is filter:blur(2px) on obfuscated (§k) text creating retained compositor layers.
Connector protocol
When implementing a custom connector, actions have these shapes:
| Action type | Fields |
|---|---|
| click | slotIndex, button ('left'/'right'/'middle'), mode ('normal'/'shift'/'double'/'number'/'drop'), numberKey? |
| drop | slotIndex, all (boolean) |
| drag | slots (number[]), button |
| trade | tradeIndex |
| rename | text |
| enchant | enchantIndex (0/1/2) |
| beacon | primaryEffect, secondaryEffect |
| hotbar-swap | slotIndex, hotbarSlot (0-8) |
| close | — |
License
MIT
