Elements

AlertDialog

AlertDialog shows the platform’s own alert or action sheet: UIAlertController on iOS and Mac Catalyst, an AlertDialog (or a list dialog for an action sheet) on Android, and an alert in the platform’s dialog look on the other platforms. AlertDialog.show (alert_dialog()’s .show() in Rust) returns a promise (a future in Rust) that resolves with { action }: the id of the action the user chose. Every label comes from the app, so an alert says what your app says, in its language.

el-alert-dialog.tsx
el-alert-dialog.tsxLive
import { mount, signal, Text, View } from "nsh";
import { AlertDialog, Button } from "nsh/base";
import { button, buttonText, desktop, focusRing, hoverFill, Inspector, Panel, size, Stage, ui, wide } from "./palette";

type Options = Parameters<typeof AlertDialog.show>[0];

// What each button shows, and what it is about.
const alerts: { label: string; detail: string; danger?: boolean; options: Options }[] = [
  {
    label: "Delete…",
    detail: "cancel + destructive roles",
    danger: true,
    options: {
      title: "Delete the photo?",
      description: "This cannot be undone.",
      actions: [
        { label: "Cancel", role: "cancel", id: "cancel" },
        { label: "Delete", role: "destructive", id: "delete" },
      ],
    },
  },
  {
    label: "Save changes…",
    detail: "three actions, closeOnEscape",
    options: {
      title: "Save changes?",
      description: "Your edits to “Trip notes” are not saved yet.",
      actions: [
        { label: "Don't save", role: "destructive", id: "discard" },
        { label: "Cancel", role: "cancel", id: "cancel" },
        { label: "Save", id: "save" },
      ],
      closeOnEscape: true,
    },
  },
  {
    label: "Share…",
    detail: 'presentation "actionSheet"',
    options: {
      title: "Share the photo",
      presentation: "actionSheet",
      actions: [
        { label: "Copy link", id: "copy" },
        { label: "Save", id: "save" },
        { label: "Delete", role: "destructive", id: "delete" },
        { label: "Cancel", role: "cancel", id: "cancel" },
      ],
    },
  },
];

function App() {
  const answer = signal("–");
  const shown = signal(0);
  const show = async (options: Options) => {
    shown.update((n) => n + 1);
    const { action } = await AlertDialog.show(options);
    answer.set(action === null ? "{ action: null } (closed without an action)" : `{ action: ${JSON.stringify(action)} }`);
  };
  const row = () => ({ ...ui().row, borderRadius: 12, padding: 14, gap: 2 });
  const hint = desktop ? "Escape or a click outside the action sheet closes it with { action: null }." : "A tap outside the action sheet closes it with { action: null }.";
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 12 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>alert-dialog</Text>
      <Text style={{ color: ui().muted }}>The platform's own alert; every label comes from the app.</Text>

      {() =>
        wide() ? (
          <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
            {/* A row per alert: what it shows, and a button as wide as its label at the end. */}
            <Stage width={400}>
              <View style={{ ...ui().card, borderRadius: 14, overflow: "hidden" }}>
                <Each of={alerts}>
                  {(a, i) => (
                    <>
                      {i() > 0 ? <View style={{ height: 1, marginLeft: 12, backgroundColor: ui().line }} /> : null}
                      <View style={{ flexDirection: "row", alignItems: "center", gap: 12, height: size.row + 4, paddingHorizontal: 12 }}>
                        <Text style={{ flex: 1, fontSize: 13, color: ui().muted }}>{a().detail}</Text>
                        <Button onPress={() => show(a().options)} {...button()}>
                          <Text style={{ ...buttonText(), color: a().danger ? ui().down : ui().text }}>{a().label}</Text>
                        </Button>
                      </View>
                    </>
                  )}
                </Each>
              </View>
            </Stage>
            <Inspector>
              <Panel title={`AlertDialog.show resolved with (${shown()} shown)`}>
                <Text style={{ color: ui().accent, fontSize: 14, fontWeight: "600" }}>{answer()}</Text>
              </Panel>
              <Text style={{ color: ui().faint, fontSize: 12 }}>{hint}</Text>
            </Inspector>
          </View>
        ) : (
          <View style={{ gap: 10 }}>
            <Each of={alerts}>
              {(a) => (
                <Button onPress={() => show(a().options)} style={row()} activeStyle={{ opacity: 0.7 }} hoverStyle={hoverFill()} focusStyle={focusRing()}>
                  <Text style={{ color: a().danger ? ui().down : ui().text, fontSize: 16, fontWeight: "600" }}>{a().label}</Text>
                  <Text style={{ color: ui().muted, fontSize: 13 }}>{a().detail}</Text>
                </Button>
              )}
            </Each>

            <View style={{ ...ui().inset, borderRadius: 12, padding: 14, gap: 4, marginTop: 4 }}>
              <Text style={{ color: ui().muted, fontSize: 13 }}>AlertDialog.show resolved with ({shown()} shown)</Text>
              <Text style={{ color: ui().accent, fontSize: 16, fontWeight: "600" }}>{answer()}</Text>
            </View>
            <Text style={{ color: ui().faint, fontSize: 13 }}>{hint}</Text>
          </View>
        )
      }
    </View>
  );
}

mount(App);
use nsh_base::{alert_dialog, button};
use nsh_core::prelude::*;

fn app() -> impl IntoElement {
    let answer = signal(SharedString::from("–"));
    let confirm = move || {
        spawn(async move {
            let choice = alert_dialog()
                .title("Delete the photo?")
                .description("This cannot be undone.")
                .cancel("cancel", "Cancel")
                .destructive("delete", "Delete")
                .show()
                .await;
            let deleted = matches!(choice, Ok(r) if r.action.as_deref() == Some("delete"));
            answer.set(if deleted { "deleted" } else { "kept" }.into());
        });
    };
    view()
        .flex_1()
        .p_4()
        .pt(px(48.0))
        .gap_3()
        .child(
            button()
                .on_press(confirm)
                .rounded_lg()
                .bg(rgb(0xdc2626))
                .px_4()
                .py_3()
                .child(text("Delete…").text_color(white()).font_weight(FontWeight::SEMIBOLD)),
        )
        .child(text(answer))
}

For dialogs with a look of your own (a card, a sheet, a full-screen flow), use Dialog.

#AlertDialog.show

AlertDialog.show(options: AlertDialogOptions): Promise<AlertDialogResult>
alert_dialog().title(..).description(..).action(..).cancel(..).destructive(..).show()
    -> impl Future<Output = Result<AlertDialogResult, String>>

The result is { action } (AlertDialogResult): the chosen action’s id, or null (None) when the dialog closed without an action: a press outside it, Escape or the back button, where closeOnBackdropPress and closeOnEscape allow it. Without actions the promise rejects with a TypeError (an Err in Rust). In Rust, nsh_base::alert_dialog() sets the dialog up with a method per option and per action, in the order the actions show, and .show() shows it; await its answer in a task (spawn).

#Action sheets

An action sheet takes the same options with the "actionSheet" presentation:

use nsh_base::alert_dialog;
use nsh_core::prelude::*;

fn share() {
    spawn(async move {
        let choice = alert_dialog()
            .title("Share the photo")
            .action_sheet()
            .action("copy", "Copy link")
            .action("save", "Save")
            .destructive("delete", "Delete")
            .cancel("cancel", "Cancel")
            .show()
            .await;
        match choice.ok().and_then(|r| r.action).as_deref() {
            Some("copy") => { /* … */ }
            Some("save") => { /* … */ }
            Some("delete") => { /* … */ }
            _ => {} // "cancel", or closed without an action.
        }
    });
}

#AlertDialogOptions

FieldRustTypeDefaultDescription
title.title(..)string | nullnoneAbove the description.
description.description(..)string | nullnoneBelow the title.
actions.action(..), .cancel(..), .destructive(..), one per actionAlertDialogAction[]requiredThe actions, in order (at least one; at most three in an Android alert).
presentation.action_sheet()"alert" | "actionSheet""alert"An alert in the middle, or a sheet of actions from the bottom (a list dialog on Android).
closeOnEscape.close_on_escape(..)boolean | nullfalse for an alert, true for an action sheetEscape and the Android back button close it without an action.
closeOnBackdropPress.close_on_backdrop_press(..)boolean | nullfalse for an alert, true for an action sheetA press outside it closes it without an action.

#AlertDialogResult

FieldRustTypeDescription
actionactionstring | null (Option<SharedString>)The chosen action’s id, or null when the dialog closed without an action.

#AlertDialogAction

FieldRustTypeDefaultDescription
idthe method’s first argumentstringrequiredWhat show reports as action when it is chosen: the app’s name for it, which stays the same whatever language the labels are in.
labelthe method’s second argumentstringrequiredWhat it says.
role.action(..), .cancel(..), .destructive(..)"default" | "cancel" | "destructive""default"What it means: the platform places and draws it by its role (a destructive action is red on iOS).

#Platform notes

  • iOS: UIAlertController lays out the actions. An action with the "cancel" role is placed where iOS puts cancel actions, and an action sheet shows it separately at its bottom. On iPad and Mac Catalyst the action sheet is a popover, with an untitled cancel action: a tap outside it closes it, and action is null.
  • Android: an AlertDialog shows up to three actions, mapped by position: the last action is the positive button, the one before it the negative button, and the one before that the neutral button. An action sheet is a list of the actions in a dialog, with a tap outside or the back button in place of its cancel action (action is then null). The list dialog shows the title, or the description when the sheet has no title.
  • Windows, Linux, GPUI and the web: NSH draws the alert or action sheet in the platform’s dialog look.