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 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 { desktop, 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" },
        { 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" },
        { 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" },
      ],
    },
  },
];

function App() {
  const answer = signal("–");
  const shown = signal(0);
  const show = async (options: Options) => {
    shown.update((n) => n + 1);
    const choice = await AlertDialog.show(options);
    answer.set(choice === null ? "null (closed without an action)" : JSON.stringify(choice));
  };
  const row = () => ({ ...ui().row, borderRadius: 12, padding: 14, gap: 2 });
  const hint = desktop ? "Escape or a click outside the action sheet closes it with null." : "A tap outside the action sheet closes it with 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)}
                          style={{ ...ui().raised, borderRadius: 8, height: size.control, paddingHorizontal: 12, justifyContent: "center" }}
                          activeStyle={{ opacity: 0.7 }}
                        >
                          <Text style={{ fontSize: 13, fontWeight: "600", 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 }}>
                  <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::ui::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")
                .destructive("Delete")
                .show()
                .await;
            let deleted = matches!(choice, Ok(Some(label)) if label == "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<string | null>
alert_dialog().title(..).description(..).action(..).cancel(..).destructive(..).show()
    -> impl Future<Output = Result<Option<SharedString>, String>>

The result is the chosen action’s id (its label when it has no id; in Rust always its label), 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. It is an error (a rejected promise, an Err) when there are no actions. 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::ui::prelude::*;

fn share() {
    spawn(async move {
        let choice = alert_dialog()
            .title("Share the photo")
            .action_sheet()
            .action("Copy link")
            .action("Save")
            .destructive("Delete")
            .cancel("Cancel")
            .show()
            .await;
        match choice.ok().flatten().as_deref() {
            Some("Copy link") => { /* … */ }
            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.

#AlertDialogAction

FieldRustTypeDefaultDescription
labelthe method’s 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).
idnone: the label answersstring | nullits labelWhat show resolves with when it is chosen.

#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 the result 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 (the result 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.