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.
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))
}import { mount, signal, Text, View } from "nsh";
import { AlertDialog, Button } from "nsh/base";
function App() {
const answer = signal("–");
const confirm = async () => {
const choice = await AlertDialog.show({
title: "Delete the photo?",
description: "This cannot be undone.",
actions: [
{ label: "Cancel", role: "cancel" },
{ label: "Delete", role: "destructive", id: "delete" },
],
});
answer.set(choice === "delete" ? "deleted" : "kept");
};
return (
<View style={{ flex: 1, padding: 16, paddingTop: 48, gap: 12 }}>
<Button onPress={confirm} class="rounded-lg bg-red-600 px-4 py-3 active:bg-red-700">
<Text class="text-center font-semibold text-white">Delete…</Text>
</Button>
<Text>{answer()}</Text>
</View>
);
}
mount(App);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.
}
});
}const choice = await AlertDialog.show({
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" },
],
});#AlertDialogOptions
| Field | Rust | Type | Default | Description |
|---|---|---|---|---|
title | .title(..) | string | null | none | Above the description. |
description | .description(..) | string | null | none | Below the title. |
actions | .action(..), .cancel(..), .destructive(..), one per action | AlertDialogAction[] | required | The 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 | null | false for an alert, true for an action sheet | Escape and the Android back button close it without an action. |
closeOnBackdropPress | .close_on_backdrop_press(..) | boolean | null | false for an alert, true for an action sheet | A press outside it closes it without an action. |
#AlertDialogAction
| Field | Rust | Type | Default | Description |
|---|---|---|---|---|
label | the method’s argument | string | required | What 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). |
id | none: the label answers | string | null | its label | What show resolves with when it is chosen. |
#Platform notes
- iOS:
UIAlertControllerlays 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 isnull. - Android: an
AlertDialogshows 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 thennull). The list dialog shows thetitle, or thedescriptionwhen the sheet has no title. - Windows, Linux, GPUI and the web: NSH draws the alert or action sheet in the platform’s dialog look.