zeroUI

Dialogue

A game dialogue box for React: the speaker on a slab, the line typed out, a blinking mark to continue, and numbered replies.

Install

terminal
# once per project: where @zeroui items come frompnpm dlx shadcn@latest registry add @zeroui=https://zeroui.catforest.dev/r/{name}.jsonpnpm dlx shadcn@latest add @zeroui/dialogue

Examples

A conversation with replies

Mara

dialogue-talk.tsx
import { useState } from "react";import {  Dialogue,  DialogueChoice,  DialogueChoices,  DialogueNext,  DialogueSpeaker,  DialogueText,} from "@/components/zero/dialogue";const script = [  {    who: "Mara",    line: "You're late. The gate shuts at two, and the crates are still at the yards.",  },  {    who: "Mara",    line: "Rook says the river road is watched tonight. Your call, courier.",  },];const replies = [  {    say: "We take the river road. It's faster.",    answer: "Fast and loud. Keep your shield charged.",  },  {    say: "We go round by the old market.",    answer: "Slower, but nobody looks twice at the market.",  },  {    say: "Who's watching it?",    answer: "Nobody you want to meet. Pick a road.",  },];export default function DialogueTalk() {  const [step, setStep] = useState(0);  const [answer, setAnswer] = useState<string | null>(null);  const [typed, setTyped] = useState("");  const line = answer ?? script[step].line;  const asking = answer === null && step === script.length - 1;  return (    <Dialogue aria-label="Conversation with Mara" className="w-full max-w-2xl">      <DialogueSpeaker>{answer ? "Mara" : script[step].who}</DialogueSpeaker>      <DialogueText key={line} onDone={() => setTyped(line)}>        {line}      </DialogueText>      {/* the replies wait for the line; until then, Continue shows the rest */}      {asking && typed === line ? (        <DialogueChoices>          {replies.map((reply, index) => (            <DialogueChoice              key={reply.say}              index={index + 1}              onClick={() => setAnswer(reply.answer)}            >              {reply.say}            </DialogueChoice>          ))}        </DialogueChoices>      ) : (        <DialogueNext          onClick={() => {            if (answer) {              setAnswer(null);              setStep(0);            } else setStep((s) => s + 1);          }}        >          {answer ? "Start over" : "Continue"}        </DialogueNext>      )}    </Dialogue>  );}

Behaviour

A black band with the speaker's name on a slab breaking its top edge. The line is typed out at speed letters a second; a click on the line or on Continue, or Enter or Space anywhere in the box, shows the rest at once, and the replies wait until it is whole. While focus is in the box, the number keys pick a reply.

Screen readers hear each whole line once, from a polite live region on the box, never the typing. Under reduced motion the line appears whole. Give DialogueText a new key for each line so the typing starts over.

Props

DialogueText

PropTypeDefaultDescription
childrenstring—The line.
speednumber45Letters a second.
onDone() => void—When the whole line is on screen.

DialogueNext

PropTypeDefaultDescription
onClick(event) => void—Called when the line is whole; before that, a click shows the rest.
childrenReactNode"Continue"Its label.

DialogueChoice

PropTypeDefaultDescription
indexnumber—Its number, and the key that picks it.

Dialogue, DialogueSpeaker, DialogueChoices