Presentation
Alert
Ask a question that needs an answer before work continues.
"use client"
import {
Alert,
AlertAction,
AlertActions,
AlertContent,
AlertDescription,
AlertIcon,
AlertTitle,
AlertTrigger,
} from "macbetter/alert"
import { Button } from "macbetter/button"
export default function Example() {
return (
<Alert>
<AlertTrigger render={<Button />}>Empty Trash…</AlertTrigger>
<AlertContent>
<AlertIcon>
<img src="/app-icon.svg" alt="" />
</AlertIcon>
<AlertTitle>Are you sure you want to permanently erase the items in the Trash?</AlertTitle>
<AlertDescription>You can’t undo this action.</AlertDescription>
<AlertActions>
<AlertAction variant="cancel">Cancel</AlertAction>
<AlertAction
variant="primary"
tone="destructive"
value="confirm"
data-agent-confirm="Permanently erases 24 items in the Trash"
>
Empty Trash
</AlertAction>
</AlertActions>
</AlertContent>
</Alert>
)
}When to use it
Use an alert when someone has to decide before anything else can happen: confirming something that can’t be undone, or choosing what happens to unsaved work. Alerts interrupt, so keep them rare. Don’t use one to report that something went well; a Notification or an inline status does that without stopping anyone.
- Write the title as the question (“Delete this photo?”), and use the message for the consequence, in a sentence or two.
- Label buttons with verbs that answer the question (“Empty Trash”, “Don’t Save”), never “Yes” and “No”.
- Mark the action people usually want as
primary. When the action destroys something, give ittone="destructive", and consider making Cancel the default instead.
Usage
import {
Alert,
AlertAction,
AlertActions,
AlertContent,
AlertDescription,
AlertIcon,
AlertTitle,
AlertTrigger,
} from "macbetter/alert"AlertContent brings its own dimmed backdrop. AlertAction closes the alert and reports its value; value="confirm" is the agreeing answer that createConfirm() resolves true for. Put the least destructive action first in the markup, since the alert opens with focus on it.
Mark an action that destroys something with data-agent-confirm, so an AI agent driving the page has to ask the person before it presses it.
Three choices
Two actions sit side by side; three or more stack, with the cancel action at the bottom. Set orientation on AlertActions to force either layout.
"use client"
import {
Alert,
AlertAction,
AlertActions,
AlertContent,
AlertDescription,
AlertIcon,
AlertTitle,
AlertTrigger,
} from "macbetter/alert"
import { Button } from "macbetter/button"
export default function Example() {
return (
<Alert>
<AlertTrigger render={<Button />}>Close Document</AlertTrigger>
<AlertContent>
<AlertIcon>
<img src="/app-icon.svg" alt="" />
</AlertIcon>
<AlertTitle>Do you want to save the changes you made to “Q3 Roadmap”?</AlertTitle>
<AlertDescription>Your changes will be lost if you don’t save them.</AlertDescription>
<AlertActions>
<AlertAction variant="cancel">Cancel</AlertAction>
<AlertAction variant="primary" value="save">
Save
</AlertAction>
<AlertAction tone="destructive" value="discard" data-agent-confirm="Discards unsaved changes to Q3 Roadmap">
Don’t Save
</AlertAction>
</AlertActions>
</AlertContent>
</Alert>
)
}Action sheet
On iPhone, a destructive choice that follows a tap usually rises from the bottom as an action sheet. Pass variant="sheet" to AlertContent: the choices sit in one card and Cancel stands apart below it. On the Mac and iPad the same markup shows a regular alert.
"use client"
import {
Alert,
AlertAction,
AlertActions,
AlertContent,
AlertDescription,
AlertTitle,
AlertTrigger,
} from "macbetter/alert"
import { Button } from "macbetter/button"
export default function Example() {
return (
<Alert>
<AlertTrigger render={<Button tone="destructive" />}>Delete Photo</AlertTrigger>
<AlertContent variant="sheet">
<AlertTitle>Delete this photo?</AlertTitle>
<AlertDescription>It will be deleted from iCloud Photos on all your devices.</AlertDescription>
<AlertActions>
<AlertAction variant="cancel">Cancel</AlertAction>
<AlertAction
tone="destructive"
value="confirm"
data-agent-confirm="Deletes the photo from every device"
>
Delete Photo
</AlertAction>
</AlertActions>
</AlertContent>
</Alert>
)
}Asking from code
Create a confirm function with createConfirm(), pass its controller to an Alert, and await the answer from any handler. It resolves true when the value="confirm" action closes the alert.
const [confirmDelete] = useState(() => createConfirm<{ name: string }>())
async function remove(name: string) {
if (await confirmDelete({ name })) deleteProject(name)
}
<Alert controller={confirmDelete.controller}>
<AlertContent<{ name: string }>>
{(state) => (
<>
<AlertTitle>Delete “{state.payload?.name}”?</AlertTitle>
<AlertActions>
<AlertAction variant="cancel">Cancel</AlertAction>
<AlertAction tone="destructive" value="confirm">Delete</AlertAction>
</AlertActions>
</>
)}
</AlertContent>
</Alert>Platform behavior
- macOS. A 260pt panel of thick material with the app icon on top, a bold 13pt title and an 11pt message. Buttons are full width; the primary one is filled with the accent. It scales in from slightly smaller, without a bounce.
- iOS and iPadOS. The 270pt alert: no icon, a 17pt semibold title, a 13pt message and full-bleed text buttons divided by hairlines. The primary action is bold and destructive ones are red. It settles in from slightly larger, as the system alert does.
- Action sheet. iPhone only. On iPad, where an action sheet would be a popover, and on the Mac,
variant="sheet"shows the alert. - iOS and iPadOS dim the page with the scrim. macOS doesn’t dim the window behind an alert, so the Mac gets a lighter veil, just enough to show the page is blocked. The alert’s material is denser than a menu’s so the message reads over any backdrop. Reduced motion drops the scale; reduced transparency makes the material opaque.
Accessibility
- The alert has the
alertdialogrole, is named by its title and described by its message. - It opens with focus on the first action, keeps Tab inside, and makes the rest of the page inert. Pressing outside never closes it.
- Escape closes it and returns focus to the trigger, like Cancel.
- In a stack, the cancel action moves to the bottom visually but keeps its place in the Tab order, first.
API
Alert
Takes every ui-sdk Alert Dialog root prop.
| Prop | Type | Default | Description |
|---|---|---|---|
open / defaultOpen | boolean | — | Controls or starts the alert open. |
onOpenChange | (open, details) => void | — | details.returnValue is the value of the action that closed it. |
controller | AlertDialogController | — | From createConfirm() or createAlertDialogController(), to open it from code. |
AlertContent
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "alert" | "sheet" | "alert" | sheet docks an action sheet to the bottom on iPhone. |
initialFocus / finalFocus | DialogInitialFocus / PopupFocusTarget | — | Where focus goes on open and close. |
AlertActions
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "auto" | "horizontal" | "vertical" | "auto" | auto puts two buttons side by side and stacks one or three and more. |
AlertAction
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "primary" | "cancel" | "default" | primary is the default action; cancel sinks to the bottom of a stack and stands apart in an action sheet. |
tone | "accent" | "destructive" | "accent" | destructive turns it red. |
value | string | — | Reported as returnValue. "confirm" is the agreeing answer. |
action | (event, details) => unknown | — | Runs before the alert closes; a Promise keeps it open and the button pending. |
AlertTrigger, AlertTitle, AlertDescription and AlertIcon take their element’s props. createConfirm, confirm and createAlertDialogController are re-exported from ui-sdk.
| Data attribute | On | Meaning |
|---|---|---|
data-variant | .mb-alert | alert or sheet |
data-orientation | .mb-alert-actions | The layout you asked for |
data-variant, data-tone | .mb-alert-action | Its role and color |
data-starting-style, data-ending-style | .mb-alert, .mb-alert-backdrop | Entering and leaving |
data-pending | .mb-alert-action | Its action is running |
| CSS variable | Default |
|---|---|
--mb-alert-width | 260px on macOS, 270px on iOS and iPadOS |
--mb-alert-radius | --mb-popup-radius on macOS, 14pt on touch |
--mb-alert-enter-scale | 0.92 on macOS, 1.12 on touch |
--mb-alert-scrim | A lighter --mb-scrim on macOS, --mb-scrim on touch |