Radix homepageRadix Homepage
PrimitivesBeta

Dialog

A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.

import React from 'react';
import { styled, keyframes } from '@stitches/react';
import { violet, blackA, mauve, green } from '@radix-ui/colors';
import { Cross2Icon } from '@radix-ui/react-icons';
import * as DialogPrimitive from '@radix-ui/react-dialog';
const overlayShow = keyframes({
'0%': { opacity: 0 },
'100%': { opacity: 1 },
});
const contentShow = keyframes({
'0%': { opacity: 0, transform: 'translate(-50%, -48%) scale(.96)' },
'100%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
});
const StyledOverlay = styled(DialogPrimitive.Overlay, {
backgroundColor: blackA.blackA9,
position: 'fixed',
inset: 0,
'@media (prefers-reduced-motion: no-preference)': {
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
},
});
function Root({ children, ...props }) {
return (
<DialogPrimitive.Root {...props}>
<StyledOverlay />
{children}
</DialogPrimitive.Root>
);
}
const StyledContent = styled(DialogPrimitive.Content, {
backgroundColor: 'white',
borderRadius: 6,
boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '90vw',
maxWidth: '450px',
maxHeight: '85vh',
padding: 25,
'@media (prefers-reduced-motion: no-preference)': {
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
willChange: 'transform',
},
'&:focus': { outline: 'none' },
});
const StyledTitle = styled(DialogPrimitive.Title, {
margin: 0,
fontWeight: 500,
color: mauve.mauve12,
fontSize: 17,
});
const StyledDescription = styled(DialogPrimitive.Description, {
margin: '10px 0 20px',
color: mauve.mauve11,
fontSize: 15,
lineHeight: 1.5,
});
// Exports
const Dialog = Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogContent = StyledContent;
const DialogTitle = StyledTitle;
const DialogDescription = StyledDescription;
const DialogClose = DialogPrimitive.Close;
// Your app...
const Flex = styled('div', { display: 'flex' });
const Box = styled('div', {});
const Button = styled('button', {
all: 'unset',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 4,
padding: '0 15px',
fontSize: 15,
lineHeight: 1,
fontWeight: 500,
height: 35,
variants: {
variant: {
violet: {
backgroundColor: 'white',
color: violet.violet11,
boxShadow: `0 2px 10px ${blackA.blackA7}`,
'&:hover': { backgroundColor: mauve.mauve3 },
'&:focus': { boxShadow: `0 0 0 2px black` },
},
green: {
backgroundColor: green.green4,
color: green.green11,
'&:hover': { backgroundColor: green.green5 },
'&:focus': { boxShadow: `0 0 0 2px ${green.green7}` },
},
},
},
defaultVariants: {
variant: 'violet',
},
});
const IconButton = styled('button', {
all: 'unset',
fontFamily: 'inherit',
borderRadius: '100%',
height: 25,
width: 25,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
color: violet.violet11,
position: 'absolute',
top: 10,
right: 10,
'&:hover': { backgroundColor: violet.violet4 },
'&:focus': { boxShadow: `0 0 0 2px ${violet.violet7}` },
});
const Fieldset = styled('fieldset', {
all: 'unset',
display: 'flex',
gap: 20,
alignItems: 'center',
marginBottom: 15,
});
const Label = styled('label', {
fontSize: 15,
color: violet.violet11,
width: 90,
textAlign: 'right',
});
const Input = styled('input', {
all: 'unset',
width: '100%',
flex: '1',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 4,
padding: '0 10px',
fontSize: 15,
lineHeight: 1,
color: violet.violet11,
boxShadow: `0 0 0 1px ${violet.violet7}`,
height: 35,
'&:focus': { boxShadow: `0 0 0 2px ${violet.violet8}` },
});
const DialogDemo = () => (
<Dialog>
<DialogTrigger asChild>
<Button size="large">Edit profile</Button>
</DialogTrigger>
<DialogContent >
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
<Fieldset>
<Label htmlFor="name">Name</Label>
<Input id="name" defaultValue="Pedro Duarte" />
</Fieldset>
<Fieldset>
<Label htmlFor="username">Username</Label>
<Input id="username" defaultValue="@peduarte" />
</Fieldset>
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
<DialogClose asChild>
<Button aria-label="Close" variant="green">
Save changes
</Button>
</DialogClose>
</Flex>
<DialogClose asChild>
<IconButton>
<Cross2Icon />
</IconButton>
</DialogClose>
</DialogContent>
</Dialog>
);
export default DialogDemo;

Features

  • Supports modal and non-modal modes.
  • Focus is automatically trapped when modal.
  • Can be controlled or uncontrolled.
  • Manages screen reader announcements with Title and Description components.
  • Esc closes the component automatically.

Install the component from your command line.

npm install @radix-ui/react-dialog

Import all parts and piece them together.

import * as Dialog from '@radix-ui/react-dialog';
export default () => (
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
</Dialog.Content>
</Dialog.Root>
);

Contains all the parts of a dialog.

PropTypeDefault
defaultOpenbooleanNo default value
openbooleanNo default value
onOpenChangefunctionNo default value
modalbooleantrue
idstringNo default value

The button that opens the dialog.

PropTypeDefault
asChildbooleanfalse

A layer that covers the inert portion of the view when the dialog is open.

PropTypeDefault
asChildbooleanfalse
forceMountbooleanNo default value

Contains content to be rendered in the open dialog.

PropTypeDefault
asChildbooleanfalse
allowPinchZoombooleanfalse
forceMountbooleanNo default value
onOpenAutoFocusfunctionNo default value
onCloseAutoFocusfunctionNo default value
onEscapeKeyDownfunctionNo default value
onPointerDownOutsidefunctionNo default value
onInteractOutsidefunctionNo default value

The button that closes the dialog.

PropTypeDefault
asChildbooleanfalse

An accessible name to be announced when the dialog is opened. Alternatively, you can provide aria-label or aria-labelledby to Dialog.Content and exclude this component.

PropTypeDefault
asChildbooleanfalse

An accessible description to be announced when the dialog is opened. Alternatively, you can provide aria-describedby to Dialog.Content and exclude this component.

PropTypeDefault
asChildbooleanfalse

Adheres to the Dialog WAI-ARIA design pattern.

KeyDescription
SpaceOpens/closes the dialog.
EnterOpens/closes the dialog.
TabMoves focus to the next focusable element.
Shift + TabMoves focus to the previous focusable element.
EscCloses the dialog and moves focus to Dialog.Trigger.

Create your own API by abstracting the primitive parts into your own component.

This example abstracts the Dialog.Overlay and Dialog.Close parts.

Usage

import { Dialog, DialogTrigger, DialogContent } from './your-dialog';
export default () => (
<Dialog>
<DialogTrigger>Dialog trigger</DialogTrigger>
<DialogContent>Dialog Content</DialogContent>
</Dialog>
);

Implementation

// your-dialog.js
import React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Cross1Icon } from '@radix-ui/react-icons';
export function Dialog({ children, ...props }) {
return (
<DialogPrimitive.Root {...props}>
<DialogPrimitive.Overlay />
{children}
</DialogPrimitive.Root>
);
}
export const DialogContent = React.forwardRef(
({ children, ...props }, forwardedRef) => (
<DialogPrimitive.Content {...props} ref={forwardedRef}>
{children}
<DialogPrimitive.Close>
<Cross1Icon />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
)
);
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogClose = DialogPrimitive.Close;