Componentmedia0.9 KB gzipZero dependencies

Dialogue Box

Speaker panel with paged lines.

Preview

Live Preview

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add dialogue-box

Or install the full npm package:

npm install bigbullui
import { DialogueBox } from "@/components/ui/dialogue-box";

<DialogueBox />

Usage guidance

Use Dialogue Box when a ticket stub surface needs Speaker panel with paged lines. Keep props minimal and prefer composition with sibling stubs.

Avoid Dialogue Box for long form flows or dense data grids where a dedicated layout block fits better.

Source code

dialogue-box.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface DialogueBoxProps extends React.HTMLAttributes<HTMLDivElement> {
  speaker?: string;
  lines?: string[];
  onNext?: () => void;
  onSkip?: () => void;
}

/** Dialogue box: visual-novel speaker panel with paging. */
export function DialogueBox({ speaker = "Coach", lines, onNext, onSkip, className, ...props }: DialogueBoxProps) {
  const fallback = ["Welcome to the arena.", "Tap next to keep the briefing going."];
  const [idx, setIdx] = React.useState(0);
  const list = lines ?? fallback;
  const line = list[Math.min(idx, list.length - 1)];
  return (
    <div role="dialog" aria-label={`Dialogue with ${speaker}`} className={cn("w-full touch-none select-none rounded-md border-2 border-foreground bg-card p-3", className)} style={{ touchAction: "none" }} {...props}>
      <p className="w-fit rounded bg-foreground px-2 py-0.5 font-mono text-[10px] font-black uppercase tracking-[0.2em] text-background">{speaker}</p>
      <p className="mt-2 min-h-11 font-mono text-sm leading-relaxed text-foreground">{line}</p>
      <div className="mt-2 flex items-center justify-between gap-2">
        <span className="font-mono text-[10px] tabular-nums text-muted-foreground">{Math.min(idx + 1, list.length)}/{list.length}</span>
        <span className="flex gap-2">
          <button type="button" onClick={onSkip} className="min-h-11 touch-none rounded-md border border-dashed border-border px-3 font-mono text-[11px] font-bold uppercase focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" style={{ touchAction: "none" }}>Skip</button>
          <button
            type="button"
            onClick={() => {
              const next = Math.min(idx + 1, list.length - 1);
              setIdx(next);
              onNext?.();
            }}
            className="min-h-11 touch-none rounded-md border-2 border-foreground bg-primary px-4 font-mono text-[11px] font-black uppercase text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
            style={{ touchAction: "none" }}
          >
            Next
          </button>
        </span>
      </div>
    </div>
  );
}