Open-source project
weizhenye/ASS avatar
weizhenye/ASS

ASS.js renders subtitles with the DOM, and accepts that the pixels will differ

A lightweight JavaScript ASS subtitle renderer

665 stars87 forksJavaScriptMIT

At a glance

What is it?
ASS.js is a JavaScript renderer for ASS subtitles on HTML5 video that draws with the DOM instead of WebAssembly, which makes it far smaller and lets the browser handle CJK font fallback. The trade is exact rendering parity with VSFilter or libass.
Who is it for?
Adopt ASS.js if you need ASS subtitles on HTML5 video in a browser and care about bundle size, CJK font fallback and avoiding a WebAssembly worker, and you can accept rendering that is close to but not pixel identical with VSFilter or libass. Do not adopt it when subtitle accuracy must match a reference renderer exactly, or when your target browsers fall below the versions listed for the features you need.
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 69 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 October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

DOM rendering instead of a WebAssembly subtitle server

ASS.js is described in the repository as a lightweight JavaScript ASS subtitle renderer that renders ASS subtitles on HTML5 video with almost full ASS features. The implementation choice is the interesting part, and the README is explicit about it.

The table in the README compares ASS.js with two WebAssembly solutions, JavascriptSubtitlesOctopus and JASSUB, and the summary line says ASS.js is 60 times smaller than WebAssembly solutions. The reason is structural. A WebAssembly approach has to ship a libass port as a binary plus a worker to run it. ASS.js draws the subtitles with the DOM instead, so the renderer is JavaScript and CSS and there is no binary payload at all.

The second difference is fonts. The README says WebAssembly solutions also require setting a fallback font to avoid CJK characters turning into tofu, and calls that a huge cost for the web, while in ASS.js font fallback is handled by the browser. That is the kind of problem that does not show up in a demo with Latin text and then shows up the moment your subtitles are in Chinese or Japanese.

The third difference is the honest one. The README says that compared with WebAssembly solutions it is almost impossible for the DOM to render exactly the same result in every pixel as VSFilter or libass, and that ASS.js will provide best efforts to accurate rendering. So the project is not claiming parity, it is claiming a different trade.

The package is published to npm as assjs, the homepage is ass.js.org with an online demo, and there is a related ass-compiler project alongside it.

Installing assjs and loading it in two ways

Installation is a single npm command.

bash
npm install assjs

The library is then usable as a JavaScript module.

html
<script type="module">
import ASS from '/path/to/assjs/dist/ass.min.js';
</script>

Or as a classic script, in which case the global is window.ASS.

html
<script src="/path/to/assjs/dist/ass.global.min.js"></script>
<script>
console.log(window.ASS);
</script>

Both paths matter depending on where you are in the stack. The module build suits anything with a bundler, and the global build suits a page that is not built at all. The package.json sets type to module and points main at dist/ass.js with types at dist/ass.d.ts, and it publishes the dist and src directories.

The build tooling in the repository explains where those files come from. Rollup produces the bundles, with a dev script that runs rollup in watch mode, and a build script that runs rollup once. The postbuild step runs tsc and then api-extractor, which is how the type declarations are produced and kept in sync with the public surface, and there is a separate api-extractor.json configuration file for that. Vitest runs the unit tests, eslint handles linting, and the test script runs both.

The container, the video element and the styling you have to write yourself

The usage pattern is small. You need a video element, a container element for the subtitles, and the ASS content itself, which the README shows being fetched as text and passed to the constructor.

js
import ASS from 'assjs';

const content = await fetch('/path/to/example.ass').then((res) => res.text());
const ass = new ASS(content, document.querySelector('#video'), {
  container: document.querySelector('#ass-container'),
});

The constructor appends several elements to the container and syncs the render area's size with the video element. What it does not do is position anything, and the README is blunt that you should set the styles yourself to make sure the container overlaps the video and matches the position.

html
<div id="player" style="position: relative;">
  <video id="video" src="./example.mp4" style="position: absolute; top: 0; left: 0;"></video>
  <div id="ass-container" style="position: absolute; top: 0; left: 0;"></div>
</div>

The relative parent with absolutely positioned children is the whole layout mechanism. That is a small amount of CSS, and it is also a small amount of CSS that you have to get right yourself, which is worth knowing before you blame the renderer for subtitles in the wrong place.

The other trap in this section is fullscreen. If a user clicks the native fullscreen button on the video element, only the video goes fullscreen and ASS does not show, because the subtitle container is a sibling. The README's fix is to use a custom button and call requestFullscreen on the wrapper element instead.

Resampling decides what happens when script and video sizes disagree

ASS scripts carry their own resolution through PlayResX and PlayResY, and the video has its own. The resampling option defines how the mismatch is handled, and the README walks through all four values with a worked example where the video is 1280x720 and the script is 640x480.

With video_width, the script resolution is set from the video width, so the script becomes 640x360 and the scale is 2. With video_height, which the README marks as the default, the script becomes 853.33x480 and the scale is 1.5. With script_width, the script resolution does not change and the scale is based on the script width, which the README warns may cause top and bottom subtitles to disappear from the video area. With script_height, the script resolution does not change, the scale is based on the script height, and the script area is centred in the video area.

js
ass.resampling = 'video_width';

One caveat sits at the end of that section: drawings and clips are always dependent on the script origin resolution, whatever the resampling setting is. So if your subtitle file contains vector drawings or clipped regions, changing resampling will not move them.

The setting is a property on the instance, so it can be changed after construction.

Show, hide, destroy and a delay you can push either way

The rest of the API is three methods and one property, which is a small surface for a renderer with this much underneath it.

js
ass.show();
js
ass.hide();
js
ass.destroy();

Show and hide control the visibility of the rendered subtitles, and destroy tears the instance down, which matters if your page is a single page application and the player is created and discarded as the user navigates. The README does not document what destroy does to the elements it appended to the container, so if you rely on it to clean the DOM completely, check the source.

The delay property is a playback offset in seconds, and it works in both directions.

js
// Subtitles will be 5s later
ass.delay = 5;
// Subtitles will be 3s earlier
ass.delay = -3;

That is a subtitle synchronisation feature rather than a rendering one, and it is the kind of thing you need when your video starts with a slate, or when the audio track and the video track are not aligned in the source file. The README does not say whether the delay is applied to the timing of the cues or to the playback clock, so if you depend on its exact behaviour, verify it against your own player.

The compatibility table is the real specification of what works

ASS.js uses many Web APIs to render subtitles, and the README says some features will be disabled if you use an old browser. The table that follows is the most useful thing in the documentation, because it maps each subtitle feature to the Web API it needs and to the browser versions that have it.

Auto resize needs ResizeObserver, available in Chrome 64, Firefox 69 and Safari 13.1. The \[i]clip tag needs clip-path and path(), so Chrome 88, Firefox 97 and Safari 13.1. Animations with \t need registerProperty, so Chrome 78, Firefox 128 and Safari 16.4. Acceleration inside \t needs linear(), so Chrome 113, Firefox 112 and Safari 17.2. The \q0 tag needs text-wrap balance, so Chrome 114, Firefox 121 and Safari 17.5. And \blur combined with \bord0 needs round(), so Chrome 125, Firefox 118 and Safari 15.4.

The pattern in that table is worth noticing. The core features sit in the low version numbers, and the more advanced tags sit in much higher ones, with the Firefox animation row at 128 being the highest requirement listed anywhere.

So the honest reading is that ASS.js renders well on current browsers, and that specific tags quietly stop working on older ones rather than throwing an error. If your audience includes machines on old operating systems, this table is where you find out which tags to avoid, and the answer is to test the specific files you ship rather than assume a feature is present.

MIT licensed, version 0.1.x, and still pre-1.0

The licence is MIT, recorded in package.json and with the text in the LICENSE file, and the badge in the README points at the licence in the source repository. That is the permissive end of the scale and it does not impose anything on a site that uses the library.

The version is the thing to notice. The package is at 0.1.10, with 0.1.9 and 0.1.8 before it, and the recent releases in the repository are dated 2026-08-15, 2026-07-19 and 2026-06-12. A 0.x version carries the usual meaning: the API can still change between releases, and the project is explicit about how far along it is.

On maintenance, the repository is not archived and the last push was on 2026-08-15, with the release tag from the same day, so the tags and the source are in step. The release cadence is roughly monthly.

The repository layout is a small TypeScript project with a src/ directory, a dist/ directory that is committed, test/, a rollup.config.js, a tsconfig.json, a vitest.config.js, an eslintrc configuration and an api-extractor.json. The dev dependencies include playwright and the vitest browser mode, so the tests run in a real browser rather than in a simulated DOM, which is the only sensible way to test something that depends on ResizeObserver, clip-path and registerProperty.

The test script runs lint and unit tests together, and there is a coverage script on top of vitest with the v8 provider, matching the Codecov badge in the README.

Editorial conclusion

Adopt ASS.js if you need ASS subtitles on HTML5 video in a browser and care about bundle size, CJK font fallback and avoiding a WebAssembly worker, and you can accept rendering that is close to but not pixel identical with VSFilter or libass. Do not adopt it when subtitle accuracy must match a reference renderer exactly, or when your target browsers fall below the versions listed for the features you need. Before you ship, check the browser compatibility table for the tags you actually use, and if the player uses the native fullscreen button, replace it with a custom one that calls requestFullscreen on the wrapper as the README describes.

Frequently asked questions

How do I install ASS.js?

Install it from npm with npm install assjs. The package is published as assjs, with the module entry at dist/ass.js, the global build at dist/ass.global.min.js and type declarations at dist/ass.d.ts.

Why is ASS.js smaller than WebAssembly subtitle renderers?

ASS.js renders with the DOM instead of shipping a libass WebAssembly port and a worker. The README describes it as 60 times smaller than WebAssembly solutions, and it also avoids the need to set a fallback font to avoid CJK characters turning into tofu, since the browser handles fallback.

Does ASS.js render subtitles identically to VSFilter or libass?

No. The README says it is almost impossible for the DOM to render exactly the same result in every pixel as VSFilter or libass, and that ASS.js provides best efforts to accurate rendering.

Which ASS features need a recent browser?

The README's compatibility table maps features to Web APIs: ResizeObserver for auto resize, clip-path and path() for the \[i]clip tag, registerProperty() for animations, linear() for acceleration, text-wrap balance for \q0, and round() for \blur with \bord0. Each row lists the Chrome, Firefox and Safari versions required.

How do I set the subtitle position and size in ASS.js?

You set them yourself with CSS. The README shows a relative wrapper div holding the video and the ass-container as absolutely positioned children, and says new ASS() appends elements to the container and syncs the render area size with the video element but does not position the container for you.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. weizhenye/ASS on GitHub
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/weizhenye-ass.svg)](https://hysenlabs.com/projects/weizhenye-ass)

Was this useful?