Package Exports
- @opentui-ui/dialog
- @opentui-ui/dialog/react
- @opentui-ui/dialog/solid
Readme
A dialog/modal library for terminal UIs built on OpenTUI
Built by Matt Simpson
Features
- Configurable backdrop opacity and color
- Size presets (small, medium, large, full)
- Click-to-close backdrop (default: enabled)
- ESC key to close
- Dialog stack support (multiple dialogs)
- Focus management (saves/restores focus on open/close)
- React and Solid.js integrations
Installation
bun add @opentui-ui/dialogQuick Start
import { DialogContainerRenderable, DialogManager } from "@opentui-ui/dialog";
import { TextRenderable } from "@opentui/core";
// 1. Create the manager and container
const manager = new DialogManager(renderer);
const container = new DialogContainerRenderable(renderer, { manager });
renderer.root.add(container);
// 2. Show dialogs from anywhere!
manager.show({
content: (ctx) => new TextRenderable(ctx, { content: "Hello World!" }),
});Quick Reference
// Show dialogs
manager.show({ content: (ctx) => new TextRenderable(ctx, { content: "Hello" }) });
manager.show({ content: fn, size: "large" }); // With size preset
manager.show({ content: fn, id: "my-dialog" }); // With custom ID
// Close dialogs
manager.close(); // Close top-most
manager.close(id); // Close specific
manager.closeAll(); // Close all
manager.replace({...}); // Close all and show new
// Query state
manager.isOpen(); // boolean
manager.getDialogs(); // readonly Dialog[]
manager.getTopDialog(); // Dialog | undefinedAPI Reference
DialogManager
const manager = new DialogManager(renderer);
// Show a dialog - returns the dialog ID
const id = manager.show({
content: (ctx) => new TextRenderable(ctx, { content: "Hello" }),
size?: "small" | "medium" | "large" | "full",
style?: DialogStyle,
closeOnClickOutside?: boolean, // default: true
onClose?: () => void,
onOpen?: () => void,
onBackdropClick?: () => void,
id?: string | number, // optional custom ID
});
// Close dialogs
manager.close(); // Close top-most
manager.close(id); // Close specific
manager.closeAll(); // Close all
manager.replace({...}); // Close all and show new
// Query state
manager.isOpen(); // boolean
manager.getDialogs(); // readonly Dialog[]
manager.getTopDialog(); // Dialog | undefined
// Subscribe to changes
const unsubscribe = manager.subscribe((data) => {
// Called when dialogs change
});
// Cleanup
manager.destroy();DialogContainerRenderable
const container = new DialogContainerRenderable(renderer, {
manager, // Required: DialogManager instance
size: "medium", // Default size preset
dialogOptions: {
// Default options for all dialogs
style: DialogStyle,
},
sizePresets: {
// Custom size presets (terminal columns)
small: 40,
medium: 60,
large: 80,
},
closeOnEscape: true, // ESC key closes top dialog (default: true)
});
// Add to render tree
renderer.root.add(container);DialogStyle
interface DialogStyle {
// Backdrop
backdropColor?: string; // Default: "#000000"
backdropOpacity?: number | string; // 0-1, or "50%" (default: ~0.59)
// Content panel
backgroundColor?: string;
borderColor?: string;
borderStyle?: BorderStyle;
border?: boolean;
// Sizing
width?: number | string;
maxWidth?: number;
minWidth?: number;
maxHeight?: number;
// Padding
padding?: number;
paddingX?: number;
paddingY?: number;
paddingTop?: number;
paddingRight?: number;
paddingBottom?: number;
paddingLeft?: number;
}Size Presets
Default size presets (in terminal columns):
| Size | Width |
|---|---|
| small | 40 |
| medium | 60 |
| large | 80 |
| full | terminal width - 4 |
Override with sizePresets option:
const container = new DialogContainerRenderable(renderer, {
manager,
sizePresets: {
small: 35,
medium: 55,
large: 75,
},
});React & Solid
Both frameworks share the same API pattern with DialogProvider, useDialog(), and useDialogState().
Setup
// React
import {
DialogProvider,
useDialog,
useDialogState,
} from "@opentui-ui/dialog/react";
// Solid
import {
DialogProvider,
useDialog,
useDialogState,
} from "@opentui-ui/dialog/solid";
function App() {
return (
<DialogProvider
size="medium"
dialogOptions={{ style: { backgroundColor: "#1a1a1a" } }}
>
<MyContent />
</DialogProvider>
);
}useDialog() Hook
Returns dialog actions for imperatively controlling dialogs.
const dialog = useDialog();
// Show a dialog
dialog.show({
content: <MyContent />, // React: JSX directly
content: () => <MyContent />, // Solid: function returning JSX
size: "medium",
style: { backgroundColor: "#1a1a1a" },
closeOnClickOutside: true,
onClose: () => {},
onOpen: () => {},
id: "my-dialog",
});
// Close dialogs
dialog.close(); // Close top-most
dialog.close(id); // Close specific
dialog.closeAll(); // Close all
dialog.replace({...}); // Close all and show newuseDialogState() Hook
Subscribe to reactive dialog state using a selector.
interface DialogState {
isOpen: boolean; // Whether any dialog is open
count: number; // Number of open dialogs
dialogs: readonly Dialog[]; // All active dialogs (oldest first)
topDialog: Dialog | undefined; // The top-most dialog
}
const isOpen = useDialogState((s) => s.isOpen);
const count = useDialogState((s) => s.count);
const topDialog = useDialogState((s) => s.topDialog);[!WARNING]
Always select primitives not new objects.
// ✅ Good - selects primitives
const isOpen = useDialogState((s) => s.isOpen);
const count = useDialogState((s) => s.count);
// ❌ Bad - creates new object every time, always re-renders
const state = useDialogState((s) => ({ isOpen: s.isOpen, count: s.count }));Key difference: React returns values directly, Solid returns accessors you must call.
// React - values directly
if (isOpen) {
console.log(`${count} dialog(s) open`);
}
// Solid - call accessors
if (isOpen()) {
console.log(`${count()} dialog(s) open`);
}Full Example
// React
function MyContent() {
const dialog = useDialog();
const isOpen = useDialogState((s) => s.isOpen);
return (
<box>
<text>{isOpen ? "Dialog open" : "No dialog"}</text>
<box onMouseUp={() => dialog.show({ content: <text>Hello!</text> })}>
<text>Open Dialog</text>
</box>
</box>
);
}
// Solid - note: content is a function, accessors are called
function MyContent() {
const dialog = useDialog();
const isOpen = useDialogState((s) => s.isOpen);
return (
<box>
<text>{isOpen() ? "Dialog open" : "No dialog"}</text>
<box
onMouseUp={() => dialog.show({ content: () => <text>Hello!</text> })}
>
<text>Open Dialog</text>
</box>
</box>
);
}TypeScript
Full TypeScript support with exported types:
import type {
Dialog,
DialogContainerOptions,
DialogContentFactory,
DialogId,
DialogOptions,
DialogShowOptions,
DialogSize,
DialogStyle,
DialogToClose,
} from "@opentui-ui/dialog";
// Type guard
import { isDialogToClose } from "@opentui-ui/dialog";License
MIT