Alert Dialog Primitive
A modal dialog that interrupts the user with important content and expects a response.
Installation
Install the component via your command line.
npx expo install @rn-primitives/alert-dialog
Install @radix-ui/react-alert-dialog
npx expo install @radix-ui/react-alert-dialog
Copy/paste the following code for web to ~/components/primitives/alert-dialog/alert-dialog.web.tsx
import * as AlertDialog from '@radix-ui/react-alert-dialog';import { useAugmentedRef, useControllableState, useIsomorphicLayoutEffect,} from '~/components/primitives/hooks';import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { Pressable, Text, View, type GestureResponderEvent } from 'react-native';import type { ActionProps, ActionRef, CancelProps, CancelRef, ContentProps, ContentRef, DescriptionProps, DescriptionRef, OverlayProps, OverlayRef, PortalProps, RootContext, RootProps, RootRef, TitleProps, TitleRef, TriggerProps, TriggerRef,} from './types';
const AlertDialogContext = React.createContext<RootContext | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ({ asChild, open: openProp, defaultOpen, onOpenChange: onOpenChangeProp, ...viewProps }, ref) => { const [open = false, onOpenChange] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChangeProp, }); const Component = asChild ? Slot.View : View; return ( <AlertDialogContext.Provider value={{ open, onOpenChange }}> <AlertDialog.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}> <Component ref={ref} {...viewProps} /> </AlertDialog.Root> </AlertDialogContext.Provider> ); });
Root.displayName = 'RootAlertWebDialog';
function useRootContext() { const context = React.useContext(AlertDialogContext); if (!context) { throw new Error( 'AlertDialog compound components cannot be rendered outside the AlertDialog component' ); } return context;}
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, onPress: onPressProp, role: _role, disabled, ...props }, ref) => { const augmentedRef = useAugmentedRef({ ref }); const { onOpenChange, open } = useRootContext(); function onPress(ev: GestureResponderEvent) { if (onPressProp) { onPressProp(ev); } onOpenChange(!open); }
useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLButtonElement; augRef.dataset.state = open ? 'open' : 'closed'; augRef.type = 'button'; } }, [open]);
const Component = asChild ? Slot.Pressable : Pressable; return ( <AlertDialog.Trigger disabled={disabled ?? undefined} asChild> <Component ref={augmentedRef} onPress={onPress} role='button' disabled={disabled} {...props} /> </AlertDialog.Trigger> ); });
Trigger.displayName = 'TriggerAlertWebDialog';
function Portal({ forceMount, container, children }: PortalProps) { return <AlertDialog.Portal forceMount={forceMount} children={children} container={container} />;}
const Overlay = React.forwardRef<OverlayRef, OverlayProps>( ({ asChild, forceMount, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <AlertDialog.Overlay forceMount={forceMount}> <Component ref={ref} {...props} /> </AlertDialog.Overlay> ); });
Overlay.displayName = 'OverlayAlertWebDialog';
const Content = React.forwardRef<ContentRef, ContentProps>( ({ asChild, forceMount, onOpenAutoFocus, onCloseAutoFocus, onEscapeKeyDown, ...props }, ref) => { const augmentedRef = useAugmentedRef({ ref }); const { open } = useRootContext();
useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLDivElement; augRef.dataset.state = open ? 'open' : 'closed'; } }, [open]);
const Component = asChild ? Slot.View : View; return ( <AlertDialog.Content onOpenAutoFocus={onOpenAutoFocus} onCloseAutoFocus={onCloseAutoFocus} onEscapeKeyDown={onEscapeKeyDown} forceMount={forceMount} asChild > <Component ref={augmentedRef} {...props} /> </AlertDialog.Content> ); });
Content.displayName = 'ContentAlertWebDialog';
const Cancel = React.forwardRef<CancelRef, CancelProps>( ({ asChild, onPress: onPressProp, disabled, ...props }, ref) => { const augmentedRef = useAugmentedRef({ ref }); const { onOpenChange, open } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (onPressProp) { onPressProp(ev); } onOpenChange(!open); }
useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLButtonElement; augRef.type = 'button'; } }, []);
const Component = asChild ? Slot.Pressable : Pressable; return ( <> <AlertDialog.Cancel disabled={disabled ?? undefined} asChild> <Component ref={augmentedRef} onPress={onPress} role='button' disabled={disabled} {...props} /> </AlertDialog.Cancel> </> ); });
Cancel.displayName = 'CancelAlertWebDialog';
const Action = React.forwardRef<ActionRef, ActionProps>( ({ asChild, onPress: onPressProp, disabled, ...props }, ref) => { const augmentedRef = useAugmentedRef({ ref }); const { onOpenChange, open } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (onPressProp) { onPressProp(ev); } onOpenChange(!open); }
useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLButtonElement; augRef.type = 'button'; } }, []);
const Component = asChild ? Slot.Pressable : Pressable; return ( <> <AlertDialog.Action disabled={disabled ?? undefined} asChild> <Component ref={augmentedRef} onPress={onPress} role='button' disabled={disabled} {...props} /> </AlertDialog.Action> </> ); });
Action.displayName = 'ActionAlertWebDialog';
const Title = React.forwardRef<TitleRef, TitleProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.Text : Text; return ( <AlertDialog.Title asChild> <Component ref={ref} {...props} /> </AlertDialog.Title> );});
Title.displayName = 'TitleAlertWebDialog';
const Description = React.forwardRef<DescriptionRef, DescriptionProps>( ({ asChild, ...props }, ref) => { const Component = asChild ? Slot.Text : Text; return ( <AlertDialog.Description asChild> <Component ref={ref} {...props} /> </AlertDialog.Description> ); });
Description.displayName = 'DescriptionAlertWebDialog';
export { Action, Cancel, Content, Description, Overlay, Portal, Root, Title, Trigger, useRootContext,};
Copy/paste the following code for native to ~/components/primitives/alert-dialog/alert-dialog.tsx
import { useControllableState } from '~/components/primitives/hooks';import { Portal as RNPPortal } from '~/components/primitives/portal';import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { BackHandler, Pressable, Text, View, type GestureResponderEvent } from 'react-native';import type { ActionProps, ActionRef, CancelProps, CancelRef, ContentProps, ContentRef, DescriptionProps, DescriptionRef, OverlayProps, OverlayRef, PortalProps, RootContext, RootProps, RootRef, TitleProps, TitleRef, TriggerProps, TriggerRef,} from './types';
const AlertDialogContext = React.createContext<(RootContext & { nativeID: string }) | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ({ asChild, open: openProp, defaultOpen, onOpenChange: onOpenChangeProp, ...viewProps }, ref) => { const nativeID = React.useId(); const [open = false, onOpenChange] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChangeProp, }); const Component = asChild ? Slot.View : View; return ( <AlertDialogContext.Provider value={{ open, onOpenChange, nativeID, }} > <Component ref={ref} {...viewProps} /> </AlertDialogContext.Provider> ); });
Root.displayName = 'RootNativeAlertDialog';
function useRootContext() { const context = React.useContext(AlertDialogContext); if (!context) { throw new Error( 'AlertDialog compound components cannot be rendered outside the AlertDialog component' ); } return context;}
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, onPress: onPressProp, disabled = false, ...props }, ref) => { const { open: value, onOpenChange } = useRootContext();
function onPress(ev: GestureResponderEvent) { onOpenChange(!value); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} aria-disabled={disabled ?? undefined} role='button' onPress={onPress} disabled={disabled ?? undefined} {...props} /> ); });
Trigger.displayName = 'TriggerNativeAlertDialog';
/** * @warning when using a custom `<PortalHost />`, you might have to adjust the Content's sideOffset to account for nav elements like headers. */function Portal({ forceMount, hostName, children }: PortalProps) { const value = useRootContext();
if (!forceMount) { if (!value.open) { return null; } }
return ( <RNPPortal hostName={hostName} name={`${value.nativeID}_portal`}> <AlertDialogContext.Provider value={value}>{children}</AlertDialogContext.Provider> </RNPPortal> );}
const Overlay = React.forwardRef<OverlayRef, OverlayProps>( ({ asChild, forceMount, ...props }, ref) => { const { open: value } = useRootContext();
if (!forceMount) { if (!value) { return null; } }
const Component = asChild ? Slot.View : View; return <Component ref={ref} {...props} />; });
Overlay.displayName = 'OverlayNativeAlertDialog';
const Content = React.forwardRef<ContentRef, ContentProps>( ({ asChild, forceMount, ...props }, ref) => { const { open: value, nativeID, onOpenChange } = useRootContext();
React.useEffect(() => { const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { onOpenChange(false); return true; });
return () => { backHandler.remove(); }; }, []);
if (!forceMount) { if (!value) { return null; } }
const Component = asChild ? Slot.View : View; return ( <Component ref={ref} role='alertdialog' nativeID={nativeID} aria-labelledby={`${nativeID}_label`} aria-describedby={`${nativeID}_desc`} aria-modal={true} {...props} /> ); });
Content.displayName = 'ContentNativeAlertDialog';
const Cancel = React.forwardRef<CancelRef, CancelProps>( ({ asChild, onPress: onPressProp, disabled = false, ...props }, ref) => { const { onOpenChange } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (disabled) return; onOpenChange(false); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} aria-disabled={disabled ?? undefined} role='button' onPress={onPress} disabled={disabled ?? undefined} {...props} /> ); });
Cancel.displayName = 'CloseNativeAlertDialog';
const Action = React.forwardRef<ActionRef, ActionProps>( ({ asChild, onPress: onPressProp, disabled = false, ...props }, ref) => { const { onOpenChange } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (disabled) return; onOpenChange(false); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} aria-disabled={disabled ?? undefined} role='button' onPress={onPress} disabled={disabled ?? undefined} {...props} /> ); });
Action.displayName = 'ActionNativeAlertDialog';
const Title = React.forwardRef<TitleRef, TitleProps>(({ asChild, ...props }, ref) => { const { nativeID } = useRootContext(); const Component = asChild ? Slot.Text : Text; return <Component ref={ref} role='heading' nativeID={`${nativeID}_label`} {...props} />;});
Title.displayName = 'TitleNativeAlertDialog';
const Description = React.forwardRef<DescriptionRef, DescriptionProps>( ({ asChild, ...props }, ref) => { const { nativeID } = useRootContext(); const Component = asChild ? Slot.Text : Text; return <Component ref={ref} nativeID={`${nativeID}_desc`} {...props} />; });
Description.displayName = 'DescriptionNativeAlertDialog';
export { Action, Cancel, Content, Description, Overlay, Portal, Root, Title, Trigger, useRootContext,};
Copy/paste the following code for types to ~/components/primitives/alert-dialog/types.ts
import type { ForceMountable, PressableRef, SlottablePressableProps, SlottableTextProps, SlottableViewProps, TextRef, ViewRef,} from '~/components/primitives/types';
type RootProps = { open?: boolean; onOpenChange?: (value: boolean) => void; defaultOpen?: boolean;} & SlottableViewProps;
interface RootContext { open: boolean; onOpenChange: (value: boolean) => void;}
interface PortalProps extends ForceMountable { children: React.ReactNode; /** * Platform: NATIVE ONLY */ hostName?: string; /** * Platform: WEB ONLY */ container?: HTMLElement | null | undefined;}type OverlayProps = ForceMountable & SlottableViewProps;
type ContentProps = ForceMountable & SlottableViewProps & { /** * Platform: WEB ONLY */ onOpenAutoFocus?: (ev: Event) => void; /** * Platform: WEB ONLY */ onCloseAutoFocus?: (ev: Event) => void; /** * Platform: WEB ONLY */ onEscapeKeyDown?: (ev: Event) => void; };
type TriggerProps = SlottablePressableProps;type CancelProps = SlottablePressableProps;type ActionProps = SlottablePressableProps;type TitleProps = SlottableTextProps;type DescriptionProps = SlottableTextProps;
type ActionRef = PressableRef;type CancelRef = PressableRef;type ContentRef = ViewRef;type DescriptionRef = TextRef;type OverlayRef = ViewRef;type RootRef = ViewRef;type TitleRef = TextRef;type TriggerRef = PressableRef;
export type { ActionProps, ActionRef, CancelProps, CancelRef, ContentProps, ContentRef, DescriptionProps, DescriptionRef, OverlayProps, OverlayRef, PortalProps, RootContext, RootProps, RootRef, TitleProps, TitleRef, TriggerProps, TriggerRef,};
Copy/paste the following code for exporting to ~/components/primitives/alert-dialog/index.ts
export * from './alert-dialog';export * from './types';
Copy/paste the following code for native to ~/components/primitives/alert-dialog/index.tsx
import { useControllableState } from '~/components/primitives/hooks';import { Portal as RNPPortal } from '~/components/primitives/portal';import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { BackHandler, Pressable, Text, View, type GestureResponderEvent } from 'react-native';import type { ActionProps, ActionRef, CancelProps, CancelRef, ContentProps, ContentRef, DescriptionProps, DescriptionRef, OverlayProps, OverlayRef, PortalProps, RootContext, RootProps, RootRef, TitleProps, TitleRef, TriggerProps, TriggerRef,} from './types';
const AlertDialogContext = React.createContext<(RootContext & { nativeID: string }) | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ({ asChild, open: openProp, defaultOpen, onOpenChange: onOpenChangeProp, ...viewProps }, ref) => { const nativeID = React.useId(); const [open = false, onOpenChange] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChangeProp, }); const Component = asChild ? Slot.View : View; return ( <AlertDialogContext.Provider value={{ open, onOpenChange, nativeID, }} > <Component ref={ref} {...viewProps} /> </AlertDialogContext.Provider> ); });
Root.displayName = 'RootNativeAlertDialog';
function useRootContext() { const context = React.useContext(AlertDialogContext); if (!context) { throw new Error( 'AlertDialog compound components cannot be rendered outside the AlertDialog component' ); } return context;}
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, onPress: onPressProp, disabled = false, ...props }, ref) => { const { open: value, onOpenChange } = useRootContext();
function onPress(ev: GestureResponderEvent) { onOpenChange(!value); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} aria-disabled={disabled ?? undefined} role='button' onPress={onPress} disabled={disabled ?? undefined} {...props} /> ); });
Trigger.displayName = 'TriggerNativeAlertDialog';
/** * @warning when using a custom `<PortalHost />`, you might have to adjust the Content's sideOffset to account for nav elements like headers. */function Portal({ forceMount, hostName, children }: PortalProps) { const value = useRootContext();
if (!forceMount) { if (!value.open) { return null; } }
return ( <RNPPortal hostName={hostName} name={`${value.nativeID}_portal`}> <AlertDialogContext.Provider value={value}>{children}</AlertDialogContext.Provider> </RNPPortal> );}
const Overlay = React.forwardRef<OverlayRef, OverlayProps>( ({ asChild, forceMount, ...props }, ref) => { const { open: value } = useRootContext();
if (!forceMount) { if (!value) { return null; } }
const Component = asChild ? Slot.View : View; return <Component ref={ref} {...props} />; });
Overlay.displayName = 'OverlayNativeAlertDialog';
const Content = React.forwardRef<ContentRef, ContentProps>( ({ asChild, forceMount, ...props }, ref) => { const { open: value, nativeID, onOpenChange } = useRootContext();
React.useEffect(() => { const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { onOpenChange(false); return true; });
return () => { backHandler.remove(); }; }, []);
if (!forceMount) { if (!value) { return null; } }
const Component = asChild ? Slot.View : View; return ( <Component ref={ref} role='alertdialog' nativeID={nativeID} aria-labelledby={`${nativeID}_label`} aria-describedby={`${nativeID}_desc`} aria-modal={true} {...props} /> ); });
Content.displayName = 'ContentNativeAlertDialog';
const Cancel = React.forwardRef<CancelRef, CancelProps>( ({ asChild, onPress: onPressProp, disabled = false, ...props }, ref) => { const { onOpenChange } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (disabled) return; onOpenChange(false); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} aria-disabled={disabled ?? undefined} role='button' onPress={onPress} disabled={disabled ?? undefined} {...props} /> ); });
Cancel.displayName = 'CloseNativeAlertDialog';
const Action = React.forwardRef<ActionRef, ActionProps>( ({ asChild, onPress: onPressProp, disabled = false, ...props }, ref) => { const { onOpenChange } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (disabled) return; onOpenChange(false); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} aria-disabled={disabled ?? undefined} role='button' onPress={onPress} disabled={disabled ?? undefined} {...props} /> ); });
Action.displayName = 'ActionNativeAlertDialog';
const Title = React.forwardRef<TitleRef, TitleProps>(({ asChild, ...props }, ref) => { const { nativeID } = useRootContext(); const Component = asChild ? Slot.Text : Text; return <Component ref={ref} role='heading' nativeID={`${nativeID}_label`} {...props} />;});
Title.displayName = 'TitleNativeAlertDialog';
const Description = React.forwardRef<DescriptionRef, DescriptionProps>( ({ asChild, ...props }, ref) => { const { nativeID } = useRootContext(); const Component = asChild ? Slot.Text : Text; return <Component ref={ref} nativeID={`${nativeID}_desc`} {...props} />; });
Description.displayName = 'DescriptionNativeAlertDialog';
export { Action, Cancel, Content, Description, Overlay, Portal, Root, Title, Trigger, useRootContext,};
Copy/paste the following code for types to ~/components/primitives/alert-dialog/types.ts
import type { ForceMountable, PressableRef, SlottablePressableProps, SlottableTextProps, SlottableViewProps, TextRef, ViewRef,} from '~/components/primitives/types';
type RootProps = { open?: boolean; onOpenChange?: (value: boolean) => void; defaultOpen?: boolean;} & SlottableViewProps;
interface RootContext { open: boolean; onOpenChange: (value: boolean) => void;}
interface PortalProps extends ForceMountable { children: React.ReactNode; /** * Platform: NATIVE ONLY */ hostName?: string; /** * Platform: WEB ONLY */ container?: HTMLElement | null | undefined;}type OverlayProps = ForceMountable & SlottableViewProps;
type ContentProps = ForceMountable & SlottableViewProps & { /** * Platform: WEB ONLY */ onOpenAutoFocus?: (ev: Event) => void; /** * Platform: WEB ONLY */ onCloseAutoFocus?: (ev: Event) => void; /** * Platform: WEB ONLY */ onEscapeKeyDown?: (ev: Event) => void; };
type TriggerProps = SlottablePressableProps;type CancelProps = SlottablePressableProps;type ActionProps = SlottablePressableProps;type TitleProps = SlottableTextProps;type DescriptionProps = SlottableTextProps;
type ActionRef = PressableRef;type CancelRef = PressableRef;type ContentRef = ViewRef;type DescriptionRef = TextRef;type OverlayRef = ViewRef;type RootRef = ViewRef;type TitleRef = TextRef;type TriggerRef = PressableRef;
export type { ActionProps, ActionRef, CancelProps, CancelRef, ContentProps, ContentRef, DescriptionProps, DescriptionRef, OverlayProps, OverlayRef, PortalProps, RootContext, RootProps, RootRef, TitleProps, TitleRef, TriggerProps, TriggerRef,};
Usage
import * as AlertDialogPrimitive from '@rn-primitives/alert-dialog';import { Text } from 'react-native';
function Example() { return ( <AlertDialogPrimitive.Root> <AlertDialogPrimitive.Trigger> <Text>Show Alert Dialog</Text> </AlertDialogPrimitive.Trigger>
<AlertDialogPrimitive.Portal> <AlertDialogPrimitive.Overlay> <AlertDialogPrimitive.Content> <AlertDialogPrimitive.Title>Are you absolutely sure?</AlertDialogPrimitive.Title> <AlertDialogPrimitive.Description> This action cannot be undone. This will permanently delete your account and remove your data from our servers. </AlertDialogPrimitive.Description>
<AlertDialogPrimitive.Cancel><Text>Cancel</Text></AlertDialogPrimitive.Cancel> <AlertDialogPrimitive.Action><Text>Continue</Text></AlertDialogPrimitive.Action> </AlertDialogPrimitive.Content> </AlertDialogPrimitive.Overlay> </AlertDialogPrimitive.Portal> </AlertDialogPrimitive.Root> );}
Props
Root
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
open | boolean | (optional) |
onOpenChange | (value: boolean) => void | (optional) |
defaultOpen | boolean | (optional) |
Trigger
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
Overlay
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
forceMount | true | undefined; | (optional) |
Portal
Prop | Type | Note |
---|---|---|
children* | React.ReactNode | |
forceMount | true | undefined | (optional) |
hostName | string | Web Only (optional) |
container | HTMLElement | null | undefined | Web Only (optional) |
Content
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
forceMount | true | undefined | (optional) |
alignOffset | number | Native Only (optional) |
insets | Insets | Native Only (optional) |
avoidCollisions | boolean | Native Only (optional) |
align | ’start’ | ‘center’ | ‘end’ | Native Only (optional) |
side | ’top’ | ‘bottom’ | Native Only (optional) |
sideOffset | number | Native Only (optional) |
disablePositioningStyle | boolean | Native Only (optional) |
onOpenAutoFocus | (ev: Event) => void | Web Only (optional) |
onCloseAutoFocus | (ev: Event) => void | Web Only (optional) |
onEscapeKeyDown | (ev: Event) => void | Web Only (optional) |
Title
Extends Text
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
Description
Extends Text
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
Cancel
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
Action
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
useRootContext
Must be used within a Root
component. It provides the following values from the alert-dialog: open
, and onOpenChange
.