DevTools-style console for the places that don't have DevTools — iOS WebViews, Android WebViews, any page where console.log goes nowhere. Logs, object inspector, and a REPL that reaches your app's live globals. Injects before your app boots — and runs whether your app boots, crashes, or never starts at all.
Use it through an explicit runtime import, or through the Vite plugin when you want it injected before your app bundle runs.
A WKWebView inside a native app can't be opened in Safari Web Inspector unless the app that hosts it sets isInspectable, which defaults to false — so if you don't own the native shell, there is no console to open. Even when you do own it, an inspector you attach after the fact can't show you what already happened during boot. This console is rendered by the page itself, so it's attached from the first line of script, to everyone looking at the screen.
Try the live demo → — no install required. Or open the website source in StackBlitz to run and edit it in-browser.
- 📱 Mobile Friendly: Toggle with 2-finger long press or Shift+C.
- 🚀 Independent of Your App: Rendered by the page, sharing no lifecycle with your framework — it's there even if your app never mounts.
- 🔍 Object Inspector: Interactive viewer for Objects, Arrays, Maps, Sets, and more.
- ⌨️ Live REPL: Evaluate expressions against your app's real globals, with a preview of the result as you type.
- 🧯 Catches What You'd Miss: Intercepts
console.log/error/warn/info/debug, plus uncaught errors and unhandled promise rejections. - 🎨 Themable: Comes with multiple themes (VSCode, Chrome Light, Dracula, Nord, Tokyo).
- ⚡ Vite Plugin: Optional injection before your app bundle runs, so boot-time logs aren't lost.
- 📦 Zero Runtime Dependencies: Nothing pulled in at runtime; Vite is an optional peer.
- 🔒 No Telemetry: Fully local and self-contained — see Privacy.
pnpm add @codehacks/virtual-console
# or
npm install @codehacks/virtual-consoleimport { installVirtualConsole } from '@codehacks/virtual-console';
import '@codehacks/virtual-console/styles.css';
const virtualConsole = installVirtualConsole({
maxLogs: 100
});
// Later, from your framework cleanup lifecycle:
virtualConsole.destroy();Add the plugin to your vite.config.ts when you want the console injected before your app bundle runs:
import { defineConfig } from 'vite';
import { virtualConsoleVitePlugin } from '@codehacks/virtual-console/plugins/vite';
export default defineConfig({
plugins: [
virtualConsoleVitePlugin({
// Specify available themes. The first one will be the default.
themes: ['vscode', 'chrome-light', 'dracula'],
options: {
maxLogs: 100
}
})
]
});The console supports the following themes:
vscode(Default Dark)chrome-light(Chrome DevTools Light)draculanordtokyo
installVirtualConsole() accepts a partial VirtualConsoleConfig:
installVirtualConsole({
maxLogs: 100, // logs kept before the oldest is dropped
minHeight: 100, // dock bounds when docked top/bottom
maxHeight: window.innerHeight * 0.8,
defaultHeight: 200,
minWidth: 200, // dock bounds when docked left/right
maxWidth: window.innerWidth * 0.8,
defaultWidth: 400,
keyboardShortcut: { code: 'KeyC', shiftKey: true }, // see below
longPressFingers: 2, // mobile activation gesture
longPressDuration: 500, // ms
replEnabled: true, // set false to remove the eval-based REPL entirely
replHistoryLimit: 50, // REPL commands kept in localStorage
targetElement: undefined // mount into a specific element instead of <body>
});keyboardShortcut takes a KeyboardEvent.code plus the exact modifiers that must be held - every
modifier not listed is required to be up, so the match is exact and won't fire on an unrelated
combination that happens to share a key:
// Ctrl+Shift+D instead of the default Shift+C
installVirtualConsole({ keyboardShortcut: { code: 'KeyD', ctrlKey: true, shiftKey: true } });
// Disable the keyboard shortcut entirely (e.g. only use the long-press gesture,
// or wire your own trigger to the exported `toggleConsole()`)
installVirtualConsole({ keyboardShortcut: null });The shortcut is automatically ignored while focus is inside a text input, textarea, select, or
contenteditable element (including the console's own REPL input) so it can't hijack a keystroke
the app being debugged relies on.
The REPL evaluates whatever you type via eval. If you want to ship a read-only log viewer with
no eval surface at all (e.g. a build that might reach production), set replEnabled: false.
The console's stacking order can be adjusted from your own CSS without a JS config option, in case
your app already has a very-high-z-index overlay of its own:
:root {
--vc-z-index: 2147483000;
}- Desktop: Press
Shift + C(orEscapeto close while the console is focused), configurable viakeyboardShortcut - Mobile: Long press with 2 fingers for 0.5s, configurable via
longPressFingers/longPressDuration
Virtual Console makes zero outbound network or telemetry calls of its own. It only reads/writes localStorage on the page it's installed on (theme choice, dock position/size, REPL history - all under keys prefixed virtual-console:) and renders everything locally in the DOM it creates. Nothing you log or throw is ever sent anywhere by this library.
# Install dependencies
pnpm install
# Build the library
pnpm build
# Lint
pnpm lint
# Typecheck
pnpm typecheck
# Run the test suite
pnpm test
# Run local workspace examples
pnpm dev:import:local
pnpm dev:vite-plugin:local
# Build local workspace examples
pnpm build:local
# Run the public website
pnpm dev:website
# Build the public website
pnpm build:website
# Build the public website and serve the real static output (what CI deploys,
# prerendered HTML included) - no hot reload, refresh manually after a change
pnpm preview:website
# Same, but rebuilds automatically on every change under website/src or
# website/index.html - still no hot reload, just refresh the browser yourself
pnpm preview:website:watch
# Pull a @codehacks/virtual-console release into the website (see Releasing below)
pnpm bump:website:latestSee examples/README.md for the full layout (local workspace examples vs. standalone examples that install the real published package) and the *:published commands.
Versions are managed by vump (vump.toml), which declares two
independently-versioned projects - main (this package, tagged v{version}) and website (the
website, tagged website-v{version}). Pushing either tag shape triggers its own workflow; see
DECISIONS.md for why they're split and what each one does.
Release the package - publishes to npm, under the alpha/beta/rc dist-tag for a
pre-release or latest for a stable one:
vump patch --project main --tag --push # or minor / majorRelease the website - for a site-only change (copy, layout, a new section); never touches npm or the package's version:
vump patch --project website --tag --push # or minor / majorPick up a new package version in the website. website/package.json pins @codehacks/virtual-console
to an exact version rather than a latest alias, so the file itself always shows exactly what the
live website runs - installed from its own committed lockfile, not re-resolved at deploy time. Bump it
explicitly for whichever channel you want live, then release the website to deploy it - this is a
deliberate follow-up, not automatic:
pnpm bump:website:latest # or bump:website:alpha / :beta / :rc to preview a pre-release
git add website/package.json website/pnpm-lock.yaml
git commit -m "chore(website): bump @codehacks/virtual-console"
vump patch --project website --tag --pushCommon vump commands, run from the repo root:
vump status # every project's version, and whether its files agree
vump check <tag> # verify a pushed tag against its project (what CI runs; infers the project from the tag's shape)
vump patch|minor|major --project <main|website> # bump a stable version
vump alpha|beta|rc --project <main|website> --from <bump> # start/advance a pre-release, e.g. --from patch
vump release --project <main|website> # drop a pre-release suffixAdd --dry-run to preview a bump without writing anything. --tag implies --commit, and --push
implies --commit too - each flag is independent otherwise, so pushing a tag needs both
--tag --push together, as in the examples above.
See website/README.md for the public website (website/) - it's not one of the examples above; it's the landing page at virtual-console.codehacks.io, deployed on its own release line independent of the package's.
See DECISIONS.md for the rationale behind non-obvious choices (REPL evaluation safety, DevTools-parity scope, known/accepted limitations) before proposing a change in those areas.
See CLAUDE.md for the engineering standards contributors (human or AI) are expected to hold to in this repo.
MIT