NarraLeaf

Custom Dialog

What It Replaces

The Dialog component renders character dialogue in Say mode, including the character nametag and text content. You can replace the default Dialog component via game.configure to fully customize the dialog appearance.

The implementation below uses Nametag and Texts for the character name and line content.

1. Create the Custom Dialog Component

Use Dialog, Nametag, and Texts to build the layout. Dialog is the container, Nametag renders the character name, and Texts renders the dialogue text.

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

function GameDialog() {
  return (
    <Dialog
      className="bg-black/70 rounded-lg px-6 py-4"
      // Dialog: container for the dialog box, supports className/style
    >
      <Nametag className="text-lg font-bold" color="#fbbf24" />
      {/* Nametag: character name, content and color are controlled by props */}
      <Texts className="text-base leading-relaxed" defaultColor="white" />
      {/* Texts: component defaults are props; Sentence/Word overrides still win */}
    </Dialog>
  );
}

2. Use useDialog for State (Optional)

If you need to read the current dialogue state (e.g. done, text, isNarrator) in a child component, use the useDialog hook:

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

function GameDialog() {
  const { done, text, isNarrator } = useDialog();
  // done: whether typing is complete
  // text: current displayed text
  // isNarrator: true when speaker is null (narrator)

  return (
    <Dialog className="bg-black/70 rounded-lg px-6 py-4">
      {!isNarrator && <Nametag className="text-lg font-bold" />}
      {/* Hide nametag when narrator speaks */}
      <Texts defaultColor="white" />
    </Dialog>
  );
}

3. Register in App

Set the dialog component when creating Game, then pass that instance to GameProviders. Creating the game outside the React component also avoids rebuilding it on every render.

import { Game, GameProviders, Player } from "narraleaf-react";
import GameDialog from "./GameDialog";

const game = new Game({ dialog: GameDialog });

function App() {
  return (
    <GameProviders game={game}>
      <Player
        story={story}
        onReady={({ liveGame }) => liveGame.newGame()}
      />
    </GameProviders>
  );
}

Use game.configure({ dialog: GameDialog }) when the component must be replaced at runtime.

4. Full Example (from narraleaf-react-skeleton)

This example uses useDialog done for a typing-complete indicator, isNarrator to hide the nametag, and background images:

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

// Sub-component: show triangle/underline when typing is done
function SentenceContext() {
  const { done } = useDialog();
  return (
    <>
      <Texts
        className="max-w-max flex items-center"
        defaultColor="white"
        fontSize={22}
      />
      <div className="flex flex-col items-center">
        <div className={clsx(
          "w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-t-[10px] border-t-white",
          done ? "opacity-100" : "opacity-0"  // Show indicator when done
        )} />
        <div className="w-[12px] h-[2px] bg-white mt-[2px]" />
      </div>
    </>
  );
}

export function GameDialog() {
  const { isNarrator } = useDialog();

  return (
    <Dialog
      className="absolute bottom-4 left-1/2 -translate-x-1/2 p-12 px-16 w-[90%] h-[216px]"
      style={{
        backgroundImage: "url('/ui/game-dialog.png')",
        backgroundSize: "contain",
        backgroundPosition: "bottom",
        backgroundRepeat: "no-repeat",
      }}
    >
      <div className={clsx("absolute left-[30px] -top-[15px]", { "hidden": isNarrator })}>
        <Nametag
          className="px-4 py-2 min-w-[220px] min-h-[56px] flex items-center justify-center"
          color="#2987a1"
          style={{
            backgroundImage: "url('/ui/game-dialog-nametag.png')",
            backgroundSize: "contain",
            backgroundPosition: "center",
            backgroundRepeat: "no-repeat",
          }}
        />
      </div>
      <div className="flex items-center gap-[5px] h-full">
        <SentenceContext />
      </div>
    </Dialog>
  );
}

Set text and nametag defaults on the components themselves. GameConfig no longer owns dialog font, text color, or nametag color defaults.

5. Simple Style Example

function GameDialog() {
  return (
    <Dialog
      style={{
        backgroundColor: "rgba(0, 0, 0, 0.6)",
        borderRadius: "12px",
        padding: "24px",
        border: "1px solid rgba(255, 255, 255, 0.2)",
      }}
    >
      <Nametag style={{ marginBottom: "8px", paddingBottom: "4px", borderBottom: "2px solid rgba(255, 193, 7, 0.8)" }} />
      <Texts defaultColor="white" fontSize={18} />
    </Dialog>
  );
}

6. Add Enter and Exit Motion

Dialog accepts Motion props directly. You do not need to wrap it with your own motion.div for the dialog box animation:

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

function GameDialog() {
  return (
    <Dialog
      className="absolute bottom-4 left-1/2 -translate-x-1/2 w-[90%] rounded-xl bg-black/75 px-6 py-4"
      initial={{ opacity: 0, y: 24 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: -24 }}
      transition={{ duration: 0.24, ease: "easeOut" }}
      layout
      onAnimationComplete={() => {
        // Optional Motion event handler
      }}
    >
      <Nametag className="font-bold" color="#fbbf24" />
      <Texts defaultColor="white" />
    </Dialog>
  );
}

The Motion props are applied to the dialog's inner motion.div. The outer wrapper still handles player scaling and click/key behavior, so transforms such as x, y, scale, and rotate will not conflict with useAspectScale.

Dialog presence is managed by the player:

  • Consecutive dialogue lines reuse the same dialog presence slot, so the previous line does not run exit before the next line appears.
  • If dialogue is followed by a non-dialogue gap, the old dialog can run its exit animation while the story continues.
  • If a later dialogue appears before the old exit finishes, the new dialogue gets a separate presence key, so the old exit and new enter can overlap.
  • Menu prompts rendered through the dialog slot use the same motion behavior before the choices appear.
  • Exiting dialogs ignore click, skip, and auto-forward events, so they cannot advance the story again.

Notes

  • Nametag content and color: Controlled by Nametag props such as name, children, color, className, and style.
  • Text defaults: Controlled by Texts props such as defaultColor, fontSize, fontFamily, fontWeight, and fontWeightBold. Word / Sentence styling still overrides specific words or lines.

On this page