import {
Dialog,
DialogTrigger,
DialogHeading,
DialogDescription,
DialogCloseTrigger,
DialogCloseIconTrigger,
} from '@cerberus-design/react'
To use the Dialog component, wrap it in a DialogProvider and use the DialogTrigger component to open the dialog. The Dialog component is a controlled component that can be used to display content in a modal.
The Dialog component supports different sizes. You can set the size prop to xs to lg, or full.
Manage the dialog state using the open and onOpenChange props.
An alternative way to control the dialog is to use the RootProvider component and the useDialog hook. This way you can access the state and methods from outside the component.
Use lazyMount to render dialog content only when first opened. Combine with unmountOnExit to unmount when closed, freeing up resources.
Prefer this over conditionally rendering DialogProvider — see Conditional Rendering.
Make the content area scrollable while keeping header and footer fixed using maxHeight and overflow: auto.
Make the positioner scrollable so the dialog can extend beyond the viewport.
Use initialFocusEl to control which element receives focus when the dialog opens.
Use finalFocusEl to control which element receives focus when the dialog closes. Defaults to the trigger element.
Access the dialog's state and methods with DialogContext or the useDialogContext hook.
Open a dialog imperatively from a Menu item using the onClick handler.
Nest dialogs within one another. The parent receives data-has-nested and --nested-layer-count CSS variable for styling effects like zoom-out:
[data-part='content'][data-has-nested] {
transform: scale(calc(1 - var(--nested-layer-count) * 0.05));
}Intercept close attempts to show confirmation prompts, preventing data loss from unsaved changes.
We don't recommend using a non-modal dialog due to the accessibility concerns they present. In event you need it, here's what you can do:
modal prop to falsepointerEvents to none on the Dialog.Positioner componentcloseOnInteractOutside prop to false
PreviewcloseOnEscape={false} - Prevent closing on EscapecloseOnInteractOutside={false} - Prevent closing on outside clickFor conditional control, use onEscapeKeyDown or onInteractOutside with e.preventDefault().
Unmounting Dialog.Root when toggling open state can break focus, scroll lock, and cleanup. Keep the root mounted and control it with open / onOpenChange.
When you want portal content out of the DOM while closed, add lazyMount and unmountOnExit to the root.
Use the --layer-index CSS variable for z-index management of stacked dialogs:
[data-part='content'] {
z-index: calc(var(--layer-index));
}When using lazyMount with lazy or Next.js dynamic, wrap the imported component in Suspense:
You can customize the Dialog using style props and data selectors on any slot primitve.
You can utilize the primitive components or the css prop to customize the dialog.
| Component | Description |
|---|---|
| DialogProvider | The main state context for the dialog. |
| DialogTrigger | The trigger element that opens the dialog. |
| DialogBackdrop | The backdrop that covers the page when the dialog is open. |
| DialogPositioner | The container that positions the dialog content. |
| DialogContent | The content that is shown within the dialog. |
| DialogHeading | The heading title of the dialog. |
| DialogDescription | The description of the dialog. |
| DialogCloseTrigger | The trigger element that closes the dialog. |
| DialogCloseIconTrigger | The trigger element that closes the dialog with an "x" icon. |
The Dialog component is an abstraction of the primitives and accepts the following props:
| Prop | Type | Required | Description |
|---|---|---|---|
aria-label | string | No | Human readable label for the dialog, in event the dialog title is not rendered |
closeOnEscape | boolean | No | Whether to close the dialog when the escape key is pressed |
closeOnInteractOutside | boolean | No | Whether to close the dialog when the outside is clicked |
defaultOpen | boolean | No | The initial open state of the dialog when rendered. Use when you don't need to control the open state of the dialog. |
finalFocusEl | () => MaybeElement | No | Element to receive focus when the dialog is closed |
id | string | No | The unique identifier of the machine. |
ids | Partial<PrimitiveLayers> | No | The ids of the elements in the dialog. Useful for composition. |
immediate | boolean | No | Whether to synchronize the present change immediately or defer it to the next frame |
initialFocusEl | () => MaybeElement | No | Element to receive focus when the dialog is opened |
lazyMount | boolean | No | Whether to enable lazy mounting |
modal | boolean | No | Whether to prevent pointer interaction outside the element and hide all content below it |
onEscapeKeyDown | (event: KeyboardEvent) => void | No | Function called when the escape key is pressed |
onExitComplete | VoidFunction | No | Function called when the animation ends in the closed state |
onFocusOutside | (event: FocusOutsideEvent) => void | No | Function called when the focus is moved outside the component |
onInteractOutside | (event: InteractOutsideEvent) => void | No | Function called when an interaction happens outside the component |
onOpenChange | (details: OpenChangeDetails) => void | No | Function to call when the dialog's open state changes |
onPointerDownOutside | (event: PointerDownOutsideEvent) => void | No | Function called when the pointer is pressed down outside the component |
onRequestDismiss | (event: LayerDismissEvent) => void | No | Function called when this layer is closed due to a parent layer being closed |
open | boolean | No | The controlled open state of the dialog |
persistentElements | (() => Element | null)[] | No | Returns the persistent elements that: should not have pointer-events disabled or should not trigger the dismiss event |
present | boolean | No | Whether the node is present (controlled by the user) |
preventScroll | boolean | No | Whether to prevent scrolling behind the dialog when it's opened |
restoreFocus | boolean | No | Whether to restore focus to the element that had focus before the dialog was opened |
role | 'dialog' | 'alertdialog' | No | The dialog's role |
skipAnimationOnMount | boolean | No | Whether to allow the initial presence animation. |
size | string | No | This size of the Dialog. |
trapFocus | boolean | No | Whether to trap focus inside the dialog when it's opened |
unmountOnExit | boolean | No | Whether to unmount on exit. |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Attribute | Value |
|---|---|
[data-scope] | dialog |
[data-part] | backdrop |
[data-state] | "open" | "closed" |
| Variable | Description |
|---|---|
--layer-index | The index of the dismissable in the layer stack |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Attribute | Value |
|---|---|
[data-scope] | dialog |
[data-part] | content |
[data-state] | "open" | "closed" |
[data-nested] | dialog |
[data-has-nested] | dialog |
| Variable | Description |
|---|---|
--layer-index | The index of the dismissable in the layer stack |
--nested-layer-count | The number of nested dialogs |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Prop | Type | Required | Description |
|---|---|---|---|
value | UseDialogReturn | Yes | |
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
immediate | boolean | No | Whether to synchronize the present change immediately or defer it to the next frame |
lazyMount | boolean | No | Whether to enable lazy mounting |
onExitComplete | VoidFunction | No | Function called when the animation ends in the closed state |
present | boolean | No | Whether the node is present (controlled by the user) |
skipAnimationOnMount | boolean | No | Whether to allow the initial presence animation. |
unmountOnExit | boolean | No | Whether to unmount on exit. |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Prop | Type | Required | Description |
|---|---|---|---|
asChild | boolean | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| Attribute | Value |
|---|---|
[data-scope] | dialog |
[data-part] | trigger |
[data-value] | The value of the item |
[data-state] | "open" | "closed" |
[data-current] | Present when current |
On this page
'use client'
import { Box, HStack, Stack } from '@/styled-system/jsx'
import {
Button,
Dialog,
DialogCloseIconTrigger,
DialogCloseTrigger,
DialogDescription,
DialogHeading,
DialogProps,
DialogProvider,
DialogTrigger,
} from '@cerberus/react'
export function SizesDemo() {
return (
<HStack gap="md">
<DialogContent size="xs" />
<DialogContent size="sm" />
<DialogContent size="md" />
<DialogContent size="lg" />
<DialogContent size="full" />
<DialogContent size="auto" />
</HStack>
)
}
function DialogContent(props: DialogProps) {
return (
<DialogProvider>
<DialogTrigger asChild>
<Button size="sm">open {String(props.size)}</Button>
</DialogTrigger>
<Dialog size={props.size}>
<DialogCloseIconTrigger />
<Stack gap="xs" w="full">
<DialogHeading>Dialog Title</DialogHeading>
<DialogDescription maxW="prose">
Far far away, behind the word mountains, far from the countries Vokalia and
Consonantia, there live the blind texts. Separated they live in
Bookmarksgrove right at the coast of the Semantics, a large language ocean.
</DialogDescription>
</Stack>
<Box mt="md" w="full">
<DialogCloseTrigger asChild>
<Button>Close</Button>
</DialogCloseTrigger>
</Box>
</Dialog>
</DialogProvider>
)
}
'use client'
import { Box, Stack } from '@/styled-system/jsx'
import {
Button,
Dialog,
DialogCloseIconTrigger,
DialogCloseTrigger,
DialogDescription,
DialogHeading,
DialogProvider,
DialogTrigger,
type OpenChangeDetails,
} from '@cerberus/react'
import { useSignal } from '@cerberus/signals'
export function ControlledDemo() {
const [open, setOpen] = useSignal<boolean>(false)
return (
<DialogProvider
open={open}
onOpenChange={(details: OpenChangeDetails) => setOpen(details.open)}
>
<DialogTrigger asChild>
<Button>open dialog</Button>
</DialogTrigger>
<Dialog size="auto">
<DialogCloseIconTrigger />
<Stack gap="xs" w="full">
<DialogHeading>Dialog Title</DialogHeading>
<DialogDescription maxW="prose">
Far far away, behind the word mountains, far from the countries Vokalia and
Consonantia, there live the blind texts. Separated they live in
Bookmarksgrove right at the coast of the Semantics, a large language ocean.
</DialogDescription>
</Stack>
<Box mt="md" w="full">
<DialogCloseTrigger asChild>
<Button>Close</Button>
</DialogCloseTrigger>
</Box>
</Dialog>
</DialogProvider>
)
}
'use client'
import { Stack } from '@/styled-system/jsx'
import {
Button,
cerberus,
Dialog,
DialogCloseIconTrigger,
DialogDescription,
DialogHeading,
DialogProvider,
DialogTrigger,
Field,
Input,
} from '@cerberus/react'
import { useRef } from 'react'
export function InitialFocusDemo() {
const inputRef = useRef<HTMLInputElement>(null)
return (
<DialogProvider initialFocusEl={() => inputRef.current}>
<DialogTrigger asChild>
<Button>open dialog</Button>
</DialogTrigger>
<Dialog size="auto">
<DialogCloseIconTrigger />
<Stack gap="md" w="full">
<DialogHeading>Edit Profile</DialogHeading>
<DialogDescription maxW="prose">
The first input will be focused when the dialog opens.
</DialogDescription>
<cerberus.form mt="md" w="full">
<Stack gap="md" w="full">
<Field label="Name">
<Input ref={inputRef} placeholder="Enter your name..." />
</Field>
<Field label="Emaiil">
<Input placeholder="Enter your email..." type="email" />
</Field>
</Stack>
</cerberus.form>
</Stack>
</Dialog>
</DialogProvider>
)
}
I will receive focus when dialog closes
'use client'
import { Box, Stack, VStack } from '@/styled-system/jsx'
import {
Button,
Dialog,
DialogCloseIconTrigger,
DialogCloseTrigger,
DialogDescription,
DialogHeading,
DialogProvider,
DialogTrigger,
Text,
} from '@cerberus/react'
import { useRef } from 'react'
export function FinalFocusDemo() {
const finalRef = useRef<HTMLParagraphElement>(null)
return (
<VStack gap="md" w="full">
<Text
ref={finalRef}
display="inline-block"
_focusVisible={{
boxShadow: 'none',
outline: '3px solid',
outlineColor: 'action.border.focus',
outlineOffset: '2px',
}}
>
I will receive focus when dialog closes
</Text>
<DialogProvider finalFocusEl={() => finalRef.current}>
<DialogTrigger asChild>
<Button>open dialog</Button>
</DialogTrigger>
<Dialog size="auto">
<DialogCloseIconTrigger />
<Stack gap="xs" w="full">
<DialogHeading>Dialog Title</DialogHeading>
<DialogDescription maxW="prose">
Far far away, behind the word mountains, far from the countries Vokalia
and Consonantia, there live the blind texts. Separated they live in
Bookmarksgrove right at the coast of the Semantics, a large language
ocean.
</DialogDescription>
</Stack>
<Box mt="md" w="full">
<DialogCloseTrigger asChild>
<Button>Close</Button>
</DialogCloseTrigger>
</Box>
</Dialog>
</DialogProvider>
</VStack>
)
}
Dialog is closed
'use client'
import { Stack } from '@/styled-system/jsx'
import {
Button,
cerberus,
Dialog,
DialogCloseIconTrigger,
DialogContext,
DialogDescription,
DialogHeading,
DialogProvider,
DialogTrigger,
} from '@cerberus/react'
export function ContextDemo() {
return (
<DialogProvider>
<DialogTrigger asChild>
<Button>open dialog</Button>
</DialogTrigger>
<Dialog size="auto">
<DialogCloseIconTrigger />
<Stack gap="xs" w="20rem">
<DialogHeading>Status</DialogHeading>
<DialogDescription maxW="prose">
<DialogContext>
{(dialog) => (
<cerberus.p>Dialog is {dialog.open ? 'open' : 'closed'}</cerberus.p>
)}
</DialogContext>
</DialogDescription>
</Stack>
</Dialog>
</DialogProvider>
)
}
'use client'
import { Box, Stack } from '@/styled-system/jsx'
import { ChevronDown } from '@carbon/icons-react'
import {
Button,
Dialog,
DialogCloseIconTrigger,
DialogCloseTrigger,
DialogDescription,
DialogHeading,
DialogProps,
DialogProvider,
Menu,
MenuContent,
MenuItem,
MenuSeparator,
MenuTrigger,
OpenChangeDetails,
} from '@cerberus/react'
import { useSignal } from '@cerberus/signals'
export function MenuDemo() {
const [open, setOpen] = useSignal<boolean>(false)
return (
<>
<Menu>
<MenuTrigger>
<Button>
Actions
<ChevronDown />
</Button>
</MenuTrigger>
<MenuContent>
<MenuItem value="edit">Edit</MenuItem>
<MenuItem value="duplicate">Duplicate</MenuItem>
<MenuSeparator />
<MenuItem value="delete" onClick={() => setOpen(true)}>
Delete
</MenuItem>
</MenuContent>
</Menu>
<MyDialog
open={open}
onOpenChange={(details: OpenChangeDetails) => setOpen(details.open)}
/>
</>
)
}
function MyDialog(props: DialogProps) {
return (
<DialogProvider {...props}>
<Dialog size="auto">
<DialogCloseIconTrigger />
<Stack gap="xs" w="full">
<DialogHeading>Dialog Title</DialogHeading>
<DialogDescription maxW="prose">
Far far away, behind the word mountains, far from the countries Vokalia and
Consonantia, there live the blind texts. Separated they live in
Bookmarksgrove right at the coast of the Semantics, a large language ocean.
</DialogDescription>
</Stack>
<Box mt="md" w="full">
<DialogCloseTrigger asChild>
<Button>Close</Button>
</DialogCloseTrigger>
</Box>
</Dialog>
</DialogProvider>
)
}
'use client'
import { Box, Stack } from '@/styled-system/jsx'
import {
Button,
Dialog,
DialogCloseIconTrigger,
DialogDescription,
DialogHeading,
DialogRootProvider,
DialogRootProviderProps,
useDialog,
} from '@cerberus/react'
export function NestedDialog() {
const parentDialog = useDialog()
const childDialog = useDialog()
return (
<>
<Button onClick={() => parentDialog.setOpen(true)}>Trigger</Button>
<ParentDialog value={parentDialog} onClick={() => childDialog.setOpen(true)} />
<ChildDialog value={childDialog} />
</>
)
}
interface ParentDialogProps extends DialogRootProviderProps {
onClick?: () => void
}
function ParentDialog(props: ParentDialogProps) {
return (
<DialogRootProvider {...props}>
<Dialog size="auto">
<DialogCloseIconTrigger />
<Stack gap="xs" w="full">
<DialogHeading>Parent Dialog</DialogHeading>
<DialogDescription maxW="prose">
This is the parent dialog. Open a nested dialog to see automatic z-index
management.
</DialogDescription>
</Stack>
<Box mt="md" w="full">
<Button onClick={props.onClick}>Open nested dialog</Button>
</Box>
</Dialog>
</DialogRootProvider>
)
}
function ChildDialog(props: DialogRootProviderProps) {
return (
<DialogRootProvider {...props}>
<Dialog size="auto">
<DialogCloseIconTrigger />
<Stack gap="xs" w="full">
<DialogHeading>Nested Dialog</DialogHeading>
<DialogDescription maxW="prose">
This dialog is nested within the parent with proper z-index layering.
</DialogDescription>
</Stack>
</Dialog>
</DialogRootProvider>
)
}
Edit Content
Unsaved Changes
'use client'
import { Box, Stack } from '@/styled-system/jsx'
import {
Button,
DialogCloseIconTrigger,
DialogCloseTrigger,
DialogContent,
DialogDescription,
DialogHeading,
DialogPositioner,
DialogProvider,
DialogTrigger,
Portal,
} from '@cerberus/react'
export function NonModalDemo() {
return (
<DialogProvider closeOnInteractOutside={false} modal={false}>
<DialogTrigger asChild>
<Button>open dialog</Button>
</DialogTrigger>
<Portal>
<DialogPositioner pointerEvents="none">
<DialogContent size="auto">
<DialogCloseIconTrigger />
<Stack gap="xs" w="full">
<DialogHeading>Dialog Title</DialogHeading>
<DialogDescription maxW="prose">
Far far away, behind the word mountains, far from the countries Vokalia
and Consonantia, there live the blind texts. Separated they live in
Bookmarksgrove right at the coast of the Semantics, a large language
ocean.
</DialogDescription>
</Stack>
<Box mt="md" w="full">
<DialogCloseTrigger asChild>
<Button>Close</Button>
</DialogCloseTrigger>
</Box>
</DialogContent>
</DialogPositioner>
</Portal>
</DialogProvider>
)
}
'use client'
import { Box, Stack } from '@/styled-system/jsx'
import {
Button,
Dialog,
DialogCloseIconTrigger,
DialogCloseTrigger,
DialogDescription,
DialogHeading,
DialogProvider,
DialogTrigger,
} from '@cerberus/react'
export function BasicDemo() {
return (
<DialogProvider>
<DialogTrigger asChild>
<Button>open dialog</Button>
</DialogTrigger>
<Dialog size="auto">
<DialogCloseIconTrigger />
<Stack gap="xs" w="full">
<DialogHeading>Dialog Title</DialogHeading>
<DialogDescription maxW="prose">
Far far away, behind the word mountains, far from the countries Vokalia and
Consonantia, there live the blind texts. Separated they live in
Bookmarksgrove right at the coast of the Semantics, a large language ocean.
</DialogDescription>
</Stack>
<Box mt="md" w="full">
<DialogCloseTrigger asChild>
<Button>Close</Button>
</DialogCloseTrigger>
</Box>
</Dialog>
</DialogProvider>
)
}
'use client'
import { Stack } from '@/styled-system/jsx'
import {
Button,
Dialog,
DialogCloseIconTrigger,
DialogDescription,
DialogHeading,
DialogProvider,
DialogTrigger,
Text,
} from '@cerberus/react'
export function LazyDemo() {
return (
<DialogProvider lazyMount unmountOnExit>
<DialogTrigger asChild>
<Button>open dialog</Button>
</DialogTrigger>
<Dialog size="auto">
<DialogCloseIconTrigger />
<Stack gap="md" w="full">
<DialogHeading asChild>
<Text textStyle="heading-sm">Lazy Loaded</Text>
</DialogHeading>
<DialogDescription maxW="prose">
This dialog content is only mounted when opened and unmounts on close.
</DialogDescription>
</Stack>
</Dialog>
</DialogProvider>
)
}
Controlled Externally
'use client'
import { Stack } from '@/styled-system/jsx'
import {
Button,
Dialog,
DialogCloseIconTrigger,
DialogDescription,
DialogHeading,
DialogRootProvider,
Text,
useDialog,
} from '@cerberus/react'
export function RootProviderDemo() {
const dialog = useDialog()
return (
<>
<Button onClick={() => dialog.setOpen(true)}>
Dialog is {dialog.open ? 'open' : 'closed'}
</Button>
<DialogRootProvider value={dialog}>
<Dialog size="auto">
<DialogCloseIconTrigger />
<Stack gap="md" w="full">
<DialogHeading asChild>
<Text textStyle="heading-sm">Controlled Externally</Text>
</DialogHeading>
<DialogDescription maxW="prose">
This dialog is controlled via the useDialog hook.
</DialogDescription>
</Stack>
</Dialog>
</DialogRootProvider>
</>
)
}
import { DialogRoot, type OpenChangeDetails, Portal } from '@cerberus/react'
import { useSignal } from '@cerberus/signals'
// ❌ Avoid
export function Dont() {
const [isOpen, setOpen] = useSignal<boolean>(false)
if (isOpen) {
return (
<DialogRoot
open={isOpen}
onOpenChange={(e: OpenChangeDetails) => setOpen(e.open)}
>
<Portal>...</Portal>
</DialogRoot>
)
}
return null
}
// ✅ Prefer
export function Do() {
const [isOpen, setOpen] = useSignal<boolean>(false)
return (
<DialogRoot
open={isOpen}
onOpenChange={(e: OpenChangeDetails) => setOpen(e.open)}
lazyMount
unmountOnExit
>
<Portal>...</Portal>
</DialogRoot>
)
}
import { Dialog, DialogProvider, DialogTrigger } from '@cerberus/react'
import dynamic from 'next/dynamic'
import { Suspense } from 'react'
const HeavyComponent = dynamic(() => import('./heavy'))
export function DynamicDemo() {
return (
<DialogProvider lazyMount>
<DialogTrigger>Open</DialogTrigger>
<Dialog>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
</Dialog>
</DialogProvider>
)
}
'use client'
import { Box, Stack } from '@/styled-system/jsx'
import {
Button,
cerberus,
DialogBackdrop,
DialogCloseIconTrigger,
DialogCloseTrigger,
DialogContent,
DialogHeading,
DialogPositioner,
DialogProvider,
DialogTrigger,
For,
Portal,
} from '@cerberus/react'
import { useRef } from 'react'
export function OutsideScrollDemo() {
const contentRef = useRef<HTMLDivElement | null>(null)
return (
<DialogProvider initialFocusEl={() => contentRef.current}>
<DialogTrigger asChild>
<Button>open dialog</Button>
</DialogTrigger>
<Portal>
<DialogBackdrop />
<DialogPositioner
display="flex"
alignItems="flex-start"
justifyContent="center"
position="fixed"
inset="0"
overflowY="auto"
overscrollBehaviorY="contain"
pointerEvents="auto"
>
<DialogContent
ref={contentRef}
margin="4rem auto"
maxH="none"
css={{
'--dialog-content-w': '20rem',
}}
>
<DialogCloseIconTrigger />
<Stack gap="md" maxW="" w="full">
<DialogHeading>Dialog Title</DialogHeading>
<Stack gap="md" w="full">
<For each={CONTENT_SECTIONS}>
{(section) => (
<cerberus.section key={section.title} w="full">
<cerberus.h3 textStyle="heading-2xs">{section.title}</cerberus.h3>
<cerberus.small color="page.text.100" textStyle="body-sm">
{section.body}
</cerberus.small>
</cerberus.section>
)}
</For>
</Stack>
</Stack>
<Box mt="md" w="full">
<DialogCloseTrigger asChild>
<Button>Close</Button>
</DialogCloseTrigger>
</Box>
</DialogContent>
</DialogPositioner>
</Portal>
</DialogProvider>
)
}
const CONTENT_SECTIONS = [
{
title: '1. Acceptance of Terms',
body: 'By accessing and using this service, you accept and agree to be bound by the terms and provisions of this agreement.',
},
{
title: '2. Use License',
body: 'Permission is granted to temporarily use this service for personal, non-commercial purposes only. This is the grant of a license, not a transfer of title.',
},
{
title: '3. User Responsibilities',
body: 'You are responsible for maintaining the confidentiality of your account and password. You agree to accept responsibility for all activities that occur under your account.',
},
{
title: '4. Privacy Policy',
body: 'Your use of this service is also governed by our Privacy Policy. Please review our Privacy Policy, which also governs the site and informs users of our data collection practices.',
},
{
title: '5. Limitations',
body: 'In no event shall we be liable for any damages arising out of the use or inability to use the materials on this service.',
},
{
title: '6. Revisions',
body: 'We may revise these terms of service at any time without notice. By using this service you are agreeing to be bound by the then current version of these terms.',
},
{
title: '7. Governing Law',
body: 'These terms and conditions are governed by and construed in accordance with applicable laws and you irrevocably submit to the exclusive jurisdiction of the courts.',
},
]
'use client'
import { Stack } from '@/styled-system/jsx'
import {
Button,
ButtonGroup,
Dialog,
DialogCloseIconTrigger,
DialogDescription,
DialogHeading,
DialogRootProvider,
DialogRootProviderProps,
Field,
Text,
Textarea,
useDialog,
} from '@cerberus/react'
import { useSignal } from '@cerberus/signals'
import { ChangeEventHandler } from 'react'
export function ConfirmationDialog() {
const [formContent, setFormContent] = useSignal<string>('')
const [isParentDialogOpen, setIsParentDialogOpen] = useSignal<boolean>(false)
const parentDialog = useDialog({
open: isParentDialogOpen,
onOpenChange: (details) => {
if (!details.open && formContent.trim()) {
confirmDialog.setOpen(true)
} else {
setIsParentDialogOpen(details.open)
}
},
})
const confirmDialog = useDialog()
const handleConfirmClose = () => {
confirmDialog.setOpen(false)
parentDialog.setOpen(false)
setFormContent('')
}
const handleCancel = () => {
confirmDialog.setOpen(false)
}
return (
<>
<Button onClick={() => parentDialog.setOpen(true)}>Trigger</Button>
<ParentDialog
onChange={(e) => setFormContent(e.target.value)}
formValue={formContent}
value={parentDialog}
/>
<ConfirmDialog
value={confirmDialog}
onCancel={handleCancel}
onClose={handleConfirmClose}
/>
</>
)
}
interface ParentDialogProps extends DialogRootProviderProps {
onChange?: ChangeEventHandler<HTMLTextAreaElement>
formValue: string
}
function ParentDialog(props: ParentDialogProps) {
return (
<DialogRootProvider {...props}>
<Dialog>
<DialogCloseIconTrigger />
<Stack gap="md" w="full">
<DialogHeading asChild>
<Text textStyle="heading-sm">Edit Content</Text>
</DialogHeading>
<DialogDescription maxW="prose">
Make changes to your content. You'll be asked to confirm before closing if
there are unsaved changes.
</DialogDescription>
<Field>
<Textarea
value={props.formValue}
onChange={props.onChange}
placeholder="Enter some text..."
rows={4}
/>
</Field>
</Stack>
</Dialog>
</DialogRootProvider>
)
}
interface ConfirmationDialogProps extends DialogRootProviderProps {
onCancel?: () => void
onClose?: () => void
}
function ConfirmDialog(props: ConfirmationDialogProps) {
return (
<DialogRootProvider {...props}>
<Dialog size="auto">
<DialogCloseIconTrigger />
<Stack gap="md" w="full">
<DialogHeading asChild>
<Text textStyle="heading-sm">Unsaved Changes</Text>
</DialogHeading>
<DialogDescription maxW="prose">
You have unsaved changes. Are you sure you want to close without saving?
</DialogDescription>
<ButtonGroup>
<Button onClick={props.onCancel} usage="outlined-subtle">
Keep Editing
</Button>
<Button palette="danger" onClick={props.onClose}>
Discard Changes
</Button>
</ButtonGroup>
</Stack>
</Dialog>
</DialogRootProvider>
)
}
'use client'
import { Box, Scrollable, Stack } from '@/styled-system/jsx'
import {
Button,
cerberus,
Dialog,
DialogCloseIconTrigger,
DialogCloseTrigger,
DialogHeading,
DialogProvider,
DialogTrigger,
For,
} from '@cerberus/react'
export function InsideScrollDemo() {
return (
<DialogProvider>
<DialogTrigger asChild>
<Button>open dialog</Button>
</DialogTrigger>
<Dialog size="sm">
<DialogCloseIconTrigger />
<Stack gap="md" w="full">
<DialogHeading>Dialog Title</DialogHeading>
<Scrollable w="full" maxH="min(32rem, calc(100vh - 4rem))">
<Stack gap="md" w="full">
<For each={CONTENT_SECTIONS}>
{(section) => (
<cerberus.section key={section.title} w="full">
<cerberus.h3 textStyle="heading-2xs">{section.title}</cerberus.h3>
<cerberus.small color="page.text.100" textStyle="body-sm">
{section.body}
</cerberus.small>
</cerberus.section>
)}
</For>
</Stack>
</Scrollable>
</Stack>
<Box mt="md" w="full">
<DialogCloseTrigger asChild>
<Button>Close</Button>
</DialogCloseTrigger>
</Box>
</Dialog>
</DialogProvider>
)
}
const CONTENT_SECTIONS = [
{
title: '1. Acceptance of Terms',
body: 'By accessing and using this service, you accept and agree to be bound by the terms and provisions of this agreement.',
},
{
title: '2. Use License',
body: 'Permission is granted to temporarily use this service for personal, non-commercial purposes only. This is the grant of a license, not a transfer of title.',
},
{
title: '3. User Responsibilities',
body: 'You are responsible for maintaining the confidentiality of your account and password. You agree to accept responsibility for all activities that occur under your account.',
},
{
title: '4. Privacy Policy',
body: 'Your use of this service is also governed by our Privacy Policy. Please review our Privacy Policy, which also governs the site and informs users of our data collection practices.',
},
{
title: '5. Limitations',
body: 'In no event shall we be liable for any damages arising out of the use or inability to use the materials on this service.',
},
{
title: '6. Revisions',
body: 'We may revise these terms of service at any time without notice. By using this service you are agreeing to be bound by the then current version of these terms.',
},
{
title: '7. Governing Law',
body: 'These terms and conditions are governed by and construed in accordance with applicable laws and you irrevocably submit to the exclusive jurisdiction of the courts.',
},
]