Package Installation
pixi.js and untitled-pixi-live2d-engine are peer dependencies. You must install them alongside this package.
npm install live2d-companion pixi.js untitled-pixi-live2d-engineReact Projects
Supports React 18 or 19. Install standard React peer dependencies if not already present:
npm install live2d-companion pixi.js untitled-pixi-live2d-engine react react-domQuick Start
React (Next.js / Vite / CRA)
Drop the companion component anywhere in your client-side React tree. Ensure the CSS file is imported.
'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/kiana_kaslana/model.json"
/>
</div>
);
}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/nina/model.json',
'https://cdn.jsdelivr.net/gh/LPH1110/live2d-registry@main/kiana_kaslana/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 Conversations
Define dialogues for interactions, user actions, or idle states. The speech bubbles display text from the props you pass.
<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?"],
}}
/>Vanilla TS / JS
For projects without React, use the framework-agnostic core class directly on a canvas element.
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');Model Preparation
Option A: CDN (Fastest)
Point modelUrl to a GitHub repo via jsDelivr. CDN-hosted models load all assets (textures, motions, physics) relative to the JSON URL automatically.
https://cdn.jsdelivr.net/gh/LPH1110/live2d-registry@main/kiana_kaslana/model.jsonBrowse community models: LPH1110/live2d-registry
Option B: Self-Host Model Files
Place the model folder in your project's public/ directory or static server:
public/
└── models/
└── my_character/
├── my_character.model3.json ← Point modelUrl here
├── my_character.moc3
├── my_character.physics3.json
├── textures/
│ └── texture_00.png
└── motions/
├── shake.motion3.json
└── idle.motion3.jsonHit Areas
Hit areas defined in the model's JSON file map to motion animation names:
<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 motion group names defined inside the model.json.
Customizing a Model with Props
Configure scaling, anchoring, and layout options directly on the component:
<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."],
}}
/>Configuration Ref
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 Core Class
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
import { companionBus } from 'live2d-companion/react';
// or
import { companionBus } from 'live2d-companion';Emit Interaction
Triggers animation, audio, and dialogue bubble together:
// Trigger 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);Emit on Hover
Triggers animation only (no audio) with a 30% gate. Automatically skipped on touch devices:
// Trigger animation only (no audio), with a 30% probability gate.
// Automatically skipped on touch devices.
<div
onMouseEnter={() => companionBus.emitOnHover('tap_face')}
>
Hover over me
</div>Audio System
Audio is optional. Organize voice clips in your audioBaseUrl to match the dialogue arrays.
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.mp3Custom Audio Path resolver
Override default naming conventions using the 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 Bubble
Customize the speech bubble using CSS classes or fully override inline styles.
Using CSS Classes
<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
Pass bubbleStyle to completely override default bubble css styles (this also disables the default tail triangle):
<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 setup
<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 Layout
<Live2DCompanion
modelUrl="/models/character/model.model3.json"
canvasWidth={600}
canvasHeight={800}
modelScale={0.2}
modelAnchor={[0.5, 0.9]}
/>Trigger from 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>
);
}