# l2d-widget: a Live2D character in one function call

> l2d-widget is a small TypeScript package that puts a Live2D character on any web page through a single createWidget call, with no framework and no runtime dependencies. It suits front-end teams that want a mascot or guide without wiring canvas, WebGL and model loading by hand, and it carries the migration caveats of a package that was recently renamed and rewritten.

**hacxy/l2d-widget** — Drop a Live2D character onto any web page. One function call, zero framework dependencies.

- Repository: https://github.com/hacxy/l2d-widget
- Website: https://l2d-widget.hacxy.cn
- Stars: 643 · Forks: 40
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/hacxy-l2d-widget

## A mascot widget without a framework or a runtime dependency list

The pitch in the package description is short: drop a Live2D character onto any web page, one function call, zero framework dependencies. The implementation is TypeScript, published on npm as l2d-widget, MIT licensed, with a documentation site at l2d-widget.hacxy.cn and a Chinese README alongside the English one.

The audience is front-end developers who want a Live2D figure in a page. That could be a guide character, a shop mascot or a chat widget. It is not a game engine and not a general Live2D authoring tool. The project ships code to embed a model, not tools to make one.

The dependency situation is the part worth stating up front, because it is unusual for the Live2D ecosystem. The feature list describes the package as roughly 500 lines with zero runtime dependencies, built on pure DOM and CSS Animation, outputting ESM and IIFE bundles that are tree-shakeable. The underlying Live2D runtime lives in a separate library, l2d, and in package.json the l2d package appears under devDependencies rather than dependencies, with the built output bundled for consumers.

That is a design choice with a consequence: you get a small bundle, but the Live2D runtime code travels with it rather than being shared with another copy already on your page.

## createWidget owns the canvas, the WebGL context and the model

The integration surface is one function. The feature list describes createWidget as handling canvas creation, WebGL context initialization, model loading and interaction binding in one call, and the quick start shows how little it takes:

```ts
import { createWidget } from 'l2d-widget';

const widget = createWidget({
  model: {
    path: 'https://model.hacxy.cn/cat-black/model.json',
  },
});
```

The model option takes a path to a model.json file. The result appears in the bottom-left corner of the page with a hover menu and tip bubbles, and the returned object gives programmatic control over switching models, sleeping and destroying.

Underneath, the widget is built on the l2d library, which auto-detects the model version and loads the matching Cubism runtime. The widget exposes that underlying instance as the l2d property on the returned object, so anything the lower level library can do remains reachable from the higher level API.

The returned Widget object has a small, readable surface: l2d for the underlying instance, switchModel(index) to change models by index, sleep() to hide the model with a click on the status bar to wake it, and destroy() to tear the widget down and release resources. The fuller option reference, covering WidgetOptions, ModelOptions, MenusOptions and TipsOptions, lives in the documentation site rather than in the package documentation, which is a limitation for anyone reading only the repository.

## Installing from npm or from a CDN script tag

The package is on npm, and the README gives the standard install line:

```bash
npm install l2d-widget
```

The package entry points confirm this is a modern bundler friendly build: main points at dist/index.min.js, module at dist/index.js and types at dist/index.d.ts, with only the dist directory published. A CDN route is offered as well, which is the quickest way to try the widget on a plain page with no build step:

```html
<script src="https://unpkg.com/l2d-widget/dist/index.min.js"></script>
```

That script tag loads the IIFE bundle. The project is developed with pnpm, and package.json pins pnpm@11.21.0 as the package manager, with a pnpm-lock.yaml and pnpm-workspace.yaml at the root. The local commands are a watch build with sourcemaps, a demo server at localhost:3000, a production build and a lint task, plus a typecheck script that runs tsc with no emit.

After install, the smallest meaningful first use is the createWidget call above with a model path. Start with a single model rather than a list, so that a failure points at the model file or the runtime rather than at the multi-model wiring.

## Lip-sync works by driving a mouth parameter per character

The most distinctive feature is not the rendering, it is the typing animation. The feature list describes lip-sync via parameter driving: character-by-character typing animation drives the mouth open and close parameter, PARAM_MOUTH_OPEN_Y, in real time, with configurable value ranges. The tip configuration also carries welcome messages, a message list, a duration and an interval:

```ts
createWidget({
  model: {
    path: '/models/cat-black/model.json',
    tips: {
      typing: {
        param: 'PARAM_MOUTH_OPEN_Y',
        speed: 200,
      },
      welcomeMessage: ['Hello!', 'Nice to meet you!'],
      messages: ['Take a break~', 'Stay hydrated!'],
      duration: 4000,
      interval: 6000,
    },
  },
});
```

Because the animation is expressed as a standard Live2D parameter name, the behaviour is not limited to that one parameter. Any parameter your model exposes can be the driver, which is the difference between a lip-sync feature and a generic parameter animation system.

The speed value sets the cadence of the simulated typing, duration is how long a tip stays on screen, and interval is how often the background messages appear. These are the knobs that decide whether the character feels like a companion or a nuisance, and the documentation gives no guidance on tuning them.

## switchModel is atomic, destroy is the cleanup you must remember

Model switching is where a naive implementation usually leaks WebGL resources. The README describes switchModel() as running an exit transition, then WebGL teardown, then re-initialisation, then an entrance transition, as one atomic async operation. The point of the atomic framing is that the intermediate states are not observable, so a user clicking through models quickly does not end up with several live canvases.

Multiple models are declared as an array, and a shuffle button appears automatically in the menu when more than one model is provided:

```ts
createWidget({
  model: [
    { path: '/models/cat-black/model.json' },
    { path: '/models/cat-white/model.json' },
  ],
});
```

With switchModel taking an index, the caller decides which entry becomes current, and the shuffle button is the built-in way to move between them.

The other half of the contract is destroy(), which the documentation describes as guaranteeing resource cleanup. This matters most for single page applications and for any route change where the widget element is removed from the DOM. The documentation does not describe what happens if destroy() is not called, nor whether createWidget is safe to call twice on the same page, so treat the widget as a resource you own for the lifetime of the view and release it explicitly.

## A rename and a rewrite that will bite migrating code

The project carries an important notice. l2d-widget is a full rewrite of oh-my-live2d, based on the l2d library, and the original oh-my-live2d package has been renamed to l2d-widget. Anyone migrating from oh-my-live2d is pointed at the documentation for the updated API.

That is the single biggest adoption risk, and it is easy to miss because the package documentation reads like a small, tidy library. The package is also young: package.json shows version 0.1.2, and the release history is v0.1.0 on 2026-05-25, v0.1.1 on 2026-07-27 and v0.1.2 on 2026-08-12. The repository is not archived and the last push was on 2026-08-12.

A version number below 1.0 tells you the API can still move, and the rename means an existing oh-my-live2d integration is not a drop-in upgrade. Read the migration notes in the documentation before assuming your import paths and option names carry over.

On the Cubism versions, the picture is unclear from the repository alone. The feature list says the runtime covers Cubism 2 and 6 and auto-detects the model version, while the repository topics list cubism2, cubism4 and cubism5. If your model is a Cubism 4 or 5 asset, verify against the documentation before you build on it.

## Licence, tooling and what a maintainer has to keep up with

The licence is MIT, with the LICENSE file at the root of the repository, and there is no additional commercial condition layered on top of it. For a widget embedded on a public page that is the easy part of the decision.

The heavier question is operational. The package bundles a Live2D runtime and ships models or references to model hosting such as the model.hacxy.cn domain used in the quick start, so a deployment depends on external model hosting unless you point the path at your own assets. The documentation does not say how models are licensed or whether you are allowed to redistribute them, so if you self-host models, that question has to be answered outside this repository.

The engineering side is small but not free. The repository carries a changelog, typedoc and vitepress configuration for the docs site, eslint config and a __tests__ directory, and the release process runs through a shell script with git hooks installed by simple-git-hooks and commit messages shaped by commitizen and cz-git. The published files list contains only dist, so consumers never see the tests, and upgrading means picking up a new dist bundle with whatever API changes came with it. Version 0.1.2 and a rename from oh-my-live2d are the two facts to weigh before you depend on this package in a product.

## Conclusion

Adopt l2d-widget if you want a Live2D character on a page you already control, in plain DOM, without pulling in a UI framework, and if you are starting fresh rather than porting an oh-my-live2d integration. Skip it if you already depend on the old package name and cannot absorb the rename and the API change, or if you need Cubism 4 or 5 support that the repository topics do not clearly claim. Before you commit, confirm which Cubism runtime your model file targets, check that createWidget is not called twice on the same page without destroy(), and read the documentation for the options reference, since the README covers only the common cases.

## FAQ

### How do I add a Live2D character to a web page with l2d-widget?

Install the package with npm install l2d-widget, then call createWidget with a model path such as the model.json URL in the quick start. The widget appears in the bottom-left corner of the page with a hover menu and tip bubbles.

### Does l2d-widget work without React, Vue or another framework?

Yes. The package is described as pure DOM and CSS Animation with zero runtime dependencies and no framework, and it outputs ESM and IIFE bundles that are tree-shakeable, so a script tag from a CDN is also a valid way to load it.

### What is the difference between l2d-widget and oh-my-live2d?

l2d-widget is a full rewrite of oh-my-live2d based on the l2d library, and the original oh-my-live2d package has been renamed to l2d-widget. Migrating users are pointed to the documentation for the updated API rather than the old import names.

### How do I clean up the widget when the page changes?

The returned widget object exposes destroy(), which the documentation describes as guaranteeing resource cleanup, along with switchModel(index), sleep() and the underlying l2d instance. The documentation does not state what happens if destroy() is skipped, so release it when the view that owns it goes away.

## Sources

- [hacxy/l2d-widget on GitHub](https://github.com/hacxy/l2d-widget)
- [License: MIT](https://github.com/hacxy/l2d-widget/blob/main/LICENSE)
- [Project website](https://l2d-widget.hacxy.cn)
- [README](https://github.com/hacxy/l2d-widget/blob/main/README.md)
- [Releases](https://github.com/hacxy/l2d-widget/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hacxy-l2d-widget
