shadimage
Awesome Cursor Rules
uialert-dialogsource: ok · refreshed

Alert Dialog

Alert dialog for Tile UI.

npx shadcn add "https://react.tileui.zmorg.cn/r/alert-dialog.json"
free

Interaction

modal

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ui-primitives@radix-ui/react-slot

Registry dependencies

@tile-ui/corebutton@tile-ui/portal@tile-ui/styles

Files3 · source cached 10/2/2026, 12:45:40 AM

packages/react/src/components/alert-dialog/alert-dialog.tsx
import React, { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Slot } from '@radix-ui/react-slot';
import type { ButtonSize, ButtonVariant } from '../lib/core';
import { alertDialogStyleKeys, getAlertDialogState } from '../lib/core';
import type { AlertDialogBaseProps, AlertDialogSize } from '../lib/core';
import { Button } from '../button';
import { usePortalContainer, type PortalContainer } from '../portal';
import styles from './alert-dialog.module.scss';

interface AlertDialogContextValue {
	open: boolean;
	setOpen: (open: boolean) => void;
	close: () => void;
	titleId: string;
	descriptionId: string;
}

const AlertDialogContext = createContext<AlertDialogContextValue | null>(null);

function useAlertDialogContext(): AlertDialogContextValue {
	const context = useContext(AlertDialogContext);
	if (!context) {
		throw new Error('AlertDialog sub-components must be used within <AlertDialog>.');
	}
	return context;
}

export interface AlertDialogProps extends AlertDialogBaseProps {
	open?: boolean;
	defaultOpen?: boolean;
	onOpenChange?: (open: boolean) => void;
	children?: React.ReactNode;
}

function AlertDialog({ open, defaultOpen = false, onOpenChange, children }: AlertDialogProps) {
	const [internalOpen, setInternalOpen] = useState(defaultOpen);
	const onOpenChangeRef = useRef(onOpenChange);

	useEffect(() => {
		onOpenChangeRef.current = onOpenChange;
	});

	const setOpen = useCallback(
		(next: boolean) => {
			if (open === undefined) {
				setInternalOpen(next);
			}

			onOpenChangeRef.current?.(next);
		},
		[open],
	);

	const close = useCallback(() => {
		setOpen(false);
	}, [setOpen]);

	const isOpen = open !== undefined ? open : internalOpen;
	const titleId = useId();
	const descriptionId = useId();

	return <AlertDialogContext.Provider value={{ open: isOpen, setOpen, close, titleId, descriptionId }}>{children}</AlertDialogContext.Provider>;
}

AlertDialog.displayName = 'AlertDialog';

export interface AlertDialogTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
	asChild?: boolean;
}

const AlertDialogTrigger = React.forwardRef<HTMLButtonElement, AlertDialogTriggerProps>(({ asChild = false, className = '', children, onClick, ...props }, ref) => {
	const context = useAlertDialogContext();
	const Comp = asChild ? Slot : 'button';

	return (
		<Comp
			ref={ref}
			type={asChild ? undefined : 'button'}
			className={className}
			onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
				onClick?.(event);
				context.setOpen(true);
			}}
			{...props}>
			{children}
		</Comp>
	);
});
AlertDialogTrigger.displayName = 'AlertDialogTrigger';

export interface AlertDialogOverlayProps extends React.HTMLAttributes<HTMLDivElement> {}

const AlertDialogOverlay = React.forwardRef<HTMLDivElement, AlertDialogOverlayProps>(({ className = '', onClick, ...props }, ref) => {
	const context = useAlertDialogContext();

	if (!context.open) {
		return null;
	}

	return (
		<div
			ref={ref}
			data-state={getAlertDialogState(context.open)}
			className={`${styles[alertDialogStyleKeys.overlay]} ${className}`}
			onClick={(event: React.MouseEvent<HTMLDivElement>) => {
				onClick?.(event);
				context.close();
			}}
			{...props}
		/>
	);
});
AlertDialogOverlay.displayName = 'AlertDialogOverlay';

export interface AlertDialogContentProps extends React.HTMLAttributes<HTMLDivElement> {
	size?: AlertDialogSize;
	container?: PortalContainer;
}

const AlertDialogContent = React.forwardRef<HTMLDivElement, AlertDialogContentProps>(({ className = '', size = 'default', container, children, ...props }, ref) => {
	const { open, close, titleId, descriptionId } = useAlertDialogContext();
	const portalContainer = usePortalContainer(container);
	const contentRef = useRef<HTMLDivElement | null>(null);

	function setContentRef(element: HTMLDivElement | null) {
		contentRef.current = element;

		if (typeof ref === 'function') {
			ref(element);
		} else if (ref) {
			ref.current = element;
		}
	}

	useLayoutEffect(() => {
		if (!open) {
			return;
		}

		const previouslyFocused = document.activeElement as HTMLElement | null;
		const previousOverflow = document.body.style.overflow;
		document.body.style.overflow = 'hidden';

		const handleKeyDown = (event: KeyboardEvent) => {
			if (event.key === 'Escape') {
				close();
				return;
			}

			if (event.key === 'Tab') {
				const container = contentRef.current;
				if (!container) {
					return;
				}
				const focusables = Array.from(
					container.querySelectorAll<HTMLElement>(
						'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
					),
				).filter((el) => el.getClientRects().length > 0);
				if (focusables.length === 0) {
					event.preventDefault();
					return;
				}
				const first = focusables[0];
				const last = focusables[focusables.length - 1];
				const active = document.activeElement;

				if (event.shiftKey) {
					if (active === first || active === container || !container.contains(active)) {
						event.preventDefault();
						last.focus();
					}
				} else if (active === last || !container.contains(active)) {
					event.preventDefault();
					first.focus();
				}
			}
		};

		document.addEventListener('keydown', handleKeyDown);

		const contentEl = contentRef.current;
		if (contentEl) {
			contentEl.focus();
		}

		return () => {
			document.body.style.overflow = previousOverflow;
			document.removeEventListener('keydown', handleKeyDown);
			previouslyFocused?.focus();
		};
	}, [close, open]);

	if (!open || !portalContainer) {
		return null;
	}

	return createPortal(
		<>
			<AlertDialogOverlay />
			<div
				ref={setContentRef}
				role="alertdialog"
				aria-modal="true"
				aria-labelledby={titleId}
				aria-describedby={descriptionId}
				tabIndex={-1}
				data-state={getAlertDialogState(open)}
				data-size={size}
				className={`${styles[alertDialogStyleKeys.content]} ${className}`}
				{...props}>
				{children}
			</div>
		</>,
		portalContainer,
	);
});
AlertDialogContent.displayName = 'AlertDialogContent';

export interface AlertDialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}

const AlertDialogHeader = React.forwardRef<HTMLDivElement, AlertDialogHeaderProps>(({ className = '', children, ...props }, ref) => {
	return (
		<div ref={ref} className={`${styles[alertDialogStyleKeys.header]} ${className}`} {...props}>
			{children}
		</div>
	);
});
AlertDialogHeader.displayName = 'AlertDialogHeader';

export interface AlertDialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {}

const AlertDialogFooter = React.forwardRef<HTMLDivElement, AlertDialogFooterProps>(({ className = '', children, ...props }, ref) => {
	return (
		<div ref={ref} className={`${styles[alertDialogStyleKeys.footer]} ${className}`} {...props}>
			{children}
		</div>
	);
});
AlertDialogFooter.displayName = 'AlertDialogFooter';

export interface AlertDialogTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {}

const AlertDialogTitle = React.forwardRef<HTMLHeadingElement, AlertDialogTitleProps>(({ className = '', children, ...props }, ref) => {
	const context = useAlertDialogContext();

	return (
		<h2 ref={ref} id={context.titleId} className={`${styles[alertDialogStyleKeys.title]} ${className}`} {...props}>
			{children}
		</h2>
	);
});
AlertDialogTitle.displayName = 'AlertDialogTitle';

export interface AlertDialogDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {}

const AlertDialogDescription = React.forwardRef<HTMLParagraphElement, AlertDialogDescriptionProps>(({ className = '', children, ...props }, ref) => {
	const context = useAlertDialogContext();

	return (
		<p ref={ref} id={context.descriptionId} className={`${styles[alertDialogStyleKeys.description]} ${className}`} {...props}>
			{children}
		</p>
	);
});
AlertDialogDescription.displayName = 'AlertDialogDescription';

export interface AlertDialogActionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
	variant?: ButtonVariant;
	size?: ButtonSize;
}

const AlertDialogAction = React.forwardRef<HTMLButtonElement, AlertDialogActionProps>(
	({ className = '', variant = 'default', size = 'default', children, onClick, ...props }, ref) => {
		const context = useAlertDialogContext();

		return (
			<Button
				ref={ref}
				type="button"
				variant={variant}
				size={size}
				className={className}
				onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
					onClick?.(event);
					context.close();
				}}
				{...props}>
				{children}
			</Button>
		);
	},
);
AlertDialogAction.displayName = 'AlertDialogAction';

export interface AlertDialogCancelProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
	variant?: ButtonVariant;
	size?: ButtonSize;
}

const AlertDialogCancel = React.forwardRef<HTMLButtonElement, AlertDialogCancelProps>(
	({ className = '', variant = 'outline', size = 'default', children, onClick, ...props }, ref) => {
		const context = useAlertDialogContext();

		return (
			<Button
				ref={ref}
				type="button"
				variant={variant}
				size={size}
				className={className}
				onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
					onClick?.(event);
					context.close();
				}}
				{...props}>
				{children}
			</Button>
		);
	},
);
AlertDialogCancel.displayName = 'AlertDialogCancel';

export {
	AlertDialog,
	AlertDialogTrigger,
	AlertDialogOverlay,
	AlertDialogContent,
	AlertDialogHeader,
	AlertDialogFooter,
	AlertDialogTitle,
	AlertDialogDescription,
	AlertDialogAction,
	AlertDialogCancel,
};
export default AlertDialog;
packages/styles/scss/components/alert-dialog.module.scss
@use '../../../styles/variables/colors' as *;

// 半透明遮罩
.overlay {
	position: fixed;
	inset: 0;
	z-index: $z-modal;
	background-color: $overlay;

	&[data-state='open'] {
		animation: alert-dialog-overlay-in $transition-normal;
	}

	&[data-state='closed'] {
		opacity: 0;
	}
}

@keyframes alert-dialog-overlay-in {
	from {
		opacity: 0;
	}

	to {
		opacity: 1;
	}
}

// 弹层内容面板
.content {
	position: fixed;
	top: 50%;
	left: 50%;
	z-index: $z-modal;
	display: grid;
	width: 100%;
	max-width: calc(100% - 2rem);
	transform: translate(-50%, -50%);
	gap: $spacing-4;
	border: 1px solid $border;
	border-radius: $radius-lg;
	background-color: $card;
	color: $card-foreground;
	padding: $spacing-6;
	box-shadow: $shadow-lg;
	outline: none;

	@media (min-width: 640px) {
		max-width: 32rem;
	}

	&[data-size='sm'] {
		max-width: 20rem;
	}

	&[data-state='open'] {
		animation: alert-dialog-content-in $transition-normal;
	}

	&[data-state='closed'] {
		opacity: 0;
		transform: translate(-50%, -50%) scale(0.95);
	}
}

@keyframes alert-dialog-content-in {
	from {
		opacity: 0;
		transform: translate(-50%, -50%) scale(0.95);
	}

	to {
		opacity: 1;
		transform: translate(-50%, -50%) scale(1);
	}
}

.header {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: $spacing-1_5;
	text-align: center;

	@media (min-width: 640px) {
		.content[data-size='default'] & {
			align-items: flex-start;
			text-align: left;
		}
	}
}

.footer {
	display: flex;
	flex-direction: column-reverse;
	gap: $spacing-2;

	@media (min-width: 640px) {
		.content[data-size='default'] & {
			flex-direction: row;
			justify-content: flex-end;
		}
	}
}

.title {
	margin: 0;
	font-size: $text-lg;
	font-weight: $font-semibold;
	line-height: $leading-tight;
	color: $foreground;
}

.description {
	margin: 0;
	font-size: $text-sm;
	line-height: $leading-normal;
	color: $muted-foreground;
}
packages/react/src/components/alert-dialog/index.ts
export {
	AlertDialog,
	AlertDialogTrigger,
	AlertDialogOverlay,
	AlertDialogContent,
	AlertDialogHeader,
	AlertDialogFooter,
	AlertDialogTitle,
	AlertDialogDescription,
	AlertDialogAction,
	AlertDialogCancel,
	type AlertDialogProps,
	type AlertDialogTriggerProps,
	type AlertDialogOverlayProps,
	type AlertDialogContentProps,
	type AlertDialogHeaderProps,
	type AlertDialogFooterProps,
	type AlertDialogTitleProps,
	type AlertDialogDescriptionProps,
	type AlertDialogActionProps,
	type AlertDialogCancelProps,
} from './alert-dialog';

More from Awesome Cursor Rules

Browse all components in this library →