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-engine

React 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-dom

Quick 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.json

Browse 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.json

Hit 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.mp3

Custom 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>
  );
}