Fast, private browser utilities for developers. No servers, no tracking — just tools that work.
Utilbench is a client-side utility tools SPA. Every tool runs entirely in the browser using Web Workers, WASM, and on-device AI models. Your files are never uploaded — nothing you drop into a tool leaves your machine.
- 33 tools across data, text, and media categories
- 100% client-side — no backend, no analytics, no uploads. Assets (code chunks, WASM, AI models) are fetched from the same origin on demand; your data never is.
- On-device AI — ONNX Runtime for background removal, TensorFlow.js + ESRGAN for image upscaling, all inference in-browser
- Off-main-thread compute via a managed Web Worker pool (2–4 threads, task queue, cancellation, 30 s default timeout)
- Cached WASM loader with streaming compilation and ArrayBuffer fallback
- Cmd/Ctrl + K global fuzzy search across all tools (Fuse.js)
- URL-state sync — many tools serialize settings to the query string so links are shareable
- Per-tool preferences persisted to
localStoragewith debounced writes - Lazy-loaded routes with per-tool skeleton loaders
- SEO-ready — prerendered per-route HTML shells, meta tags, OG images, JSON-LD, auto-generated sitemap
| Tool | Description |
|---|---|
| JSON Formatter | Format, validate, and beautify JSON with syntax highlighting |
| JSON Schema Generator | Generate JSON Schema from sample JSON data |
| JWT Decoder | Decode and inspect JWT tokens without sending data to a server |
| Base64 Encoder | Encode and decode Base64 strings |
| CSV to JSON | Convert CSV files to JSON with customizable options |
| YAML to JSON | Convert YAML data to JSON format |
| Merge PDF | Combine multiple PDF files into one, in any order |
| Split PDF | Split a PDF by page range, every N pages, or one per page |
| Compress PDF | Shrink a PDF — lossless restructure or strong image compression |
| Watermark PDF | Add a text or image watermark — opacity, rotation, tiling, page targeting |
| PDF Metadata Remover | Strip title, author, dates, XMP, and producer fingerprints from a PDF |
| Images to PDF | Convert JPG, PNG, and WebP images into a single PDF with reorder and fit options |
| Tool | Description |
|---|---|
| Cron Parser | Parse and explain cron expressions in plain English |
| Diff Checker | Compare two texts and highlight the differences |
| Case Converter | Convert text between camelCase, snake_case, and more |
| Markdown Preview | Render Markdown to clean HTML, live, in your browser |
| Markdown to PDF | Convert Markdown into a print-ready PDF — choose page size, margins, and fonts |
| Lorem Ipsum | Generate placeholder text for designs and mockups |
| Tool | Description |
|---|---|
| Background Remover | Remove image backgrounds with a local AI model — transparent PNG cutouts |
| Image Upscaler | Upscale images 2× or 4× with a local AI super-resolution model (ESRGAN) |
| Image Compressor | Shrink images with real JPEG, WebP, AVIF, and PNG codecs without changing dimensions |
| Image Format Converter | Convert PNG, JPG, WebP, GIF, BMP, and AVIF to PNG, JPG, or WebP |
| Image Resizer | Resize and crop images directly in the browser |
| Image Cropper | Crop and trim images with a visual drag box and aspect presets |
| Image Watermark | Add a text or logo watermark — position grid, opacity, tiling, rotation |
| Add Text to Image | Meme maker — add captions and text layers with a drag editor |
| Image Metadata Remover | Remove EXIF, GPS, and camera metadata from images locally |
| HEIC Converter | Convert iPhone HEIC/HEIF photos to JPG, PNG, or WebP (libheif) |
| PDF to Image | Convert each PDF page to a PNG or JPEG — choose DPI, format, and pages |
| QR Generator | Generate QR codes from text, URLs, or structured data |
| SVG Optimizer | Optimize and minify SVG files while preserving visual quality |
| Favicon Generator | Generate favicons in all required sizes from a single image |
| Lottie Previewer | Preview and inspect Lottie animation files |
- React 19 + React Router v7 (lazy-loaded routes)
- Vite 6 (build & dev server) · Cloudflare Vite plugin + Wrangler (deploy)
- Tailwind CSS 4 + shadcn/ui (utility-first styling + Radix primitives)
- TypeScript (strict mode,
noUncheckedIndexedAccess,verbatimModuleSyntax) - Biome (linting & formatting — replaces ESLint + Prettier)
- Vitest + Testing Library + vitest-axe (jsdom environment)
- Bun (package manager & script runner)
Tool-side heavy lifting:
- onnxruntime-web — background removal (local ONNX segmentation model)
- @tensorflow/tfjs + upscaler / @upscalerjs/esrgan-slim — AI super-resolution
- libheif-js — HEIC/HEIF decoding (WASM)
- @jsquash/* (jpeg, png, webp, avif, oxipng) — real image codecs in WASM
- pdf-lib + pdfjs-dist — PDF manipulation and rendering
- svgo, marked + dompurify, fflate (ZIP), lottie-web, qrcode, croner / cronstrue, js-yaml, diff
- Fuse.js (fuzzy search) · react-helmet-async (SEO) · @dnd-kit (reordering) · sonner (toasts)
# Install dependencies (postinstall copies WASM/model assets into public/)
bun install
# Start dev server
bun run dev
# Production build (copy assets → Vite → OG image → icons → SEO shells)
bun run build
# Vite build only (still copies assets first)
bun run build:only
# Build + serve via Wrangler locally
bun run preview
# Lint & format check
bun run check
# Auto-fix lint/format issues
bunx --bun @biomejs/biome check --write ./src
# Deploy to Cloudflare
bun run deploybun run test # watch mode
bun run test:run # single run
bun run test:ui # Vitest UI
bun run test:coverage # v8 coverageVitest runs in jsdom with @testing-library/react and @testing-library/jest-dom matchers. Test files live alongside source as **/__tests__/*.test.{ts,tsx}. The shared setup (src/test/setup.ts) installs jsdom polyfills required by Radix UI (ResizeObserver, PointerEvent, pointer-capture, scrollIntoView).
Notes:
- Auto-cleanup is not enabled — component tests must call
cleanup()inafterEach. - Tools using
useUrlState(or any React Router hook) must wrap renders in<MemoryRouter>.
Tools are auto-discovered at build time via import.meta.glob — no manual registration. Each tool lives in src/tools/<tool-slug>/ and exports two files.
1. tool.ts — named tool export of type ToolDefinition:
import type { ToolDefinition } from "../types";
export const tool: ToolDefinition = {
name: "My Tool",
slug: "my-tool",
description: "What it does",
seoDescription: "Longer description used for meta tags (optional)",
category: "data", // "data" | "text" | "media"
tags: ["tag1", "tag2"],
featured: false,
icon: "Wrench", // Lucide icon name — must also be mapped in src/lib/icons.ts
route: () => import("./Route"),
features: [ // rendered as the three feature cards on the tool page
{ icon: "Zap", title: "Instant", description: "Runs locally in your browser." },
{ icon: "Code", title: "Customizable", description: "Configure to taste." },
{ icon: "Download", title: "Exportable", description: "Copy or download results." },
],
};2. Route.tsx — default-exports the React component (lazy-loaded by the router):
import { ToolShell, TwoPane, PaneHeader, ErrorAlert } from "../../components/tool-layout";
export default function Route() {
return (
<ToolShell>
<TwoPane>
{/* tool UI */}
</TwoPane>
</ToolShell>
);
}Also remember to:
- Add the Lucide icon import + mapping entry to
src/lib/icons.ts(falls back toBracesotherwise). - Add a skeleton entry to
src/tools/skeletonRegistry.tsxfor the lazy-loading state.
The Route.tsx body must start directly with the tool's interactive UI inside a ToolShell. Page chrome — breadcrumbs, hero, feature cards, sibling tools — is centralized in src/pages/ToolPage.tsx and must not be duplicated per tool.
src/
├── components/
│ ├── ui/ # shadcn/ui primitives
│ ├── tool-layout/ # ToolShell, TwoPane, PaneHeader, ErrorAlert
│ ├── skeleton/ # Shared skeleton primitives + FeatureCardsSkeleton
│ ├── Layout.tsx # Global <main>, header, footer
│ ├── SearchModal.tsx # Cmd+K fuzzy search (cmdk + Fuse.js)
│ ├── FeatureCards.tsx # Three feature cards on every tool page
│ ├── Breadcrumbs.tsx
│ ├── PageTransition.tsx
│ ├── ReportIssueButton.tsx
│ ├── Logo.tsx / IconSwap.tsx / KbdHint.tsx
│ ├── RootErrorBoundary.tsx
│ └── ToolErrorBoundary.tsx
├── hooks/
│ ├── useClipboard.ts # async copy/read with auto-resetting `copied` state
│ ├── useToolPreferences.ts # debounced localStorage settings per tool
│ ├── useUrlState.ts # schema-based URL search-param sync
│ ├── useKeyboardShortcut.ts # declarative shortcut registration
│ └── useScrollReveal.ts
├── lib/
│ ├── icons.ts # Static Lucide icon registry — add entries here
│ ├── image.ts / encode.ts # Shared image + encoding helpers
│ ├── pdf.ts / pdfjs-render.ts / pdf-tools-client.ts
│ ├── markdown.ts # marked + DOMPurify pipeline
│ ├── prefetch.ts # Route prefetch on hover/intent
│ ├── errorReport.ts
│ └── utils.ts # cn() helper
├── pages/
│ ├── Home.tsx / Tools.tsx / Privacy.tsx / NotFound.tsx
│ └── ToolPage.tsx # Shared chrome for every tool route
├── seo/
│ ├── SEOHead.tsx # title, description, canonical, OG, Twitter tags
│ ├── JsonLd.tsx # JSON-LD structured data
│ ├── schemas.ts # Organization, WebSite, SoftwareApplication, Breadcrumb, WebPage
│ └── constants.ts
├── tools/ # Tool plugins — one directory per tool
│ ├── registry.ts # Auto-discovers tools via import.meta.glob
│ ├── skeletonRegistry.tsx # Per-tool skeleton loaders
│ ├── constants.ts
│ └── types.ts # ToolDefinition interface
├── workers/
│ ├── pool.ts # 2–4 thread pool, task queue, 30 s timeouts, cancellation
│ ├── worker.ts # Generic pool worker
│ └── pdf-tools.worker.ts # Dedicated PDF worker
├── wasm/
│ └── loader.ts # Cached module loader with streaming compilation
├── test/setup.ts # jsdom polyfills for Radix
├── App.tsx
├── router.tsx # Lazy-loaded routes with title metadata
├── main.tsx # Entry point — HelmetProvider, RouterProvider
├── config.ts # APP_NAME, APP_DESCRIPTION
└── index.css # Tailwind 4 + hex theme tokens (:root)
- Plugin system — tools self-register by filesystem convention; the registry exposes
getAllTools(),getToolBySlug(),getFeaturedTools(),getToolsByCategory(). - Tool layout primitives — every
Route.tsxcomposesToolShell(+ optionalTwoPane,PaneHeader,ErrorAlert). Thewidevariant (1900 px) is for tools needing a bigger work surface:diff-checker,jwt-decoder,background-remover,image-upscaler,image-compress,image-crop,image-caption,image-watermark. - Worker pool —
workerPoolsingleton insrc/workersruns CPU-heavy tasks off the main thread with queue, cancellation, and a 30 s default timeout. A wedged worker is terminated and replaced in place to avoid pool poisoning. - WASM loader —
compileWasm/instantiateWasmfromsrc/wasmstream + cache modules in memory with an ArrayBuffer fallback. - Theming — semantic CSS custom properties (hex) under
:rootinsrc/index.css. Adarkvariant is declared (@custom-variant dark) but there is no.darkpalette or in-app toggle yet — the app ships light-only. - Vendor splitting —
vendor-react,vendor-router,vendor-fuse,vendor-lottie,vendor-radix-core(eager first-paint graph),vendor-radix-tools(lazy tool routes only),vendor-cmdk,vendor-marked,vendor-dompurify,vendor-pdf,vendor-pdfjs,vendor-dnd,vendor-jsquash. ONNX Runtime is deliberately not seeded — only the background-remover worker imports it. - Path alias —
@/*→./src/*(used by shadcn components; toolRoute.tsxfiles use relative imports).
Asset copy steps run on postinstall and before every build, staging WASM binaries and AI models into public/:
copy-pdfjs-assets.ts— pdf.js worker + cmapscopy-ort-assets.ts— ONNX Runtime WASMcopy-upscaler-models.ts— ESRGAN model weightscopy-libheif-assets.ts— libheif WASMcopy-tfjs-wasm-assets.ts— TensorFlow.js WASM backend
Post-Vite steps:
generate-og-image.ts— Satori + Resvg, produces OG imagesgenerate-icons.ts— convertspublic/favicon.svginto 192 px / 512 px PNGsgenerate-seo-shells.ts— writes a static prerendered HTML shell per route (/,/tools,/privacy,/tools/<slug>) with route-specific head tags, JSON-LD, and above-the-fold content; React clobber-renders over it at runtimeprerender.ts— optional Puppeteer route prerendering (not in the default build chain)
A custom Vite plugin in vite.config.ts also generates sitemap.xml from the discovered tool slugs.
Deployed on Cloudflare Workers Assets via Wrangler (bun run deploy).
wrangler.jsonc—not_found_handling: "single-page-application"provides the SPA fallback (no_redirectsfile needed; Cloudflare flags it as an infinite loop).public/_headers—X-Frame-Options: DENY,X-Content-Type-Options: nosniff, strict referrer policy, permissions policy, and long-lived cache headers for/assets/*and/fonts/*.
CLAUDE.md— conventions and architecture notes for contributors and Claude CodePRODUCT.md— strategic context: users, purpose, brand personality, anti-references, design principlesDESIGN.md— visual design system: tokens, typography, elevation, components, do's and don'ts
MIT — see LICENSE for details.