DocsBlog
  • 1.2.1

  • light

    dark

    system

    Switch mode
  • Cerberus

    Acheron

    Elysium

Get Started
Components
Data Grid
Signals
Styling
Theming

Concepts

OverviewCompositionCerberus ContextTesting

Layout

Aspect RatioBleedBoxCenterContainerDividerFlexFloatGridGroupLink OverlayScrollableStackWrap

Components

AccordionAdmonitionAvatarButtonCarouselCheckboxClipboardCollapsibleComboboxConfirm ModalCTAModalDate PickerDialogFieldFieldsetFile UploaderIcon ButtonInputLoading StatesMenuNotificationsNumber InputPaginationPin InputPopoverNewProgressPrompt ModalRadioRatingSelectSplit ButtonSwitchTableTabsTagTextTextareaToggleTooltip

Utilities

Client OnlyDownload TriggerEnvironmentFeature FlagsFocus TrapForFormat ByteFormat NumberFormat Relative TimeFrameHighlightJSON Tree ViewLocaleLocal StoragePortalPresenceShowsplitPropsTheme

Dialog

A modal window that appears on top of the main content.

  • npm
  • source
  • recipe
  • Ark
import {
  Dialog,
  DialogTrigger,
  DialogHeading,
  DialogDescription,
  DialogCloseTrigger,
  DialogCloseIconTrigger,
} from '@cerberus-design/react'

Usage

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.

Sizes

The Dialog component supports different sizes. You can set the size prop to xs to lg, or full.

Controlled

Manage the dialog state using the open and onOpenChange props.

Note

Notice how the dialog triggers automagically handle the state management via onOpenChange.

Root Provider

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.

Lazy Mount

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.

Inside Scroll

Make the content area scrollable while keeping header and footer fixed using maxHeight and overflow: auto.

Outside Scroll

Make the positioner scrollable so the dialog can extend beyond the viewport.

Initial Focus

Use initialFocusEl to control which element receives focus when the dialog opens.

Final Focus

Use finalFocusEl to control which element receives focus when the dialog closes. Defaults to the trigger element.

Context

Access the dialog's state and methods with DialogContext or the useDialogContext hook.

Open from Menu

Open a dialog imperatively from a Menu item using the onClick handler.

Nested

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));
}

Confirmation

Intercept close attempts to show confirmation prompts, preventing data loss from unsaved changes.

Non-Modal

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:

  • set the modal prop to false
  • set pointerEvents to none on the Dialog.Positioner component
  • (optional) set the closeOnInteractOutside prop to false Preview

Guides

Close Behavior

  • closeOnEscape={false} - Prevent closing on Escape
  • closeOnInteractOutside={false} - Prevent closing on outside click

For conditional control, use onEscapeKeyDown or onInteractOutside with e.preventDefault().

Conditional Rendering

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.

Z-index Stacking

Use the --layer-index CSS variable for z-index management of stacked dialogs:

[data-part='content'] {
  z-index: calc(var(--layer-index));
}

Dynamic Imports

When using lazyMount with lazy or Next.js dynamic, wrap the imported component in Suspense:

Customizing

You can customize the Dialog using style props and data selectors on any slot primitve.

Primitives

You can utilize the primitive components or the css prop to customize the dialog.

ComponentDescription
DialogProviderThe main state context for the dialog.
DialogTriggerThe trigger element that opens the dialog.
DialogBackdropThe backdrop that covers the page when the dialog is open.
DialogPositionerThe container that positions the dialog content.
DialogContentThe content that is shown within the dialog.
DialogHeadingThe heading title of the dialog.
DialogDescriptionThe description of the dialog.
DialogCloseTriggerThe trigger element that closes the dialog.
DialogCloseIconTriggerThe trigger element that closes the dialog with an "x" icon.

API

Props

The Dialog component is an abstraction of the primitives and accepts the following props:

Root Props:

PropTypeRequiredDescription
aria-labelstringNoHuman readable label for the dialog, in event the dialog title is not rendered
closeOnEscapebooleanNoWhether to close the dialog when the escape key is pressed
closeOnInteractOutsidebooleanNoWhether to close the dialog when the outside is clicked
defaultOpenbooleanNoThe initial open state of the dialog when rendered. Use when you don't need to control the open state of the dialog.
finalFocusEl() => MaybeElementNoElement to receive focus when the dialog is closed
idstringNoThe unique identifier of the machine.
idsPartial<PrimitiveLayers>NoThe ids of the elements in the dialog. Useful for composition.
immediatebooleanNoWhether to synchronize the present change immediately or defer it to the next frame
initialFocusEl() => MaybeElementNoElement to receive focus when the dialog is opened
lazyMountbooleanNoWhether to enable lazy mounting
modalbooleanNoWhether to prevent pointer interaction outside the element and hide all content below it
onEscapeKeyDown(event: KeyboardEvent) => voidNoFunction called when the escape key is pressed
onExitCompleteVoidFunctionNoFunction called when the animation ends in the closed state
onFocusOutside(event: FocusOutsideEvent) => voidNoFunction called when the focus is moved outside the component
onInteractOutside(event: InteractOutsideEvent) => voidNoFunction called when an interaction happens outside the component
onOpenChange(details: OpenChangeDetails) => voidNoFunction to call when the dialog's open state changes
onPointerDownOutside(event: PointerDownOutsideEvent) => voidNoFunction called when the pointer is pressed down outside the component
onRequestDismiss(event: LayerDismissEvent) => voidNoFunction called when this layer is closed due to a parent layer being closed
openbooleanNoThe controlled open state of the dialog
persistentElements(() => Element | null)[]NoReturns the persistent elements that: should not have pointer-events disabled or should not trigger the dismiss event
presentbooleanNoWhether the node is present (controlled by the user)
preventScrollbooleanNoWhether to prevent scrolling behind the dialog when it's opened
restoreFocusbooleanNoWhether to restore focus to the element that had focus before the dialog was opened
role'dialog' | 'alertdialog'NoThe dialog's role
skipAnimationOnMountbooleanNoWhether to allow the initial presence animation.
sizestringNoThis size of the Dialog.
trapFocusbooleanNoWhether to trap focus inside the dialog when it's opened
unmountOnExitbooleanNoWhether to unmount on exit.

Backdrop Props:

PropTypeRequiredDescription
asChildbooleanNoUse the provided child element as the default rendered element, combining their props and behavior.

Backdrop Data Attributes:

AttributeValue
[data-scope]dialog
[data-part]backdrop
[data-state]"open" | "closed"

Backdrop CSS Variables:

VariableDescription
--layer-indexThe index of the dismissable in the layer stack

CloseTrigger Props:

PropTypeRequiredDescription
asChildbooleanNoUse the provided child element as the default rendered element, combining their props and behavior.

Content Props:

PropTypeRequiredDescription
asChildbooleanNoUse the provided child element as the default rendered element, combining their props and behavior.

Content Data Attributes:

AttributeValue
[data-scope]dialog
[data-part]content
[data-state]"open" | "closed"
[data-nested]dialog
[data-has-nested]dialog

Content CSS Variables:

VariableDescription
--layer-indexThe index of the dismissable in the layer stack
--nested-layer-countThe number of nested dialogs

Description Props:

PropTypeRequiredDescription
asChildbooleanNoUse the provided child element as the default rendered element, combining their props and behavior.

Positioner Props:

PropTypeRequiredDescription
asChildbooleanNoUse the provided child element as the default rendered element, combining their props and behavior.

RootProvider Props

PropTypeRequiredDescription
valueUseDialogReturnYes
asChildbooleanNoUse the provided child element as the default rendered element, combining their props and behavior.
immediatebooleanNoWhether to synchronize the present change immediately or defer it to the next frame
lazyMountbooleanNoWhether to enable lazy mounting
onExitCompleteVoidFunctionNoFunction called when the animation ends in the closed state
presentbooleanNoWhether the node is present (controlled by the user)
skipAnimationOnMountbooleanNoWhether to allow the initial presence animation.
unmountOnExitbooleanNoWhether to unmount on exit.

Title Props:

PropTypeRequiredDescription
asChildbooleanNoUse the provided child element as the default rendered element, combining their props and behavior.

Trigger Props

PropTypeRequiredDescription
asChildbooleanNoUse the provided child element as the default rendered element, combining their props and behavior.

Trigger Data Attributes

AttributeValue
[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

  • Edit this page on Github

Dialog Title

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.

Dialog Title

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.

Dialog Title

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.

Dialog Title

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.

Dialog Title

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.

Dialog Title

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.
Copy
'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>
  )
}

Dialog Title

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.
Copy
'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>
  )
}

Dialog Title

1. Acceptance of Terms

By accessing and using this service, you accept and agree to be bound by the terms and provisions of this agreement.

2. Use License

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.

3. User Responsibilities

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.

4. Privacy Policy

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.

5. Limitations

In no event shall we be liable for any damages arising out of the use or inability to use the materials on this service.

6. Revisions

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.

7. Governing Law

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.
Copy

Edit Profile

The first input will be focused when the dialog opens.
Copy
'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

Dialog Title

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.
Copy
'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>
  )
}

Status

Dialog is closed

Copy
'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>
  )
}
Edit
Duplicate

Delete

Dialog Title

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.
Copy
'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>
  )
}

Parent Dialog

This is the parent dialog. Open a nested dialog to see automatic z-index management.

Nested Dialog

This dialog is nested within the parent with proper z-index layering.
Copy
'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

Make changes to your content. You'll be asked to confirm before closing if there are unsaved changes.

Unsaved Changes

You have unsaved changes. Are you sure you want to close without saving?
Copy

Dialog Title

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.
Copy
'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>
  )
}

Dialog Title

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.
Copy
'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>
  )
}
Copy
'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

This dialog is controlled via the useDialog hook.
Copy
'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>
    </>
  )
}

Dialog Title

1. Acceptance of Terms

By accessing and using this service, you accept and agree to be bound by the terms and provisions of this agreement.

2. Use License

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.

3. User Responsibilities

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.

4. Privacy Policy

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.

5. Limitations

In no event shall we be liable for any damages arising out of the use or inability to use the materials on this service.

6. Revisions

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.

7. Governing Law

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.
Copy
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.',
  },
]