JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 7521
  • Score
    100M100P100Q123251F
  • License MIT

A dialog/modal library for terminal UIs built on OpenTUI

Package Exports

  • @opentui-ui/dialog
  • @opentui-ui/dialog/react
  • @opentui-ui/dialog/solid
  • @opentui-ui/dialog/themes

Readme


A dialog/modal library for terminal UIs built on OpenTUI
Built by Matt Simpson

Features

  • Lightweight default styling (minimal theme)
  • 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)
  • Theme presets (minimal, classic, unstyled, danger)
  • Backdrop stacking modes (prevent compounded dimming)
  • React and Solid.js integrations

Installation

bun add @opentui-ui/dialog

Quick 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 | undefined

Default Styling

Out of the box, dialogs use the minimal theme:

  • Lighter backdrop (35% opacity)
  • No borders
  • Tighter padding (1 cell all around)
  • top-only backdrop mode (only topmost dialog shows backdrop)

This provides a clean, unobtrusive appearance while still being usable immediately.

Themes

Theme presets provide alternative visual styles. Import from @opentui-ui/dialog/themes:

import { DialogContainerRenderable, DialogManager } from "@opentui-ui/dialog";
import { themes } from "@opentui-ui/dialog/themes";

const container = new DialogContainerRenderable(renderer, {
  manager,
  ...themes.classic,
});
Theme Description
minimal Lighter backdrop (35%), no borders, tighter padding (default)
classic Traditional style with heavier backdrop (59%), wider padding
unstyled No backdrop, no background, no border, no padding
danger Red-accented border, darker backdrop for destructive actions

Customize a theme:

const container = new DialogContainerRenderable(renderer, {
  manager,
  ...themes.classic,
  size: "large", // Override specific options
});

Backdrop Modes

When stacking multiple dialogs, backdrops can compound and make the screen too dark. Use backdropMode to control this:

const container = new DialogContainerRenderable(renderer, {
  manager,
  backdropMode: "per-dialog", // Each dialog renders its own backdrop
});
Mode Description
top-only Only the topmost dialog renders a backdrop (default)
per-dialog Each dialog renders its own backdrop

Unstyled Mode

For full control over dialog styling, use unstyled: true. This disables all default styles (backdrop, background, border, padding):

const container = new DialogContainerRenderable(renderer, {
  manager,
  unstyled: true,
  dialogOptions: {
    style: {
      // Add your own styles
      backgroundColor: "#262626",
      border: true,
      borderColor: "#525252",
    },
  },
});

API 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,
  unstyled?: boolean,
  backdropMode?: "per-dialog" | "top-only",
  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,
    unstyled: boolean,
    backdropMode: "per-dialog" | "top-only",
  },
  sizePresets: {
    // Custom size presets (terminal columns)
    small: 40,
    medium: 60,
    large: 80,
  },
  closeOnEscape: true, // ESC key closes top dialog (default: true)
  unstyled: false, // Disable default styles (default: false)
  backdropMode: "top-only", // Backdrop stacking behavior (default)
});

// 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.35)

  // Content panel
  backgroundColor?: string; // Default: "#262626"
  borderColor?: string;
  borderStyle?: BorderStyle;
  border?: boolean; // Default: false

  // Sizing
  width?: number | string;
  maxWidth?: number;
  minWidth?: number;
  maxHeight?: number;

  // Padding (default: 1 cell all around)
  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">
      <MyContent />
    </DialogProvider>
  );
}

Using Themes with React/Solid

import { DialogProvider } from "@opentui-ui/dialog/react";
import { themes } from "@opentui-ui/dialog/themes";

function App() {
  return (
    <DialogProvider {...themes.classic}>
      <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" },
  unstyled: false,
  backdropMode: "top-only",
  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 new

useDialogState() 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 {
  ComputedDialogStyle,
  ComputeDialogStyleInput,
  Dialog,
  DialogBackdropMode,
  DialogContainerOptions,
  DialogContentFactory,
  DialogId,
  DialogOptions,
  DialogShowOptions,
  DialogSize,
  DialogStyle,
  DialogTheme,
  DialogToClose,
} from "@opentui-ui/dialog";

// Type guard
import { isDialogToClose } from "@opentui-ui/dialog";

// Themes and default style constants
import {
  DEFAULT_BACKDROP_OPACITY,
  DEFAULT_PADDING,
  DEFAULT_STYLE,
  themes,
  type DialogTheme,
} from "@opentui-ui/dialog/themes";

License

MIT