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
exitbefore the next line appears. - If dialogue is followed by a non-dialogue gap, the old dialog can run its
exitanimation 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.