Viewer.js: a dependency-free image viewer with an unusually complete option surface
JavaScript image viewer.
At a glance
- What is it?
- A single image viewer library distributed as six files in a dist folder, with 61 options, 23 methods and 17 events documented in the README, and no framework attached to it.
- Who is it for?
- Viewer.js fits the case where you have images on a page and want a lightbox with gallery navigation, and you do not want a framework in the build to get it. It is MIT licensed, framework-agnostic, and documented well enough that the options reference answers most questions before you ask them.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 23 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two script tags and a constructor, with no framework required
The installation is two lines, and there is no build step in the README's version of it:
<link href="/path/to/viewer.css" rel="stylesheet">
<script src="/path/to/viewer.js"></script>Then the constructor takes an element and an optional options object. The target can be a single image or a container of images, and when it is a container the library finds the images itself by calling `element.querySelectorAll('img')` on it. The README is clear about the one structural requirement: a block container is required, which is why the markup example wraps every image in a `div`.
The full example shows both modes of use, single image with options and a gallery:
const viewer = new Viewer(document.getElementById('image'), {
inline: true,
viewed() {
viewer.zoomTo(1);
},
});
const gallery = new Viewer(document.getElementById('images'));The `viewed` callback calling `zoomTo(1)` is a small but telling detail: it normalises zoom when the viewer opens, so a click does not land the user at whatever scale the previous session left it. Inline mode is the other half of the interface, since the library supports both modal and inline presentation and inline is what you want when the viewer should occupy a fixed region of the page rather than take over the screen.
There is also a static entry point, `Viewer.create(element[, options])`, for callers who would rather not hold a reference to the instance.
What the dist folder actually ships
The README documents the `dist/` layout rather than describing it in prose, which is more useful than it sounds:
dist/
├── viewer.css
├── viewer.min.css (compressed)
├── viewer.js (UMD)
├── viewer.min.js (UMD, compressed)
├── viewer.common.js (CommonJS, default)
└── viewer.esm.js (ES Module)Four module formats for the same library is the kind of thing that quietly saves a bundler configuration later. `package.json` confirms the mapping: `main` is the CommonJS build, `module` is the ES module build, `browser` is the UMD build, `style` is the CSS file and `types` points at `types/index.d.ts`. So TypeScript consumers get declarations, bundlers pick the ES module, and a plain script tag uses the UMD file with no module system involved.
The published `files` array is `src`, `dist` and `types`, which means the package ships the sources as well as the builds. Handy when you want to read the implementation, slightly unusual for a package of this size.
The build itself is a small, readable toolchain rather than a framework: Rollup for JavaScript, PostCSS for the stylesheet, Babel configured through `.babelrc`, Karma for tests, ESLint and Stylelint for the two languages, and `simple-git-hooks` wired to a `prepare` script so hooks install on install. The release script chains clean, lint, build, compress, copy and test in that order, which tells you what has to pass before a version ships.
Sixty-one options, and the two knobs that actually change the layout
The README lists the full option surface and, unusually for a project this size, documents most options with their type, default and behaviour. The feature list claims 61 options, 23 methods and 17 events. Most of those options are switches for one control, but two of them change how much room the viewer takes, and those are worth understanding before you configure anything.
`navbar` controls the thumbnail strip and accepts a number from 0 to 4 or a string, where the number is a breakpoint behaviour. `0` or `false` hides it, `1` or `true` shows it, and `2`, `3` and `4` show it only above 768, 992 and 1200 pixels respectively. On top of that there are `small`, `medium` and `large` thumbnail sizes, and an object form:
navbar: {
show: true,
size: 'large',
}The same 0 to 4 scheme applies to `navigation` and to `title`, which is the pattern to notice: these options are responsive visibility rules, not booleans. `navigation` defaults to `false`, so the previous and next buttons are hidden unless you ask for them, and it also accepts separate objects for `prev` and `next` with their own size and visibility. Thumbnail sizing follows a fixed 9:16 width-to-height ratio.
The default for most other options is `true`, meaning the sensible behaviour is on and you opt out. `backdrop` takes a `static` value for a backdrop that does not close the modal on click, which is the kind of detail you would otherwise discover by trying it. Global defaults can be changed with `Viewer.setDefaults(options)` rather than repeating the same object at every call site.
Keyboard mapping, events, and what the README leaves out
Keyboard support is modal only, and the README gives the complete mapping rather than a sample. `Esc` exits full screen or closes the viewer, `Space` stops play, `Tab` moves focus across the viewer buttons, `Enter` triggers the focused button, the arrow keys navigate between images and zoom, and `Ctrl + 0` and `Ctrl + 1` jump to initial and natural size. The arrow glyphs are the documented binding, so the keys themselves are the API.
Events follow the same thoroughness, with 17 of them and the README documenting the signature of each. The `viewed` callback in the usage example is one of them, and the naming convention is consistent, which is the main thing you need from an event surface: you can guess that `zoom` fires after a zoom without reading the reference, and be right most of the time.
What the README does not settle is where you should stop reading. There is a website at fengyuanchen.github.io/viewerjs, a cdnjs entry for both the CSS and the JavaScript, a `jquery-viewer` wrapper maintained as a separate repository, a CHANGELOG in the tree, and a karma-based test setup. Beyond that, the documentation lives on the pages, and the README itself is long enough that you will find most of what you need without leaving it.
Accessibility is the gap worth naming. The README documents keyboard shortcuts and focus movement between buttons, which is more than most libraries in this category bother with, but it does not describe ARIA roles, screen reader announcements, or how the viewer behaves for users who have disabled animation. If that is a requirement for you, the source in `src/` and the docs site are where you would have to look.
Comparing with the alternatives a reader would otherwise reach for
The realistic comparison set is not other image viewers so much as the browser's own behaviour and a short custom implementation. The browser gives you a link that opens an image in a tab, which is free, requires no JavaScript, and is completely accessible. Viewer.js exists because that is not what you want when the images are part of a page.
Against other libraries, the differences come down to what you give up. A framework component library gives you reactivity, theming through CSS variables, and a component you can drop into a Vue or React tree, but it also gives you a dependency on that framework and its lifecycle. Viewer.js gives you none of that: `new Viewer(element)` is a constructor over a DOM node, the styles are a plain stylesheet, and there is no React wrapper to keep in step with.
What you gain from owning the implementation is that the whole thing is often less code than the configuration you would write for a general library. Zoom, rotate, flip, thumbnail strip, keyboard and touch are not trivial to build well, which is the argument for a library rather than a weekend project. But if you need only zoom and next and previous, the honest answer may be that you need less than Viewer.js offers.
The presence of a `jquery-viewer` wrapper in a separate repository is worth reading as a signal. It says the library integrates with older stacks deliberately, and it is the fastest way to check whether the API will fit an existing codebase before you commit to the plain JavaScript path.
Maintenance, versioning and the cost of 61 options
The project is MIT licensed, authored by Chen Fengyuan, and it is clearly still being worked on. The last push was on 2026-09-13 and v1.14.0 was published on 2026-09-12, with v1.13.0 on 2026-09-06 and v1.12.0 on 2026-08-22. Three releases in three weeks is a maintained dependency, and the README's versioning section plus the commitlint configuration indicate releases follow conventional commits rather than being tagged ad hoc.
The cost of the option surface is the thing to weigh. Sixty-one options with breakpoint-number semantics means the defaults are doing more work than your configuration is, and once you start passing `navbar`, `navigation` and `title` objects with their own nested sizes, you have written a chunk of configuration that has to survive upgrades. The library's own recommendation pattern, `Viewer.setDefaults`, exists precisely because repeating that at every call site is worse, and it is the right first thing to reach for.
A second cost is the six-file dist. Four module formats plus two stylesheet variants is a sensible packaging decision and a mild annoyance if you pin an exact version, since you are choosing which artifacts your build resolves. Nothing here is a real problem; it is just more surface than a 30-line custom lightbox would have.
The version history in the CHANGELOG is the right place to look before pinning, and the docs site is where the option reference continues past what the README chooses to print.
Editorial conclusion
Viewer.js fits the case where you have images on a page and want a lightbox with gallery navigation, and you do not want a framework in the build to get it. It is MIT licensed, framework-agnostic, and documented well enough that the options reference answers most questions before you ask them. It is the wrong tool when you need virtualised rendering for very large galleries, when you need accessibility guarantees beyond what the README describes, or when you would rather write twenty lines of your own markup than adopt a 61-option surface. The repository is not archived, the last push was on 2026-09-13, and v1.14.0 shipped on 2026-09-12 with v1.13.0 and v1.12.0 in the weeks before, so this is an actively released line. Install it with `npm install viewerjs`, include `viewer.css` and `viewer.js`, and start with `new Viewer(element)` before configuring anything.
Frequently asked questions
What is Viewer.js and how do I install it?
Viewer.js is a standalone JavaScript image viewer. Install it with `npm install viewerjs` or load `viewer.css` and `viewer.js` directly with a link tag and a script tag, then call `new Viewer(element)` on an image or a container of images.
How do I turn a list of images into a gallery with Viewer.js?
Wrap the images in a block container, give it an id, and pass that element to the constructor. Viewer.js finds every image inside it by calling `element.querySelectorAll('img')`, and the navigation buttons are hidden by default, so enable them with the `navigation` option.
Which keyboard shortcuts does Viewer.js support?
Keyboard support is available in modal mode only. `Esc` exits full screen or closes the viewer, `Space` stops play, `Tab` switches focus between buttons, `Enter` activates one, arrow keys navigate and zoom, and `Ctrl + 0` or `Ctrl + 1` reset to initial or natural size.
Does Viewer.js work with React, Vue or Angular?
It works with them, because it is a plain constructor over a DOM element and does not depend on any framework. There is a separate `jquery-viewer` repository that wraps it as a jQuery plugin, and the package ships UMD, CommonJS and ES module builds so bundlers can resolve it directly.
How many options, methods and events does Viewer.js have?
The README lists 61 options, 23 methods and 17 events, with most of the options documented in place along with their types and defaults. Global defaults can be changed once with `Viewer.setDefaults(options)` instead of passing the same object at every call site.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/fengyuanchen-viewerjs)