Alert Modal
alert-modaluiProps-driven shortcut for confirm and destructive prompts — pass `title`, `description`, `actionLabel`, and a `tone` and you get a fully styled modal with a leading icon ring, action button, and optional async loading state. Skip it and use Dialog when you need a free-form modal instead.
Installation
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/alert-modal.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/alert-modal.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/alert-modal.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/alert-modal.jsonhttps://uipkge.dev/r/svelte/alert-modal.jsonInstalls to:src/lib/components/ui/alert-modal/Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
openControlled open state. Pair with `bind:open`. | boolean | — | optional |
defaultOpenUncontrolled initial open state. | boolean | false | optional |
titleTitle rendered in the header. Override with the `titleSnippet`. | string | — | optional |
descriptionDescription rendered under the title. Override with the `descriptionSnippet`. | string | — | optional |
actionLabelLabel for the primary action button. | string | Continue | optional |
cancelLabelLabel for the cancel button. Pass null to hide. | string | null | Cancel | optional |
toneVisual tone — colors the icon and action button. | AlertModalTone | default | optional |
iconQuick icon shortcut. Override with the `iconSnippet`. | AlertModalIcon | null | — | optional |
loadingShow a spinner on the action button and disable both buttons. | boolean | false | optional |
actionDisabledDisable the primary action without a spinner. | boolean | false | optional |
triggerClicking the trigger snippet content opens the dialog. | Snippet | — | optional |
iconSnippet | Snippet | — | optional |
titleSnippet | Snippet | — | optional |
descriptionSnippet | Snippet | — | optional |
actions | Snippet | — | optional |
ref | HTMLDivElement | null | — | optional |
onOpenChange | (open: boolean) => void | — | optional |
onAction | (event: MouseEvent) => void | — | optional |
onCancel | (event: MouseEvent) => void | — | optional |
npm dependencies
Includes
Files installed (2)
src/lib/components/ui/alert-modal/AlertModal.svelte7.5 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' export type AlertModalTone = 'default' | 'destructive' | 'success' | 'warning' export type AlertModalIcon = 'info' | 'warning' | 'error' | 'success' export interface AlertModalProps extends HTMLAttributes<HTMLDivElement> { /** Controlled open state. Pair with `bind:open`. */ open?: boolean /** Uncontrolled initial open state. */ defaultOpen?: boolean /** Title rendered in the header. Override with the `titleSnippet`. */ title?: string /** Description rendered under the title. Override with the `descriptionSnippet`. */ description?: string /** Label for the primary action button. */ actionLabel?: string /** Label for the cancel button. Pass null to hide. */ cancelLabel?: string | null /** Visual tone — colors the icon and action button. */ tone?: AlertModalTone /** Quick icon shortcut. Override with the `iconSnippet`. */ icon?: AlertModalIcon | null /** Show a spinner on the action button and disable both buttons. */ loading?: boolean /** Disable the primary action without a spinner. */ actionDisabled?: boolean /** Clicking the trigger snippet content opens the dialog. */ trigger?: Snippet iconSnippet?: Snippet titleSnippet?: Snippet descriptionSnippet?: Snippet actions?: Snippet ref?: HTMLDivElement | null onOpenChange?: (open: boolean) => void onAction?: (event: MouseEvent) => void onCancel?: (event: MouseEvent) => void } </script> <script lang="ts"> import { CircleAlert, CircleCheck, Info, TriangleAlert } from '@lucide/svelte' import { Button } from '$lib/components/ui/button' import { cn } from '$lib/utils' let { open = $bindable(), defaultOpen = false, title = '', description = '', actionLabel = 'Continue', cancelLabel = 'Cancel', tone = 'default', icon, loading = false, actionDisabled = false, trigger, iconSnippet, titleSnippet, descriptionSnippet, actions, class: className, children, ref = $bindable(null), onOpenChange, onAction, onCancel, ...restProps }: AlertModalProps = $props() // Sync internal state with bind:open and emit changes back out so the // component works in both controlled and uncontrolled modes. // Snapshot once: later `defaultOpen` changes must not reopen a user-closed dialog. const getInitialOpen = () => open ?? defaultOpen let internalOpen = $state(getInitialOpen()) $effect.pre(() => { if (open !== undefined) internalOpen = open }) const isOpen = $derived(internalOpen) function setOpen(v: boolean) { // Keep the dialog open while an async action is in flight. if (!v && loading) return internalOpen = v open = v onOpenChange?.(v) } const ResolvedIcon = $derived.by(() => { switch (icon) { case 'info': return Info case 'success': return CircleCheck case 'warning': return TriangleAlert case 'error': return CircleAlert default: return null } }) const iconColorClass = $derived.by(() => { switch (tone) { case 'destructive': return 'text-destructive' case 'success': return 'text-success' case 'warning': return 'text-warning' default: return 'text-muted-foreground' } }) const actionToneClass = $derived.by(() => { switch (tone) { case 'destructive': return 'bg-destructive text-destructive-foreground hover:bg-destructive/90' case 'success': return 'bg-success text-success-foreground hover:bg-success/90' case 'warning': return 'bg-warning text-warning-foreground hover:bg-warning/90' default: return '' } }) // Focus the panel on open (alertdialog role) and lock body scroll while open. $effect(() => { if (!isOpen || !ref) return ref.focus({ preventScroll: true }) const prev = document.body.style.overflow document.body.style.overflow = 'hidden' return () => { document.body.style.overflow = prev } }) function handleAction(e: MouseEvent) { if (loading || actionDisabled) { e.preventDefault() return } onAction?.(e) if (!e.defaultPrevented) setOpen(false) } function handleCancel(e: MouseEvent) { onCancel?.(e) if (!e.defaultPrevented) setOpen(false) } const dialogId = $props.id() const titleId = `${dialogId}-title` const descriptionId = `${dialogId}-description` </script> {#if trigger} <!-- Clicking (or keyboard-activating, via bubbled click) the trigger content opens the dialog. --> <span data-slot="alert-modal-trigger" role="presentation" onclick={() => setOpen(true)}> {@render trigger()} </span> {/if} {#if isOpen} <div data-uipkge="" data-slot="alert-modal-overlay" class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-neutral-900/80" data-state="open" aria-hidden="true" ></div> <div bind:this={ref} role="alertdialog" aria-modal="true" aria-labelledby={titleId} aria-describedby={description || descriptionSnippet ? descriptionId : undefined} tabindex="-1" data-uipkge="" data-slot="alert-modal" data-state="open" data-tone={tone} class={cn( 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg', className, )} {...restProps} > <div class="flex flex-col gap-2 text-center sm:text-left"> {#if iconSnippet || ResolvedIcon} <div class={cn('bg-muted mb-2 flex size-10 items-center justify-center rounded-full', iconColorClass)}> {#if iconSnippet} {@render iconSnippet()} {:else if ResolvedIcon} <ResolvedIcon class="size-5" /> {/if} </div> {/if} <div id={titleId} data-slot="alert-modal-title" class="text-lg font-semibold"> {#if titleSnippet} {@render titleSnippet()} {:else} {title} {/if} </div> {#if description || descriptionSnippet} <div id={descriptionId} data-slot="alert-modal-description" class="text-muted-foreground text-sm"> {#if descriptionSnippet} {@render descriptionSnippet()} {:else} {description} {/if} </div> {/if} </div> {#if children} <div class="text-sm"> {@render children()} </div> {/if} <div class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"> {#if actions} {@render actions()} {:else} {#if cancelLabel} <Button variant="outline" disabled={loading} onclick={handleCancel}> {cancelLabel} </Button> {/if} <Button disabled={loading || actionDisabled} aria-busy={loading} class={actionToneClass} onclick={handleAction}> {#if loading} <span class="mr-2 inline-block size-4 animate-spin rounded-full border-2 border-current border-t-transparent" aria-hidden="true" ></span> {/if} {actionLabel} </Button> {/if} </div> </div> {/if}src/lib/components/ui/alert-modal/index.ts0.1 kB
export { default as AlertModal, type AlertModalProps, type AlertModalTone, type AlertModalIcon, } from './AlertModal.svelte'
Raw manifest:https://uipkge.dev/r/svelte/alert-modal.json