Dialogs
Dialogs provide important prompts in a user flow
Usage
Cards are used to display content and actions on a single topic.
Instalation
Run the following command if is not already installed:
npm i @radix-ui/react-dialog
Copy and paste the following code into your project.
'use client'
import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const dialogVariants = cva(
[
'fixed z-50 w-full gap-6 bg-surfaceContainerHigh text-onSurface outline-none',
'data-[state=open]:duration-200 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
'data-[state=closed]:duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
],
{
variants: {
variant: {
default:
'grid left-[50%] top-[50%] max-w-[90%] translate-x-[-50%] translate-y-[-50%] rounded-lg p-6 shadow-lg sm:max-w-[560px] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]',
full: 'inset-0 block',
responsive:
'inset-0 block sm:inset-auto sm:grid sm:left-[50%] sm:top-[50%] sm:max-w-[90%] sm:translate-x-[-50%] sm:translate-y-[-50%] sm:rounded-lg sm:p-6 sm:shadow-lg sm:data-[state=open]:slide-in-from-left-1/2 sm:data-[state=open]:slide-in-from-top-[48%] sm:data-[state=closed]:slide-out-to-left-1/2 sm:data-[state=closed]:slide-out-to-top-[48%]',
},
},
defaultVariants: {
variant: 'default',
},
}
)
const DialogRoot = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogCloseButton = DialogPrimitive.Close
const DialogPortal = ({ ...props }: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal {...props} />
)
DialogPortal.displayName = DialogPrimitive.Portal.displayName
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-scrim/38 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &
VariantProps<typeof dialogVariants>
>(({ className, children, variant, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(dialogVariants({ variant, className }))}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-4', className)} {...props} />
)
DialogHeader.displayName = 'DialogHeader'
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-row justify-end space-x-2', className)}
{...props}
/>
)
DialogFooter.displayName = 'DialogFooter'
const DialogHeadline = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-headline-sm text-onSurface', className)}
{...props}
/>
))
DialogHeadline.displayName = 'DialogHeadline'
const DialogSupportingText = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-body-md text-onSurfaceVariant', className)}
{...props}
/>
))
DialogSupportingText.displayName = 'DialogSupportingText'
const Dialog = Object.assign(DialogRoot, {
Portal: DialogPortal,
Overlay: DialogOverlay,
Content: DialogContent,
Headline: DialogHeadline,
SupportingText: DialogSupportingText,
Header: DialogHeader,
Footer: DialogFooter,
Trigger: DialogTrigger,
Close: DialogCloseButton,
})
export { Dialog }
Update the import paths to match your project setup.
Examples
Basic dialog
Basic dialogs interrupt users with urgent information, details, or actions. Common use cases for basic dialogs include alerts, quick selection, and confirmation.
import { Button } from '@/components/ui/button'
import { Dialog } from '@/components/ui/dialog'
export const BasicDialog = () => {
return (
<Dialog>
<Dialog.Trigger asChild>
<Button>Reset settings</Button>
</Dialog.Trigger>
<Dialog.Content className="md:max-w-[425px]">
<Dialog.Header>
<Dialog.Headline>Reset settings?</Dialog.Headline>
<Dialog.SupportingText>
This will reset your app preferences back to their default settings.
</Dialog.SupportingText>
</Dialog.Header>
<Dialog.Footer>
<Dialog.Close asChild>
<Button variant="text">Cancel</Button>
</Dialog.Close>
<Button variant="text">Accept</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
)
}
Dialog with icon
import { Avatar } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Dialog } from '@/components/ui/dialog'
import { Divider } from '@/components/ui/divider'
import { Icon } from '@/components/ui/icon'
const Account = (id: number, email: string, avatar: string) => ({
id,
email,
avatar,
})
const accounts = [
Account(
1,
'eevillanuevanotes@gmail.com',
'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=2980&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'
),
Account(
2,
'alloitsalejandro@gmail.com',
'https://images.unsplash.com/photo-1610186594416-2c7c0131e35d?q=80&w=3087&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'
),
Account(
3,
'oliortega3000@gmail.com',
'https://images.unsplash.com/photo-1599566150163-29194dcaad36?q=80&w=3087&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'
),
]
export const DialogWithIcon = () => {
return (
<Dialog>
<Dialog.Trigger asChild>
<Button>Reset settings</Button>
</Dialog.Trigger>
<Dialog.Content className="md:max-w-[324px]">
<Dialog.Header>
<span className="mx-auto">
<Icon symbol="restart_alt" />
</span>
<Dialog.Headline className="md:text-center">
Reset settings?
</Dialog.Headline>
<Dialog.SupportingText>
This will reset your app preferences back to their default settings.
The following accounts will also be signed out:
</Dialog.SupportingText>
</Dialog.Header>
<Divider />
<section className="space-y-4">
{accounts.map(({ id, email, avatar }) => (
<div key={id} className="text-body-md flex items-center gap-4">
<Avatar>
<Avatar.Image src={avatar} />
</Avatar>
<p>{email}</p>
</div>
))}
</section>
<Divider />
<Dialog.Footer>
<Dialog.Close asChild>
<Button variant="text">Cancel</Button>
</Dialog.Close>
<Button variant="text">Accept</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
)
}
Full screen dialog
Full-screen dialogs fill the entire screen, containing actions that require a series of tasks to complete.
import { Avatar } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Dialog } from '@/components/ui/dialog'
import { Icon } from '@/components/ui/icon'
import { IconButton } from '@/components/ui/icon-button'
import { OutlinedTextField } from '@/components/ui/text-field'
import { TopAppBar } from '@/components/ui/top-app-bar'
export const FullScreenDialog = () => {
return (
<Dialog>
<Dialog.Trigger asChild>
<Button>New Album</Button>
</Dialog.Trigger>
<Dialog.Content variant="full">
<Dialog.Header>
<TopAppBar className="relative">
<TopAppBar.Toolbar>
<TopAppBar.LeadingSection>
<Dialog.Close asChild>
<IconButton variant="standard">
<Icon symbol="close" />
</IconButton>
</Dialog.Close>
</TopAppBar.LeadingSection>
<TopAppBar.Headline>Create new album</TopAppBar.Headline>
<TopAppBar.TrailingSection>
<Button variant="text">Save</Button>
</TopAppBar.TrailingSection>
</TopAppBar.Toolbar>
</TopAppBar>
</Dialog.Header>
<div className="space-y-10 p-6">
<section className="grid gap-4">
<OutlinedTextField>
<OutlinedTextField.Input />
<OutlinedTextField.Label>Album title</OutlinedTextField.Label>
</OutlinedTextField>
<OutlinedTextField>
<OutlinedTextField.Input />
<OutlinedTextField.Label>Description</OutlinedTextField.Label>
</OutlinedTextField>
</section>
<section className="space-y-3">
<p className="text-label-lg">Shared with</p>
<div className="grid gap-4">
{accounts.map(({ id, email, avatar }) => (
<div key={id} className="text-body-md flex items-center gap-4">
<Avatar>
<Avatar.Image src={avatar} />
</Avatar>
<p>{email}</p>
</div>
))}
</div>
</section>
</div>
</Dialog.Content>
</Dialog>
)
}
Responsive dialog
When the variant:"responsive"
attribute is applied to the Dialog Content, it ensures that the dialog is displayed in full screen on mobile devices, while appearing as a basic dialog on larger breakpoints. Additionally, this requires the creation of two Dialog Headers, one for each display scenario.
import { Avatar } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Dialog } from '@/components/ui/dialog'
import { Icon } from '@/components/ui/icon'
import { IconButton } from '@/components/ui/icon-button'
import { OutlinedTextField } from '@/components/ui/text-field'
import { TopAppBar } from '@/components/ui/top-app-bar'
export const ResponsiveDialog = () => {
return (
<Dialog>
<Dialog.Trigger asChild>
<Button>New Album</Button>
</Dialog.Trigger>
<Dialog.Content variant="responsive">
<Dialog.Header className="relative sm:hidden">
<TopAppBar className="sticky top-0">
<TopAppBar.Toolbar>
<TopAppBar.LeadingSection>
<Dialog.Close asChild>
<IconButton variant="standard">
<Icon symbol="close" />
</IconButton>
</Dialog.Close>
</TopAppBar.LeadingSection>
<TopAppBar.Headline>Create a new album</TopAppBar.Headline>
<TopAppBar.TrailingSection>
<Button variant="text">Save</Button>
</TopAppBar.TrailingSection>
</TopAppBar.Toolbar>
</TopAppBar>
</Dialog.Header>
<Dialog.Header className="hidden sm:flex">
<Dialog.Headline>Create a new album</Dialog.Headline>
</Dialog.Header>
<div className="space-y-10 p-6 sm:p-0">
<section className="grid gap-4">
<OutlinedTextField>
<OutlinedTextField.Input />
<OutlinedTextField.Label>Album title</OutlinedTextField.Label>
</OutlinedTextField>
<OutlinedTextField>
<OutlinedTextField.Input />
<OutlinedTextField.Label>Description</OutlinedTextField.Label>
</OutlinedTextField>
</section>
<section className="space-y-3">
<p className="text-label-lg">Shared with</p>
<div className="grid gap-4">
{accounts.map(({ id, email, avatar }) => (
<div key={id} className="text-body-md flex items-center gap-4">
<Avatar>
<Avatar.Image src={avatar} />
</Avatar>
<p>{email}</p>
</div>
))}
</div>
</section>
</div>
<Dialog.Footer className="hidden sm:flex">
<Dialog.Close asChild>
<Button variant="text">Cancel</Button>
</Dialog.Close>
<Button variant="text">Save</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
)
}