live2d-companion
v1.0.4
Published
Live2D Companion component for web and React using PixiJS v8
Readme
live2d-companion
A drop-in Live2D character companion for the web. Built on PixiJS v8 and the untitled-pixi-live2d-engine.
Ships a framework-agnostic core class and a ready-made React component. Works with Next.js App Router out of the box.
Features
- Model rendering — Point at any
.model3.jsonor Cubism 2.model.jsonand the character appears on a transparent canvas. - Seamless Model Switching — Change models on-the-fly via React props or
.loadModel()without WebGL context loss or DOM flickering. - Dynamic Canvas Resizing — Canvas & renderer automatically resize according to model metadata presets or explicit layout props.
- Companion Metadata Registry — Auto-configures canvas dimensions, scale, anchor points, and hit area maps when a model JSON defines a
companionblock. - Touch interactions — Tap hit areas on the model to trigger motion animations.
- Eye & head tracking — The character follows the user's cursor.
- Speech bubbles — Prop-driven dialogue appears above the character on interaction.
- Audio playback — Plays voice clips when the character reacts (optional).
- Idle behavior — The character periodically plays idle animations with random remarks when you provide idle dialogue.
- Event bus — Trigger motions from anywhere in your app with
companionBus.
Installation
npm install live2d-companion pixi.js untitled-pixi-live2d-engine
pixi.jsanduntitled-pixi-live2d-engineare peer dependencies. You must install them alongside this package.
React projects
# React 18 or 19 both work
npm install live2d-companion pixi.js untitled-pixi-live2d-engine react react-domQuick Start
React (Next.js / Vite / CRA)
'use client'; // Required for Next.js App Router
import { Live2DCompanion } from 'live2d-companion/react';
import "live2d-companion/react.css";
export default function MyCompanion() {
return (
<div style={{ position: 'fixed', bottom: 0, right: 0, zIndex: 50 }}>
<Live2DCompanion
modelUrl="https://cdn.jsdelivr.net/gh/LPH1110/live2d-registry@main/honkai-impact/Kiana/model.json"
/>
</div>
);
}That's it. The character renders, tracks your cursor, and responds to clicks. If you want speech bubbles or idle remarks, pass them through dialogueData.
Tip:
modelUrlaccepts both local paths (/models/character/model.model3.json) and full CDN URLs. Using a CDN link is the fastest way to get started — no model files to download.
Dynamic Model Switching (React)
You can dynamically change characters simply by updating state. The companion will reuse the WebGL context and adjust canvas size automatically:
import { useState } from 'react';
import { Live2DCompanion } from 'live2d-companion/react';
import "live2d-companion/react.css";
const MODELS = [
'https://cdn.jsdelivr.net/gh/LPH1110/live2d-registry@main/honkai-impact/Nina/model.json',
'https://cdn.jsdelivr.net/gh/LPH1110/live2d-registry@main/honkai-impact/Kiana/model.json',
];
export default function CompanionSwitcher() {
const [modelIndex, setModelIndex] = useState(0);
return (
<div>
<button onClick={() => setModelIndex((prev) => (prev + 1) % MODELS.length)}>
Switch Character
</button>
<Live2DCompanion modelUrl={MODELS[modelIndex]} />
</div>
);
}Adding your own conversations
<Live2DCompanion
modelUrl="/models/character/model.model3.json"
dialogueData={{
interactions: {
flick_head: ["Hey, that's a little tickly!", "Please be gentle~"],
tap_face: ["My face is right here.", "A little warning next time?"],
},
ui_elements: {
shake: ["Let's go!", "Ready when you are."],
},
idleRemarks: ["I'm waiting here for your next command.", "What should we explore next?"],
}}
/>There are no built-in conversation strings in the package anymore. Everything shown in the bubble comes from the props you pass in.
Vanilla TypeScript / JavaScript
import { Live2DCompanionCore } from 'live2d-companion';
import "live2d-companion/react.css";
const canvas = document.getElementById('live2d-canvas') as HTMLCanvasElement;
const companion = new Live2DCompanionCore(canvas, {
audioBaseUrl: '/audio',
volume: 0.5,
});
companion.onLoaded = () => console.log('Model ready');
companion.onResize = (w, h) => console.log(`Canvas resized to ${w}x${h}`);
companion.onBubbleMessage = (msg) => {
// msg is a string when showing, null when hiding
document.getElementById('bubble')!.textContent = msg ?? '';
};
// Initial model load
companion.init('/models/my_character/model.model3.json');
// Switch models dynamically at any time without destroying the renderer
// companion.loadModel('/models/another_character/model.model3.json');
// Clean up when done
// companion.destroy();Preparing Your Model
Option A: Use a CDN (fastest)
You can skip downloading model files entirely by pointing modelUrl to a CDN-hosted model. GitHub repos containing open-source Live2D models can be served directly via jsDelivr:
// Kiana
<Live2DCompanion modelUrl="https://cdn.jsdelivr.net/gh/LPH1110/live2d-registry@main/honkai-impact/Kiana/model.json" />Browse available models:
- LPH1110/live2d-registry — Large collection of community models.
Note: CDN-hosted models load all assets (textures, motions, physics) relative to the
model.jsonURL. As long as the entire model directory is in the same repo, it works automatically.
Option B: Self-host model files
For production use or if you need custom models, host them yourself. A typical model directory looks like this:
public/
└── models/
└── my_character/
├── my_character.model3.json ← Point modelUrl here
├── my_character.moc3
├── my_character.physics3.json
├── my_character.cdi3.json
├── textures/
│ └── texture_00.png
└── motions/
├── shake.motion3.json
├── flick_head.motion3.json
├── tap_face.motion3.json
└── idle.motion3.jsonPlace this folder in your project's public/ directory (or any static file server) so it's accessible via URL.
Where to get models
- Live2D Sample Models: Official free models from Live2D Inc.
- Booth.pm: Community marketplace with free and paid models.
- Create your own: Use Live2D Cubism Editor (free tier available).
Important: Respect model licenses. Many models are free for personal use but require attribution or prohibit commercial use.
Hit areas
Hit areas are defined inside the model's model.json file under HitAreas. Common names are head, face, breast, belly, and leg. This package maps each hit area to a motion animation via the hitAreaMap option.
If your model uses different hit area names, override the mapping:
<Live2DCompanion
modelUrl="/models/custom/model.json"
hitAreaMap={{
'Head': 'nod', // When user taps the "Head" hit area, play the "nod" motion
'Body': 'wave', // "Body" → "wave"
'Skirt': 'surprised', // Custom hit areas work too
}}
/>Motion names
Motion names correspond to the motion group names in your model.json. Open the file and look for the "Motions" section:
{
"Motions": {
"shake": [{ "File": "motions/shake.motion3.json" }],
"flick_head": [{ "File": "motions/flick_head.motion3.json" }],
"idle": [{ "File": "motions/idle.motion3.json" }]
}
}Use these exact group names when configuring hitAreaMap or calling companionBus.emit().
Customizing a model with props
You can tune the model's look and behavior without changing the library code:
<Live2DCompanion
modelUrl="/models/character/model.model3.json"
bodyMode="partialBody"
canvasWidth={420}
canvasHeight={560}
modelScale={1.05}
modelAnchor={[0.5, 1]}
hitAreaMap={{
head: 'flick_head',
face: 'tap_face',
breast: 'tap_breast',
belly: 'tap_belly',
leg: 'tap_leg',
}}
dialogueData={{
interactions: {
flick_head: ["Hey!", "That tickles a little."],
},
ui_elements: {
shake: ["Let's begin!"],
},
idleRemarks: ["I'll be here when you're ready."],
}}
/>For model authors, the packaged metadata can also provide a companion block inside the model JSON to override canvas size, scale, anchor, and hit-area mapping at runtime. This is useful when shipping a custom model registry with opinionated defaults.
Configuration Reference
Every option has a sensible default. Pass only what you want to override.
React Component Props
<Live2DCompanion
// Required
modelUrl="/models/character/model.model3.json"
// Layout (React wrapper only)
width="100%" // Container width — default: '100%'
height="100%" // Container height — default: '100%'
className="" // CSS class on the outer container div
bubbleClassName="" // CSS class on the speech bubble div
bubbleStyle={{}} // Replace the default bubble inline styles entirely
// Canvas & Model
canvasWidth={300} // Internal canvas pixel width — default: 300
canvasHeight={400} // Internal canvas pixel height — default: 400
modelScale={0.1} // Model scale factor — default: 0.1
modelAnchor={[0.5, 1]} // [x, y] anchor point — default: [0.5, 1] (bottom-center)
// Interaction
hitAreaMap={{ // Map hit area names → motion group names
head: 'flick_head', // default mapping shown here
face: 'tap_face',
breast: 'tap_breast',
belly: 'tap_belly',
leg: 'tap_leg',
}}
// Audio
audioBaseUrl="/companion" // Base path for audio files — default: '/companion'
volume={0.6} // Audio volume (0.0 to 1.0) — default: 0.6
audioPathResolver={ // Custom function to resolve audio file paths
(base, category, motion, index) =>
`${base}/${category}/${motion}/${index}.mp3`
}
// Dialogue (opt-in)
dialogueData={{ // Only used when you want speech bubbles
interactions: { // Shown when user taps the model directly
flick_head: ["Ouch!", "Hey!"],
},
ui_elements: { // Shown when triggered via companionBus
shake: ["Let's go!"],
},
idleRemarks: [ // Shown during idle animations
"Still there?",
"...",
],
}}
// Timing
idleIntervalMinMs={30000} // Min idle interval (ms) — default: 30000
idleIntervalMaxMs={60000} // Max idle interval (ms) — default: 60000
motionDebounceMs={500} // Min time between event bus motions — default: 500
// Cubism SDK
cubism2CoreUrl="https://..." // CDN URL for Cubism 2 runtime
cubism4CoreUrl="https://..." // CDN URL for Cubism 4 runtime
/>Vanilla Class Constructor & Methods
const companion = new Live2DCompanionCore(canvas, {
// Same options as above, minus: width, height, className, bubbleClassName, bubbleStyle
// (those are React-only layout props)
});
companion.onLoaded = () => { /* model is ready */ };
companion.onResize = (width, height) => { /* canvas dimensions updated */ };
companion.onBubbleMessage = (msg: string | null) => { /* render your own bubble UI */ };
await companion.init('/models/character/model.model3.json');
// Update options or dialogue dynamically
companion.updateOptions({ volume: 0.8 });
// Switch model dynamically without context loss
await companion.loadModel('/models/another_character/model.model3.json');
// Later:
companion.destroy();Event Bus
The companionBus lets any part of your app trigger character animations. It uses DOM CustomEvent under the hood, so it works across any framework.
import { companionBus } from 'live2d-companion/react';
// or
import { companionBus } from 'live2d-companion';companionBus.emit(motion, priority?)
Triggers a full interaction: animation + audio + speech bubble.
<button onClick={() => companionBus.emit('shake')}>
Click me
</button>
// With priority (higher = more likely to interrupt current animation)
companionBus.emit('flick_head', 5);companionBus.emitOnHover(motion, priority?)
Triggers animation only (no audio), with a built-in 30% probability gate. Useful for hover effects so the character doesn't react to every mouse movement.
Automatically skipped on touch devices.
<div
onMouseEnter={() => companionBus.emitOnHover('tap_face')}
>
Hover over me
</div>Supported motions
The built-in motion type includes: 'shake', 'flick_head', 'tap_face', 'tap_breast', 'tap_belly', 'idle'.
You can also pass any custom string — it will be forwarded directly to model.motion(). As long as the motion group exists in your model's .model3.json, it will play.
companionBus.emit('my_custom_dance'); // Works if the model has a "my_custom_dance" motion groupAudio Setup (Optional)
Audio is optional. The companion works fine without any audio files — it will just show the speech bubble text silently.
If you want voice clips, organize them to match this directory structure:
public/
└── companion/ ← This is your audioBaseUrl
├── interactions/ ← Played when user taps the model
│ ├── flick_head/
│ │ ├── 1.mp3
│ │ ├── 2.mp3
│ │ └── 3.mp3
│ ├── tap_face/
│ │ ├── 1.mp3
│ │ └── 2.mp3
│ └── tap_belly/
│ └── 1.mp3
└── ui_elements/ ← Played when triggered via companionBus
├── shake/
│ ├── 1.mp3
│ └── 2.mp3
└── flick_head/
└── 1.mp3The file names (1.mp3, 2.mp3, etc.) correspond to the index of the dialogue text array. If your dialogueData.interactions.flick_head has 3 entries, you need 1.mp3, 2.mp3, and 3.mp3.
Custom audio path format
If your audio files follow a different naming convention, provide a custom audioPathResolver:
<Live2DCompanion
modelUrl="/model.json"
audioBaseUrl="/voices"
audioPathResolver={(base, category, motion, index) =>
`${base}/${motion}_${category}_${index}.wav`
// Resolves to: /voices/flick_head_interactions_1.wav
}
/>Styling the Speech Bubble
Using CSS classes
The speech bubble div gets the class live2d-bubble by default. Add your own class to style it:
<Live2DCompanion
modelUrl="/model.json"
bubbleClassName="my-custom-bubble"
/>.my-custom-bubble {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 12px 20px;
border-radius: 20px;
font-family: 'Comic Sans MS', cursive;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}Replacing inline styles entirely
Pass bubbleStyle to completely override the default dark-theme inline styles. When bubbleStyle is provided, the default tail triangle is also removed so you have full control:
<Live2DCompanion
modelUrl="/model.json"
bubbleStyle={{
position: 'absolute',
top: '-2rem',
left: '50%',
transform: 'translateX(-50%)',
background: 'white',
color: '#333',
padding: '10px 18px',
borderRadius: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
fontSize: '14px',
}}
/>Examples
Minimal (just a model, no audio)
<Live2DCompanion modelUrl="/models/haru/haru.model3.json" />Custom character personality
<Live2DCompanion
modelUrl="/models/robot/robot.model3.json"
dialogueData={{
interactions: {
flick_head: ["Sensor array nominal.", "Scanning..."],
tap_face: ["Facial recognition active.", "Identity confirmed."],
},
ui_elements: {
shake: ["Executing protocol.", "Acknowledged."],
},
idleRemarks: [
"Awaiting instructions...",
"All systems nominal.",
"Running diagnostics...",
],
}}
/>Large canvas with custom scale
<Live2DCompanion
modelUrl="/models/character/model.model3.json"
canvasWidth={600}
canvasHeight={800}
modelScale={0.2}
modelAnchor={[0.5, 0.9]}
/>Trigger from a navigation bar
import { companionBus } from 'live2d-companion/react';
function NavLink({ href, label }: { href: string; label: string }) {
return (
<a
href={href}
onMouseEnter={() => companionBus.emitOnHover('flick_head')}
onClick={() => companionBus.emit('shake')}
>
{label}
</a>
);
}Peer Dependencies
| Package | Version |
|---|---|
| pixi.js | ^8.0.0 |
| untitled-pixi-live2d-engine | ^1.3.0 |
| react | ^18.0.0 \|\| ^19.0.0 |
| react-dom | ^18.0.0 \|\| ^19.0.0 |
reactandreact-domare only required if you use thelive2d-companion/reactentry point.
License
MIT
