If you've ever downloaded a .jxl file and watched your browser shrug at it, you've met the JPEG XL problem: it's a genuinely great image format that almost nothing opens yet. So I built a small tool that converts images to and from JPEG XL entirely in the browser — no upload, no account, no server touching your files.
Live: https://jpegxlconvert.com · Source: https://github.com/robertcassch-dot/jpegxlconvert
Why bother?
JPEG XL (.jxl) gives you smaller files at the same quality, lossless JPEG repacking (~20% off your existing JPEGs with zero quality loss), transparency, and a royalty-free spec. The catch in 2026 is support: most browsers and apps still can't display a .jxl. Chrome removed it in 2022 and is in the process of bringing it back — but until that's everywhere, people keep ending up with .jxl files they can't view.
Most "online converters" answer that by uploading your image to their server. For a personal photo, that's a lot of trust for a format conversion. I wanted the opposite: the file never leaves your device.
How it works
Everything runs client-side with WebAssembly:
-
JPG / PNG / WebP / GIF → JXL: the browser decodes the source natively, then
@jsquash/jxl(libjxl compiled to WASM) encodes the JXL. -
.jxl→ PNG / JPG: decoded with@jsquash/jxl, re-encoded natively via a<canvas>. - HEIC (iPhone photos) → JPG/PNG/JXL: decoded in-browser too, so you can finally open HEIC outside Apple's world.
import { encode } from '@jsquash/jxl';
const bitmap = await createImageBitmap(file); // native decode
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
canvas.getContext('2d').drawImage(bitmap, 0, 0);
const imageData = canvas.getContext('2d')
.getImageData(0, 0, bitmap.width, bitmap.height);
const jxl = await encode(imageData, { quality: 90 }); // WASM encode, on-device
No fetch() to a backend anywhere in that path — you can verify it in the Network tab, which is kind of the whole point.
A few things I learned
-
Self-host the WASM codec. Pulling it from a CDN at runtime works in dev, but for speed, reliability and a cleaner privacy story you really want the
.wasmserved from your own origin. - Orientation metadata bites you on HEIC. iPhone photos lean on EXIF orientation; if you ignore it, half your conversions come out sideways.
-
A "viewer" is half the product. Plenty of people don't want to convert — they just want to see the
.jxl. Decoding it to a<canvas>and offering a one-click "save as JPG/PNG" turned out to be the most-used path.
It's open source
The whole static site is on GitHub: https://github.com/robertcassch-dot/jpegxlconvert. It's multilingual (EN/ES/DE/FR/RU), deploys to any static host (I use Cloudflare Pages), and has zero backend.
If you've got .jxl files gathering dust, try it: https://jpegxlconvert.com. Feedback and issues welcome.
Top comments (2)
How did you handle the complexity of JPEG XL's modular encoding and decoding, particularly with regard to the variable-size code trees?
Honest answer: I didn't reimplement any of that — and I'd be skeptical of anyone who claims they hand-rolled JPEG XL's modular mode in a side project. All the format-level heavy lifting (Modular mode, the meta-adaptive context trees / "variable-size code trees", the ANS entropy coding) is handled by libjxl, the reference implementation. What I actually built is the delivery layer: I compiled libjxl to WebAssembly as a single-threaded build (so it runs in any browser with no cross-origin-isolation headers) and wired it to run fully client-side.
The one piece I engineered around deliberately is lossless JPEG transcoding (JxlEncoderAddJPEGFrame). For the JPG→JXL path it intentionally does not decode to pixels and re-run the modular/MA-tree machinery — it repacks the original JPEG's DCT coefficients with JXL's entropy coder plus JPEG-reconstruction metadata. You get a ~10–20% smaller file that decodes back to a bit-identical copy of the source JPEG, which sidesteps most of the modular-coding complexity for that path.
So the credit for the clever tree/context modeling goes 100% to the libjxl team — my contribution was making it private and instant in the browser. Happy to share the exact WASM build flags if useful.