termino
~/docs/custom/modal

Modalcustom

Dimmed overlay dialog with focus on the panel

~/custom/modal
live
$ termino modal — press space to open
────────────────────────────────────────────────────
app shell — normal content here
build passed
────────────────────────────────────────────────────

##API

Renders `null` when closed. When open, an absolute full-screen dim box centers a double-bordered panel; the panel grabs focus; esc/q closes.

properties
proptypedefaultdescription
openboolean-Show or hide
onClose() => void-Called on esc/q
titlestring-Optional title row
width / heightnumber46 / 10Panel size
borderColorstring"#7aa2f7"Panel border color

##Keymap

~/keymap
esc / qClose the modal
(panel)Focus moves to the modal panel when opened

##Notes

  • The overlay uses an RGBA color with alpha — OpenTUI renders real dimming, not a static shade.
  • Combine with ToastProvider: confirmations push toasts, modals ask questions.

##Source

Real implementation — this is the code that runs in your terminal.

tsxmodal.tsx
1import { createElement as h, useEffect, useRef } from "react";
2import { useKeyboard } from "@opentui/react";
3import type { BoxRenderable } from "@opentui/core";
4
5export interface ModalProps {
6 open: boolean;
7 onClose: () => void;
8 title?: string;
9 children?: React.ReactNode;
10 width?: number;
11 height?: number;
12 borderColor?: string;
13}
14
15export function Modal({
16 open,
17 onClose,
18 title,
19 children,
20 width = 46,
21 height = 10,
22 borderColor = "#7aa2f7",
23}: Readonly<ModalProps>) {
24 const panelRef = useRef<BoxRenderable | null>(null);
25
26 useKeyboard((key) => {
27 if (!open) return;
28 if (key.name === "escape" || key.name === "q") {
29 onClose();
30 }
31 });
32
33 useEffect(() => {
34 if (open) panelRef.current?.focus();
35 }, [open]);
36
37 if (!open) return null;
38
39 return h(
40 "box",
41 {
42 style: {
43 position: "absolute",
44 left: 0,
45 top: 0,
46 right: 0,
47 bottom: 0,
48 alignItems: "center",
49 justifyContent: "center",
50 backgroundColor: "rgba(10,12,18,0.85)",
51 },
52 },
53 h(
54 "box",
55 // eslint-disable-next-line react-hooks/refs -- ref via createElement wrapper
56 {
57 ref: panelRef,
58 style: {
59 width,
60 height,
61 border: true,
62 borderStyle: "double",
63 borderColor,
64 backgroundColor: "#1a1b26",
65 flexDirection: "column",
66 paddingLeft: 1,
67 paddingRight: 1,
68 paddingTop: 0,
69 paddingBottom: 1,
70 },
71 },
72 title
73 ? h(
74 "box",
75 { flexDirection: "row", justifyContent: "space-between", width: width - 4 },
76 h("text", { fg: borderColor }, title),
77 h("text", { fg: "#565f89" }, "esc"),
78 )
79 : null,
80 h("box", { style: { flexGrow: 1, justifyContent: "center" } }, children),
81 ),
82 );
83}
84