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) forwards`,
},
});
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) forwards`,
},
'&:focus': { outline: 'none' },
});
function Content({ children, ...props }) {
return (
<DialogPrimitive.Portal>
<StyledOverlay />
<StyledContent {...props}>{children}</StyledContent>
</DialogPrimitive.Portal>
);
}
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
export const Dialog = DialogPrimitive.Root;
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogContent = Content;
export const DialogTitle = StyledTitle;
export const DialogDescription = StyledDescription;
export 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;
Title
and Description
components.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.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
Contains all the parts of a dialog.
Prop | Type | Default |
---|---|---|
defaultOpen | boolean | |
open | boolean | |
onOpenChange | function | |
modal | boolean | true |
allowPinchZoom | boolean | false |
id | string |
The button that opens the dialog.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
When used, portals your overlay and content parts into the body
.
Prop | Type | Default |
---|---|---|
forceMount | boolean | |
container | HTMLElement | document.body |
A layer that covers the inert portion of the view when the dialog is open.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
forceMount | boolean |
Contains content to be rendered in the open dialog.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
forceMount | boolean | |
onOpenAutoFocus | function | |
onCloseAutoFocus | function | |
onEscapeKeyDown | function | |
onPointerDownOutside | function | |
onInteractOutside | function |
The button that closes the dialog.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
An accessible title to be announced when the dialog is opened.
If you want to hide the title, wrap it inside our Visually Hidden utility like this <VisuallyHidden asChild>
.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
An optional accessible description to be announced when the dialog is opened.
If you want to hide the description, wrap it inside our Visually Hidden utility like this <VisuallyHidden asChild>
. If you want to remove the description entirely, remove this part and pass aria-describedby={undefined}
to Dialog.Content
.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
Move the content inside the overlay to render a dialog with overflow.
import { styled } from '@stitches/react';
import * as Dialog from '@radix-ui/react-alert-dialog';
const Overlay = styled(Dialog.Overlay, {
background: 'rgba(0 0 0 / 0.5)',
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'grid',
placeItems: 'center',
overflowY: 'auto',
});
const Content = styled(Dialog.Content, {
minWidth: 300,
background: 'white',
padding: 30,
borderRadius: 4,
});
export default () => {
return (
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Portal>
<Overlay>
<Content>...</Content>
</Overlay>
</Dialog.Portal>
</Dialog.Root>
);
};
Customise the element that your dialog portals into.
export default () => {
const [container, setContainer] = React.useState(null);
return (
<div>
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Portal container={container}>
<Dialog.Overlay />
<Dialog.Content>...</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<div ref={setContainer} />
</div>
);
};
Adheres to the Dialog WAI-ARIA design pattern.
Key | Description |
---|---|
Space | Opens/closes the dialog. |
Enter | Opens/closes the dialog. |
Tab | Moves focus to the next focusable element. |
Shift + Tab | Moves focus to the previous focusable element. |
Esc | Closes 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.
import { Dialog, DialogTrigger, DialogContent } from './your-dialog';
export default () => (
<Dialog>
<DialogTrigger>Dialog trigger</DialogTrigger>
<DialogContent>Dialog Content</DialogContent>
</Dialog>
);
// your-dialog.js
import React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Cross1Icon } from '@radix-ui/react-icons';
export const DialogContent = React.forwardRef(
({ children, ...props }, forwardedRef) => (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay />
<DialogPrimitive.Content {...props} ref={forwardedRef}>
{children}
<DialogPrimitive.Close>
<Cross1Icon />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
)
);
export const Dialog = DialogPrimitive.Root;
export const DialogTrigger = DialogPrimitive.Trigger;