NarraLeaf

Dialog

The Dialog component renders the dialog box, including the character nametag, dialog text, and the optional dialog avatar (small portrait).

To customize the dialog, provide your own component via game.configure({ dialog }). You can compose Avatar, Nametag, and Texts, or use useAvatar / useDialog for layout logic.

Use Avatar for the default portrait slot. Use useAvatar() when the dialog layout should change depending on whether an avatar is available.

Quick example

  1. Import the components
import { Dialog, Texts, Nametag, useGame } from "narraleaf-react";
  1. Compose the default ADV layout (optional Avatar)
import { Avatar, Dialog, Nametag, Texts } from "narraleaf-react";

function GameDialog() {
    return (
        <Dialog className="bg-white">
            <div className="dialog-content flex items-start gap-4 w-full h-full">
                <Avatar />
                <div className="dialog-text-content min-w-0 flex-1">
                    <Nametag className="font-bold" />
                    <Texts className="text-lg" />
                </div>
            </div>
        </Dialog>
    );
}

The built-in DefaultDialog uses the same structure: flex row with Avatar, then a column with Nametag and Texts.

  1. Configure the game
function App() {
    const game = useGame();

    useEffect(() => {
        game.configure({
            dialog: GameDialog,
        });
    }, []);

    return /* ... */
}

Components

Dialog

Renders the dialog container. Children are typically Avatar, Nametag, and Texts (any layout you prefer).

  • children?: React.ReactNode — Dialog content.
  • ...props: HTMLMotionProps<"div"> — Passed to the inner motion.div wrapper. This includes normal div props plus Motion props such as initial, animate, exit, transition, layout, and Motion event handlers.

The outer dialog wrapper is kept separate for aspect-ratio scaling and click/key handling. Put visual transforms on Dialog itself; the player will apply them to the inner motion element.

Exported prop types: NametagProps, TextAppearanceProps, TextsProps, RawTextsProps, and EntryTextsProps.

Nametag

Renders the speaker name. Must be used inside Dialog.

Nametag is now component-driven: pass props to control its content and presentation. If no custom content is provided, it falls back to the current speaker name. If no color prop is provided, it falls back to the current character color when available.

  • character?: Character | null — Explicit character source.
  • entry?: NvlDialogEntry — NVL entry source.
  • name?: React.ReactNode — Override the displayed name.
  • color?: Color — Text color for the nametag.
  • children?: React.ReactNode — Fully custom nametag content.
  • ...props: Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "color">

Texts

Renders the typed dialogue text. Must be used inside Dialog.

Text defaults are component props or inherited CSS. Sentence and Word style overrides still win for the individual line or word.

  • children?: never
  • defaultColor?: Color — Fallback text color when sentence/word color is not set.
  • fontSize?: React.CSSProperties["fontSize"]
  • fontWeight?: React.CSSProperties["fontWeight"]
  • fontWeightBold?: React.CSSProperties["fontWeight"] — Weight used when a sentence/word is bold.
  • fontFamily?: React.CSSProperties["fontFamily"]
  • ...props: React.HTMLAttributes<HTMLDivElement>

Avatar

Renders the resolved dialog avatar image for the current line, or nothing when no avatar is resolved (no layout gap).

  • ...props: React.ImgHTMLAttributes<HTMLImageElement> — Merged with defaults (see useAvatar).

Default image style:

  • width / height: 96
  • objectFit: "cover"
  • borderRadius: 6
  • flex: "0 0 auto"

Dialog motion props

Custom dialog components can pass Motion props directly to Dialog. The built-in DefaultDialog does not add enter or exit animation by default.

import { Dialog, Nametag, Texts } from "narraleaf-react";

function GameDialog() {
    return (
        <Dialog
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -20 }}
            transition={{ duration: 0.25, ease: "easeOut" }}
            layout
        >
            <Nametag />
            <Texts />
        </Dialog>
    );
}

Presence timing is handled by the player:

  • Direct dialogue-to-dialogue replacement keeps the same visual dialog slot alive. The previous line does not run exit, and the next line does not restart the enter animation just because text or nametag changed.
  • Dialogue-to-non-dialogue content removes the active dialog from the current text list, so exit can continue while the story advances.
  • If a new dialogue appears while an older exit animation is still running, the new dialogue receives a new presence key. The old exit and new enter can overlap.
  • Menu prompts rendered through the dialog slot use the same presence behavior before menu choices appear.
  • Exiting dialogs are inactive: click, skip, and auto-forward handlers are ignored, and retained nodes cannot call onFinished twice.

GameConfig.animationPropagate is passed to the dialog AnimatePresence boundary. Leave it false unless you need nested AnimatePresence exits inside your custom dialog to propagate.

Dialog avatars

Dialog avatars show a small portrait in the ADV box. They fit visual novels where the speaker may be on-screen, off-screen (voice-over), or where most lines should not need per-line avatar configuration.

Mental model

  • Character holds avatar intent — default and off-screen avatar, plus the portrait bindings.
  • A stage Image refines the avatar when it is the current visible bound portrait.
  • Sentence overrides only exceptions — hide avatar or use a one-off asset/resolver.

Resolution order:

  1. Narrator or unnamed character → no avatar.
  2. sentence.config.avatar === false → no avatar.
  3. Sentence-level avatar override (string, resolver, etc.).
  4. Avatar from the most recent visible bound portrait for that character.
  5. Character-level avatar.
  6. Otherwise no avatar.

The engine does not auto-crop full-body sprites. If no avatar is configured, nothing is shown.

Character-level avatar

Use when the same headshot should appear for normal lines and off-screen lines.

import { Character } from "narraleaf-react";

const alice = new Character("Alice", {
    avatar: "/assets/alice/avatar-default.png",
});

alice.say("I can speak off-screen and still show an avatar.");

Method style:

const alice = new Character("Alice")
    .setAvatar("/assets/alice/avatar-default.png");

alice.say("This line uses Alice's default avatar.");

Binding stage portraits

When a visible sprite should drive the avatar, bind a stage Image to the character.

import { Character, Image } from "narraleaf-react";

const aliceBody = new Image({
    name: "alice-body",
    src: "/assets/alice/body-normal.png",
    opacity: 1,
});

const alice = new Character("Alice", {
    avatar: "/assets/alice/avatar-default.png",
    portraits: [
        {
            image: aliceBody,
            avatar: "/assets/alice/avatar-normal.png",
        },
    ],
});

When aliceBody is visible in the current scene, that portrait’s avatar is used; otherwise the character-level avatar is used.

const alice = new Character("Alice")
    .setAvatar("/assets/alice/avatar-default.png")
    .addPortrait(aliceBody, {
        avatar: "/assets/alice/avatar-normal.png",
    });

Expression-based avatars (tagged images)

Portrait avatars can be resolvers that receive the current portrait, currentSrc, tags, character, sentence, and gameState. The resolver may return:

  • an image URL or StaticImageData;
  • null to force no avatar at this layer;
  • undefined to fall through to the next layer.
const aliceBody = new Image({
    name: "alice-body",
    src: {
        groups: [
            ["normal", "happy", "angry"],
            ["school", "casual"],
        ],
        defaults: ["normal", "school"],
        resolve: (emotion, outfit) => `/assets/alice/body-${emotion}-${outfit}.png`,
    },
    opacity: 1,
});

const alice = new Character("Alice", {
    avatar: "/assets/alice/avatar-default.png",
    portraits: [
        {
            image: aliceBody,
            avatar: ({ tags }) => {
                const emotion = tags?.[0] ?? "normal";
                return `/assets/alice/avatar-${emotion}.png`;
            },
        },
    ],
});

Per-line overrides

alice.say("Hide avatar for this line only.", {
    avatar: false,
});

alice.say("Special cut-in.", {
    avatar: "/assets/alice/avatar-special.png",
});

alice.say("Resolver for one line.", {
    avatar: ({ tags, currentSrc }) => {
        if (tags?.includes("angry")) {
            return "/assets/alice/avatar-angry-close.png";
        }
        return undefined;
    },
});

avatar: false skips avatar immediately; other overrides win over portrait and character defaults.

Multiple visible portraits

If several bound portraits are visible, the engine picks the most recently shown visible one along the scene’s display order (back to front). A displayable with opacity effectively zero is ignored.

Custom dialogs and useAvatar

Use <Avatar /> for simple layouts. Use useAvatar() when grid/flex should depend on whether an avatar exists (visible, src, character, portrait).

See useAvatar for props, return shape, and examples.

TypeScript reference (public exports)

Types and classes are exported from narraleaf-react (see Character, SentenceConfig, CharacterConfig):

  • DialogAvatarSource, DialogAvatarResolverContext, DialogAvatarResolver, DialogAvatar
  • CharacterPortraitConfig, DialogAvatarResolution
  • Character methods: setAvatar, addPortrait, setPortraits
  • Sentence / say config: avatar on SentenceUserConfig
  • React: Avatar, useAvatar, type DialogAvatarContext

Avatar resolution for the default UI runs inside the player; you normally configure characters and sentences, then render <Avatar /> or call useAvatar().

Customize the dialog box

Example: container styles

function GameDialog() {
    return (
        <Dialog
            style={{
                backgroundColor: "rgba(0, 0, 0, 0.5)",
                borderRadius: "10px",
                padding: "20px",
            }}
        >
            {/* ... */}
        </Dialog>
    );
}

Example: nametag decoration

function GameDialog() {
    return (
        <Dialog>
            <Nametag
                style={{
                    backgroundImage: "url('/path/to/image.png')",
                    backgroundSize: "cover",
                    width: "100%",
                    height: "100%",
                }}
            />
        </Dialog>
    );
}

On this page