VueHex
February 6, 2026 · View on GitHub
Description
VueHex is a fast, virtualized hex viewer/editor component for Vue 3. It can be used both for cleanly viewing and editing binary data and for efficiently handling very large datasets.
- No dependencies
- Small package (~72KB minimized / ~18KB zipped)
- Render any local or remote data
- Can handle extremely large data sizes
- Very flexible and themeable, can be as simple or as complicated as you wish

Installation
npm install vuehex
Register the plugin once to make the <VueHex> component available everywhere and load the bundled styles:
import { createApp } from "vue";
import VueHex from "vuehex";
import "vuehex/style.css";
import App from "./App.vue";
const app = createApp(App);
app.use(VueHex);
app.mount("#app");
vuehex/style.css resolves to the package's bundled stylesheet.
Quick start
It can be as simple as this
<template>
<VueHex
v-model="windowData"
/>
</template>
<script setup lang="ts">
import { ref } from "vue";
const backingFile = crypto.getRandomValues(new Uint8Array(2 ** 20));
const windowData = ref(backingFile.slice(0, 16 * 48));
</script>
Props
| Prop | Default | Description |
|---|---|---|
dataMode / data-mode | auto | Data handling mode: auto, buffer, or window. |
expandToContent / expand-to-content | false | Disables internal scrolling/virtualization and expands the component height to fit the full buffer (expects the full data in v-model). |
totalSize | modelValue.length | Total bytes available. |
bytesPerRow | 16 | Number of bytes to display per row. |
uppercase | false | Whether to display hex values in uppercase. |
nonPrintableChar | '.' | Character to display for non-printable bytes in ASCII column. |
isPrintable / renderAscii | — | Functions to customize ASCII rendering. |
theme | — | Color theme: 'dark', 'light', 'terminal', 'sunset', or 'auto'. Omit to follow prefers-color-scheme between dark and light. |
cellClassForByte | — | Function (payload: { kind: 'hex' | 'ascii'; index: number; byte: number }) => string | string[] | void for custom highlighting. |
getSelectionData | — | Function (selectionStart: number, selectionEnd: number) => Uint8Array used for clipboard copy (required when using virtual windows; auto-implemented in full-data mode). |
overscan | 2 | Number of extra rows to render above and below the viewport. |
showChunkNavigator + chunkNavigatorPlacement | — | Enable and configure the optional chunk navigator UI. |
statusbar | null | Status bar placement: 'top', 'bottom', or null to hide. Shows byte info on hover and selection details. |
statusbarLayout | — | Configuration object controlling which items appear in the status bar and their placement (left, middle, right sections). |
cursor | false | Enable keyboard/click cursor navigation. Navigate with arrow keys when focused or click bytes to move cursor. |
editable | false | Enable editor mode. Typing/paste/cut emit edit intents. Cursor is automatically enabled while editing. |
Models
VueHex uses Vue 3's defineModel for two-way data binding. These support v-model syntax:
| Model | Type | Default | Description |
|---|---|---|---|
v-model (modelValue) | Uint8Array | new Uint8Array(0) | The currently visible data buffer. In buffer mode, this is the entire dataset; in window mode, this is the current slice. |
v-model:windowOffset | number | 0 | Absolute byte offset of the current window. Automatically updates when scrolling, and scrolls the viewer when changed externally. Use for two-way sync of scroll position. |
v-model:cursorLocation | number | null | null | Current cursor position (absolute byte index). Only active when cursor prop is true. Automatically updates when navigating with keyboard/mouse. |
Events
VueHex emits several events to enable interactive features:
| Event | Payload | Description |
|---|---|---|
updateVirtualData | { offset: number, length: number } | Emitted when the component needs more data in windowed mode. Load the requested byte range and update v-model with the new data. |
edit | VueHexEditIntent | Emitted when editable is enabled and the user types, deletes, pastes, or cuts. In windowed mode, the parent should apply the intent to the backing store and refresh v-model. |
byte-click | { index: number, byte: number, kind: 'hex' | 'ascii' } | Emitted when a user clicks on a specific byte cell. index is the absolute byte position, byte is the value (0-255), and kind indicates whether the hex or ASCII column was clicked. |
selection-change | { start: number | null, end: number | null, length: number } | Emitted when the selection range changes. start and end are absolute byte positions (inclusive), or null if nothing is selected. length is the number of selected bytes. |
row-hover-on / row-hover-off | { offset: number } | Emitted when hovering over/leaving a row. |
hex-hover-on / hex-hover-off | { index: number, byte: number } | Emitted when hovering over/leaving a hex cell. |
ascii-hover-on / ascii-hover-off | { index: number, byte: number } | Emitted when hovering over/leaving an ASCII cell. |
Editable mode
Set editable to turn VueHex into a hex editor. VueHex emits a single edit event describing user intent.
Editing highlights:
- Click hex vs ASCII to choose the active column (or press
Tabto toggle). - Press
Insertto toggle insert/overwrite mode. - Hex typing commits after two nibbles.
- Selection integrates with editing: typing/paste replaces the selection;
Delete/Backspacedelete the selection. - Clipboard shortcuts:
Ctrl/Cmd+Ccopy selectionCtrl/Cmd+Vpaste (hex column treats clipboard as hex bytes; whitespace is ignored)Ctrl/Cmd+Xcut (copy + delete selection)
- Undo/redo shortcuts:
Ctrl/Cmd+ZundoCtrl/Cmd+Yredo (alsoCtrl/Cmd+Shift+Z)
Buffer mode (self-managed)
In data-mode="buffer", VueHex will apply edits to v-model automatically. Listening to @edit is optional.
Undo/redo is built-in in this mode and is implemented as a compact history of edit diffs (not full buffer snapshots).
<template>
<VueHex v-model="bytes" data-mode="buffer" editable style="height: 320px" />
</template>
<script setup lang="ts">
import { ref } from "vue";
import VueHex from "vuehex";
const bytes = ref(new Uint8Array(await file.arrayBuffer()));
</script>
Window mode (parent-managed)
In data-mode="window", VueHex does not own the full dataset. The parent is responsible for applying edit intents to the backing store and then refreshing windowData.
For undo/redo in windowed mode, VueHex emits { kind: "undo" } / { kind: "redo" } intents on the edit event and the parent should implement history.
<template>
<VueHex
v-model="windowData"
data-mode="window"
:window-offset="windowOffset"
:total-size="store.length"
:get-selection-data="getSelectionData"
editable
style="height: 320px"
@updateVirtualData="handleUpdateVirtualData"
@edit="handleEdit"
/>
</template>
<script setup lang="ts">
import { ref } from "vue";
import VueHex, { type VueHexEditIntent, type VueHexWindowRequest } from "vuehex";
const store = ref(new Uint8Array(await file.arrayBuffer()));
const windowOffset = ref(0);
const windowData = ref(new Uint8Array());
let windowLength = 0x4000;
function handleUpdateVirtualData(request: VueHexWindowRequest) {
windowOffset.value = request.offset;
windowLength = request.length ?? windowLength;
windowData.value = store.value.slice(windowOffset.value, windowOffset.value + windowLength);
}
function getSelectionData(start: number, end: number) {
const from = Math.min(start, end);
const to = Math.max(start, end);
return store.value.slice(from, to + 1);
}
function handleEdit(intent: VueHexEditIntent) {
// Apply the intent to your backing store, then refresh windowData.
// (See Storybook "Editable (windowed)" / docs guide for a complete apply function.)
store.value = applyEditIntent(store.value, intent);
windowData.value = store.value.slice(windowOffset.value, windowOffset.value + windowLength);
}
</script>
Styling options
- Import
vuehex/style.cssfor the default look. - Pass
theme="dark" | "light" | "terminal" | "sunset" | "auto"to toggle bundled palettes explicitly (or skip the prop entirely to stick with OS detection). - Roll your own styles targeting the emitted class names (
.vuehex,.vuehex-byte,.vuehex-ascii-char, etc.). The default sheet sets.vuehex { height: 100%; }, so remember to give the wrapper a concrete height.
How VueHex handles large datasets
Technique 1/3 : Virtual Scrolling
Instead of rendering all rows in the DOM, which works fine for small files but quickly becomes unusable for large datasets. A 1 MB file with 16 bytes per row produces over 65,000 DOM nodes. A 100 MB file? 6.5 million nodes. Browsers slow to a crawl when manipulating or even keeping these trees in memory.
VueHex solves this with optimized virtual scrolling:
- Calculate the full height – VueHex computes how tall the entire table would be if every row were rendered (e.g., 65,000 rows × 24px = 1,560,000px).
- Create a container of that height – The scroll container's inner wrapper is sized to the full calculated height, so the scrollbar represents the entire dataset.
- Render only visible rows – As you scroll, VueHex calculates which rows are currently in the viewport (plus a small overscan buffer above/below) and renders only those rows.
- Translate the table – The rendered slice is positioned at the correct scroll offset using CSS
transform: translateY()so it appears in the right place as you scroll.
Result: Instead of 65,000 DOM nodes for a 1 MB file, VueHex renders ~50 rows (what fits on your screen + overscan). Scrolling through a 100 MB file feels instant because the browser only ever manages a tiny fraction of the data.
Technique 2/3 : Chunking
The above technique works very well, but unfortunately it has a limit. Browsers impose limits on element dimensions to prevent rendering engine crashes. Most modern browsers cap max-height at around 33,554,432 pixels (Chrome/Edge) or 17,895,698 pixels (Firefox). Once your calculated scroll container height exceeds this limit, virtualization breaks—the scrollbar becomes inaccurate, and you can't reach data beyond the cap.
For a hex viewer/editor with 16 bytes per row and 24px row height:
- Firefox limit: ~746,000 rows = ~11.4 MB of data
- Chrome limit: ~1.4 million rows = ~21.4 MB of data
Files larger than these thresholds need chunking:
- Divide the dataset into chunks – VueHex splits the file into manageable pieces (e.g., 20,000 rows per chunk).
- Render one chunk at a time – The scroll container only represents the current chunk, keeping its height safely below browser limits.
- Provide chunk navigation – Users click through chunks via the optional chunk navigator UI, and VueHex resets the scroll position and loads the next slice of data.
Why this matters:
- Without chunking, a 100 MB file would require a scroll container 416 million pixels tall—far beyond any browser's rendering capabilities.
- With chunking (e.g., 10,000 rows per chunk = 240,000px per chunk), the same file becomes 6,400 navigable chunks, each comfortably under browser limits.
When chunking activates:
- VueHex automatically enables chunking when the calculated container height would exceed 8,000,000 pixels (a safe default below browser caps).
- You can disable chunking by setting
expand-to-content(which removes virtualization entirely)
Technique 3/3 : Virtual Data
Even with virtualization and chunking, loading a 40 GB file entirely into memory isn't practical. VueHex supports windowed data mode, where:
- Your application keeps the full file in a storage backend.
- You provide VueHex with only the currently needed slice via
v-model(e.g., 100 KB around the visible rows). - When VueHex needs different bytes (because the user scrolled or jumped to a new chunk), it emits
updateVirtualDatawith{ offset, length }. - Your application fetches the requested slice and updates
v-modelasynchronously.
Result: VueHex can display terabyte-scale files while keeping browser memory usage under a few megabytes.
Data modes
Virtual data mode
When VueHex needs different bytes it emits updateVirtualData with { offset, length }. The component keeps rendering whatever you provide through v-model until you feed it a new slice. That means you can back the viewer with disk I/O, HTTP range requests, IndexedDB, or any other storage you control.
You can use v-model:window-offset for two-way binding of the current offset. This automatically:
- Updates the parent when the user scrolls to new data
- Scrolls the viewer when you change the offset programmatically
<template>
<VueHex
v-model="windowData"
v-model:window-offset="windowOffset"
:total-size="fileSize"
data-mode="window"
@updateVirtualData="loadWindow"
/>
</template>
<script setup>
const windowData = ref(new Uint8Array());
const windowOffset = ref(0);
async function loadWindow({ offset, length }) {
const data = await fetchBytesFromSource(offset, length);
windowData.value = data;
// windowOffset will be automatically updated by the component
}
</script>
Alternatively, manually sync the offset when handling updateVirtualData:
<template>
<VueHex
v-model="windowData"
:window-offset="windowOffset"
:total-size="fileSize"
@updateVirtualData="loadWindow"
/>
</template>
<script setup>
async function loadWindow({ offset, length }) {
windowOffset.value = offset; // Manual sync
windowData.value = await fetchBytesFromSource(offset, length);
}
</script>
Full data mode
If you already have the entire Uint8Array, you can skip the virtual data handshake entirely. Set data-mode="buffer" (or omit it entirely) and point v-model at the whole buffer.
<VueHex v-model="entireFile" />
For large datasets you'll probably still want to use the virtual data mode.
Expand-to-content mode
If you don't want VueHex to create its own scroll container, enable expand-to-content. In this mode the component grows to fit the rendered rows, so scrolling happens in the parent/page instead.
- No internal scrolling
- No virtualization / window requests
v-modelmust contain the fullUint8Array
<VueHex v-model="entireFile" expand-to-content :bytes-per-row="16" />
This is useful for docs pages or print-style layouts, but avoid it for very large buffers.
Asynchronous providers
Responding to updateVirtualData can be asynchronous—just update windowOffset and windowData when the bytes arrive:
async function handleUpdateVirtualData(request: VueHexWindowRequest) {
const response = await fetch(
`/api/blob?offset=${request.offset}&length=${request.length}`,
);
const arrayBuffer = await response.arrayBuffer();
windowOffset.value = request.offset;
windowData.value = new Uint8Array(arrayBuffer);
}
Storybook workspace
Run Storybook to explore prebuilt demos:
npm run storybook
The static build lives in storybook-static/ when you run npm run storybook:build.
Viewport sizing
VueHex virtualizes DOM rows, so make sure the component has a bounded height. Set height, max-height, or place it inside a flex/grid cell with a defined size. Without a viewport the table expands indefinitely and virtualization is effectively disabled.
Imperative scrolling
Call scrollToByte through a template ref to jump to absolute offsets:
<VueHex ref="viewer" v-model="windowData" ... />
const viewer = ref<InstanceType<typeof VueHex> | null>(null);
viewer.value?.scrollToByte(0x1f400);
Hover events
VueHex emits enter/leave events for rows, hex cells, and ASCII cells so you can power tooltips or side panels:
row-hover-on/row-hover-off–{ offset }hex-hover-on/hex-hover-off–{ index, byte }ascii-hover-on/ascii-hover-off–{ index, byte }
<VueHex
v-model="windowData"
v-model:window-offset="windowOffset"
@hex-hover-on="handleHexEnter"
@hex-hover-off="handleHexLeave"
/>
function handleHexEnter(payload: { index: number; byte: number }) {
tooltip.open({ byteOffset: payload.index, value: payload.byte });
}
function handleHexLeave() {
tooltip.close();
}
ASCII rendering overrides
The ASCII pane renders characters in the standard printable range (0x20–0x7E) by default. Override the behaviour with is-printable and render-ascii props:
<VueHex
v-model="windowData"
:is-printable="(byte) => byte >= 0x30 && byte <= 0x39"
:render-ascii="(byte) => `${String.fromCharCode(byte)}`"
/>
VueHex also exports VUE_HEX_ASCII_PRESETS (standard, latin1, visibleWhitespace) if you want a drop-in configuration.
Chunk Navigator Customization
When using the chunk navigator (show-chunk-navigator prop), you can customize how the navigator header and chunk items are rendered using slots:
chunk-navigator-header
Customize the chunk navigator header with access to the chunks list and active index:
<VueHex
v-model="windowData"
show-chunk-navigator
chunk-navigator-placement="right"
>
<template #chunk-navigator-header="{ chunks, activeIndex }">
<div class="custom-header">
<h3>File Chunks</h3>
<p>{{ chunks.length }} total | Active: {{ activeIndex + 1 }}</p>
</div>
</template>
</VueHex>
Slot props:
chunks: Array of all chunk descriptorsactiveIndex: Index of the currently active chunk
chunk-navigator-item
Customize individual chunk items with access to chunk data and active state:
<VueHex
v-model="windowData"
show-chunk-navigator
chunk-navigator-placement="right"
>
<template #chunk-navigator-item="{ chunk, active, select }">
<div :class="{ 'my-chunk': true, 'active': active }">
<strong>{{ chunk.label }}</strong>
<span>{{ chunk.range }}</span>
<button @click="select">Jump</button>
</div>
</template>
</VueHex>
Slot props:
chunk: Object with{ index: number, label: string, range: string }active: Boolean indicating if this is the currently active chunkselect: Function to programmatically select this chunk
Both slots are optional. If not provided, VueHex uses the default chunk navigator appearance.
Status bar
Enable the status bar by setting statusbar="top" or statusbar="bottom". You can control which items appear (and where) using statusbarLayout.
Layout
statusbarLayout splits the status bar into three sections:
left(left-aligned)middle(centered)right(right-aligned)
Each section is an array of status bar components (strings or { name, config } objects). Unknown component names are ignored.
When statusbarLayout is omitted, VueHex defaults to:
left: ["offset", "hex", "ascii"]right: ["selection"]
Built-in components
offset: hovered byte offset (respectsuppercase+isPrintable/renderAsciiwhere relevant)hex: hovered byte value as a 2-digit hex stringascii: hovered byte rendered as ASCII (ornonPrintableChar)selection: selection summary ("<count> bytes (start–end)")editable: editor state label (VIEW/EDIT)mode: editor mode (INS/OVR) or placeholder when not availablecolumn: active editor column (HEX/ASCII) or placeholder when not availabletotal: total size in bytes (derived fromtotalSizewhen provided; otherwisev-model.length). Does not include the EOF "ghost" cell.slot: renders one of the status bar slots (see below)
Notes:
- The hover-driven items (
offset,hex,ascii) reflect the cell under the mouse, not the keyboard cursor. - Editor items (
editable,mode,column) render whenever they are included in the layout (they do not auto-hide wheneditableis false).
Per-component configuration (config)
All built-ins support:
label(string): overrides the label textvalueMinWidth(string): CSS length (e.g."10ch") for stable widthsvalueWidth(string): CSS length (e.g."120px")
Component-specific keys:
offset:format("hex"or"decimal"),pad(number),prefix(boolean)hex:prefix(boolean)ascii:quote(boolean)selection:showWhenEmpty(boolean)editable:short(boolean)mode:short(boolean),placeholder(string)column:short(boolean),placeholder(string)total:format("human"or"hex"),decimals(number, for"human"),unit(boolean),pad(number, for"hex"),prefix(boolean, for"hex")
Slot components
Use the built-in "slot" component name to render your own custom content inside the status bar.
Available slots:
#statusbar-left#statusbar-middle#statusbar-right
Example:
<VueHex
v-model="data"
statusbar="bottom"
:statusbar-layout="{
left: ['offset', 'slot', 'hex'],
middle: ['ascii'],
right: ['selection', 'slot'],
}"
>
<template #statusbar-left>
<span>Mode: RO</span>
</template>
<template #statusbar-right>
<span>Endian: LE</span>
</template>
</VueHex>
The "slot" entry controls where your slot content renders relative to built-in items.
Contributing
Thanks for contributing! VueHex aims to stay fast, dependency-free, and easy to embed, so PRs are easiest to review when they’re small and well-scoped.
Local development
Run the Vite dev server (for the workspace app):
npm run dev
Explore demos and component states via Storybook:
npm run storybook
Build the library (and generate type declarations):
npm run build
Build the demo site:
npm run build:demo
Quality checks
This repo uses Biome for formatting and linting:
npm run lint
npm run format
Type-check with Vue’s TS tooling:
npm run type-check
PR guidelines
- Keep changes focused and easy to review
- If you change behavior or add a feature, update Storybook docs/examples when possible
- Keep public API changes intentional (props/events/types) and document them in the README and/or Storybook
Filing issues
Bug reports are most actionable with:
- A minimal reproduction (StackBlitz/link or small repo)
- Browser + OS + Vue version
- Whether you’re using
data-mode="buffer"ordata-mode="window"(and approximate dataset size)
License
MIT © Vincent Vollers