NarraLeaf

Dialog

Dialog renders the dialog box: the character nametag, the dialog text, and an optional avatar.

To replace the built-in dialog box, pass your own component to game.configure({ dialog }). Compose Avatar, Nametag, and Texts inside it, or read the active line with useDialog.

Example

  1. Import the components
import { Avatar, Dialog, Nametag, Texts, useGame } from "narraleaf-react";
  1. Compose the ADV layout
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>
    );
}

Avatar is optional. The built-in dialog box uses this layout.

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

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

    return /* ... */
}

Components

Dialog

Renders the dialog container. Its children are usually Avatar, Nametag, and Texts, in any layout.

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

Apply visual transforms to Dialog itself; the player forwards them to the inner motion element.

Nametag

Renders the speaker name. Use it inside Dialog. Without children or name it shows the current speaker, and without color it uses the current character color.

  • character?: Character | null - Character to read the name from.
  • entry?: NvlDialogEntry - NVL entry to read the name from.
  • name?: React.ReactNode - Name to display.
  • color?: Color - Text color.
  • children?: React.ReactNode - Custom nametag content.
  • ...props: Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "color">

Texts

Renders the typed dialogue text. Use it inside Dialog. These props set the defaults, unset properties are inherited from CSS, and styles set on a Sentence or Word override both.

  • children?: never
  • defaultColor?: Color - Text color when the sentence or word sets none.
  • fontSize?: React.CSSProperties["fontSize"]
  • fontWeight?: React.CSSProperties["fontWeight"]
  • fontWeightBold?: React.CSSProperties["fontWeight"] - Weight for bold sentences and words.
  • fontFamily?: React.CSSProperties["fontFamily"]
  • autoFit?: boolean - Keeps the whole line inside its box by setting it smaller as it is typed. Default true. Since 0.34.0.
  • autoFitMinFontSize?: number - Smallest size scaling sets, in px. Default 12. Since 0.34.0.
  • ...props: React.HTMLAttributes<HTMLDivElement>

Avatar

Renders the avatar resolved for the current line, and renders nothing when the line has no avatar.

  • ...props: React.ImgHTMLAttributes<HTMLImageElement> - Merged with the default image style.

Default image style:

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

Animation

Pass Motion props to Dialog. The built-in dialog box has no enter or exit animation.

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

The player runs the animations at these points:

  • One dialogue replaced by another: the box stays in place. exit does not run, and the enter animation does not restart when the text or the name changes.
  • Dialogue followed by other content: exit runs while the story advances.
  • A new dialogue while an earlier exit is still running: both animations play at the same time.
  • Menu prompts shown in the dialog box: the same behavior, before the choices appear.

A dialog box that is exiting ignores clicks, skip, and auto-forward.

GameConfig.animationPropagate applies to the dialog AnimatePresence boundary. Set it to true only when your dialog box nests another AnimatePresence whose exit animations must run.

Avatars

Avatar shows a small portrait for the speaking character. Set the image on the character, on a stage portrait bound to the character, or on a single line.

For each line, the avatar is resolved in this order:

  1. Narrator or unnamed character: no avatar.
  2. avatar: false in the sentence config: no avatar.
  3. The sentence-level avatar.
  4. The avatar of the most recently shown visible portrait bound to the character.
  5. The character-level avatar.
  6. No avatar.

Full-body sprites are not cropped into avatars. A character with no configured avatar shows none.

Character avatar

Use one image for both on-stage 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.");

The same with method calls:

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

alice.say("This line uses the default avatar.");

Stage portraits

Bind a stage Image to the character to let the visible sprite select the avatar.

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",
        },
    ],
});

The portrait avatar is used while aliceBody is visible in the current scene, otherwise the character avatar is used.

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

Avatars per expression

A portrait avatar can be a function. It receives the current portrait, currentSrc, tags, character, sentence, and gameState, and returns:

  • an image URL or StaticImageData;
  • null to show no avatar;
  • undefined to continue with the next step of the resolution order.
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 hides the avatar for that line. Any other value takes priority over the portrait and character avatars.

Multiple visible portraits

When several bound portraits are visible, the engine uses the one shown most recently along the scene's display order (back to front). A displayable with an effective opacity of zero is ignored.

Layouts that depend on the avatar

useAvatar() returns visible, src, character, and portrait, so a grid or flex layout can change when a line has no avatar. See useAvatar for the return shape and examples.

Styling

Style the container through Dialog:

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

Style the nametag through Nametag:

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

Line breaking

Dialogue text is typeset to the strict kinsoku rules in both writing modes. A line does not begin with 、 。 」 ? !, a small kana, or a prolonged sound mark, and does not end with 「 (. A Latin word is never split, and a run that fits nowhere, such as a URL, breaks rather than overflowing.

The strict rules apply only when the document declares a language. Set lang on the page hosting the player. Any value is enough, and a shell built by NarraLeaf Studio already carries one.

Text scaling

Available since 0.35.0.

A dialogue line is kept inside its box by being set down as it is typed. It is set at fontSize and stays there for as long as it fits, so a short line is drawn at full size. Once the text reaches the end of the box, every further character is measured and the size comes down by what it takes to keep the whole line inside, to no less than autoFitMinFontSize. A line that still overflows at the smallest size is left overflowing.

Scaling is on by default. Turn it off for one line with autoFit={false}, or for the whole game with GameConfig.disableTextScaling.

<Texts fontSize={24} autoFitMinFontSize={16} />

Sizes set on a Sentence or a Word are scaled with the line rather than replaced, so their relative weights hold at every size.

The box is the container's parent element. A parent with no height of its own leaves the line at its authored size.

Overlay

The ADV dialog box carries an overlay that covers it and paints above it, inside the same scaled stage. Anything that belongs to a line but does not fit inside the text box goes there: the definition popup of an inline word, a tooltip on a name. Reach it with useDialogOverlay.

The overlay is part of the built-in Dialog component, so a custom dialog composed from Dialog has it. NVL mode has no overlay.

Exported types

narraleaf-react exports:

  • Component props: NametagProps, TextAppearanceProps, TextsProps, RawTextsProps, EntryTextsProps
  • Avatar sources: DialogAvatarSource, DialogAvatarResolverContext, DialogAvatarResolver, DialogAvatar
  • Portraits: CharacterPortraitConfig, DialogAvatarResolution
  • React: Avatar, useAvatar, and the type DialogAvatarContext

Character provides setAvatar, addPortrait, and setPortraits. See Character, CharacterConfig, and the avatar field of SentenceUserConfig and SentenceConfig.

On this page