Menu

Site and documentation links

Search documentation

Type to filter pages and sections. Use the arrow keys to move and Enter to open.

    Dialog

    A modal window over the page that keeps focus inside until it closes.

    Delete this project?

    This removes the project and its deploy history. You can't undo it.

    View code
    import {
      Dialog,
      DialogClose,
      DialogContent,
      DialogDescription,
      DialogTitle,
      DialogTrigger
    } from '@syncclouds/ui'
    
    export function DialogDemo() {
      return (
        <Dialog>
          <DialogTrigger>Delete project</DialogTrigger>
          <DialogContent>
            <DialogTitle>Delete this project?</DialogTitle>
            <DialogDescription>
              This removes the project and its deploy history. You can&apos;t undo it.
            </DialogDescription>
            <div className="mt-6 flex justify-end gap-3">
              <DialogClose autoFocus>Cancel</DialogClose>
              <DialogClose data-tone="danger">Delete</DialogClose>
            </div>
          </DialogContent>
        </Dialog>
      )
    }

    Installation

    npm install @syncclouds/ui

    Usage

    import {
      Dialog,
      DialogClose,
      DialogContent,
      DialogDescription,
      DialogTitle,
      DialogTrigger
    } from '@syncclouds/ui'
    <Dialog>
      <DialogTrigger>Delete project</DialogTrigger>
      <DialogContent>
        <DialogTitle>Delete this project?</DialogTitle>
        <DialogDescription>You can't undo it.</DialogDescription>
        <DialogClose autoFocus>Cancel</DialogClose>
        <DialogClose data-tone="danger">Delete</DialogClose>
      </DialogContent>
    </Dialog>

    Anatomy

    Dialog
    ├── DialogTrigger
    └── DialogContent
        ├── DialogTitle
        ├── DialogDescription
        └── DialogClose

    Controlled

    Pass open and onOpenChange to open or close it from your own state.

    Publish to production?

    Visitors will see these changes straight away.

    Open: no. Last choice: Nothing yet.

    View code
    'use client'
    
    import { useState } from 'react'
    import {
      Dialog,
      DialogClose,
      DialogContent,
      DialogDescription,
      DialogTitle,
      DialogTrigger
    } from '@syncclouds/ui'
    
    export function DialogControlled() {
      const [open, setOpen] = useState(false)
      const [result, setResult] = useState('Nothing yet')
    
      return (
        <div className="flex flex-col items-center gap-4">
          <Dialog open={open} onOpenChange={setOpen}>
            <DialogTrigger>Publish changes</DialogTrigger>
            <DialogContent>
              <DialogTitle>Publish to production?</DialogTitle>
              <DialogDescription>Visitors will see these changes straight away.</DialogDescription>
              <div className="mt-6 flex justify-end gap-3">
                <DialogClose onClick={() => setResult('Cancelled')}>Cancel</DialogClose>
                <DialogClose data-tone="primary" onClick={() => setResult('Published')}>
                  Publish
                </DialogClose>
              </div>
            </DialogContent>
          </Dialog>
          <p className="text-sm">
            Open: {open ? 'yes' : 'no'}. Last choice: {result}.
          </p>
        </div>
      )
    }

    Form inside

    A form inside the dialog. Save stays open while a field is invalid.

    Rename project

    The new name shows everywhere straight away.

    Project name: Checkout

    View code
    'use client'
    
    import { useState, type FormEvent } from 'react'
    import {
      Dialog,
      DialogClose,
      DialogContent,
      DialogDescription,
      DialogTitle,
      DialogTrigger
    } from '@syncclouds/ui'
    
    export function DialogForm() {
      const [open, setOpen] = useState(false)
      const [name, setName] = useState('Checkout')
    
      const onSubmit = (event: FormEvent<HTMLFormElement>) => {
        event.preventDefault()
        setName(String(new FormData(event.currentTarget).get('name')))
        setOpen(false)
      }
    
      return (
        <div className="flex flex-col items-center gap-4">
          <Dialog open={open} onOpenChange={setOpen}>
            <DialogTrigger>Rename project</DialogTrigger>
            <DialogContent>
              <DialogTitle>Rename project</DialogTitle>
              <DialogDescription>The new name shows everywhere straight away.</DialogDescription>
              <form onSubmit={onSubmit} className="mt-5 flex flex-col gap-2">
                <label htmlFor="project-name" className="text-sm font-semibold">
                  Name
                </label>
                <input
                  id="project-name"
                  name="name"
                  defaultValue={name}
                  required
                  className="h-10 rounded-lg border border-current/40 bg-transparent px-3"
                />
                <div className="mt-4 flex justify-end gap-3">
                  <DialogClose>Cancel</DialogClose>
                  {/* Submits the form; stays open while a field is invalid. */}
                  <DialogClose
                    type="submit"
                    data-tone="primary"
                    onClick={event => {
                      const form = event.currentTarget.form
                      if (form && !form.reportValidity()) event.preventDefault()
                    }}
                  >
                    Save
                  </DialogClose>
                </div>
              </form>
            </DialogContent>
          </Dialog>
          <p className="text-sm">Project name: {name}</p>
        </div>
      )
    }

    Without the theme

    Keyboard shortcuts

    Press these anywhere in the app.

    • Ctrl K opens search
    • G then P goes to projects
    View code
    import {
      Dialog,
      DialogClose,
      DialogContent,
      DialogDescription,
      DialogTitle,
      DialogTrigger
    } from '@syncclouds/ui'
    
    const BUTTON =
      'rounded-md px-3 py-1.5 text-sm font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'
    
    export function DialogUnstyled() {
      return (
        <Dialog>
          <DialogTrigger className={`${BUTTON} bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900`}>
            Show shortcuts
          </DialogTrigger>
          <DialogContent className="m-auto w-full max-w-sm rounded-xl bg-white p-6 text-zinc-900 shadow-xl backdrop:bg-zinc-950/50 dark:bg-zinc-900 dark:text-zinc-100">
            <DialogTitle className="text-lg font-semibold">Keyboard shortcuts</DialogTitle>
            <DialogDescription className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
              Press these anywhere in the app.
            </DialogDescription>
            <ul className="mt-4 space-y-2 text-sm">
              <li>Ctrl K opens search</li>
              <li>G then P goes to projects</li>
            </ul>
            <DialogClose className={`${BUTTON} mt-6 border border-zinc-300 dark:border-zinc-600`}>
              Close
            </DialogClose>
          </DialogContent>
        </Dialog>
      )
    }

    Accessibility

    Follows the WAI-ARIA Dialog (Modal) pattern.

    KeysWhat they do
    Enter, SpaceOn the trigger, opens the dialog.
    Tab, Shift TabMoves between the focusable elements inside. The page behind is inert.
    EscapeCloses the dialog and returns focus to the trigger.
    • Focus moves to the first focusable element, or to one marked autoFocus. For a destructive action, put autoFocus on the safe choice.
    • A click on the backdrop closes the dialog.
    • Always include DialogTitle and DialogDescription. Hide them visually if the design has no room, but keep them in the markup.

    API reference

    Every part also accepts the props of the element it renders, including className and ref.

    Dialogrenders no element

    Holds the open state. Renders no element of its own.

    PropTypeDefaultDescription
    openbooleannoneWhether the dialog is open, when you control it.
    defaultOpenbooleanfalseWhether it starts open, when the dialog controls itself.
    onOpenChange(open: boolean) => voidnoneCalled when it opens or closes, including on Esc and backdrop clicks.

    DialogTriggerrenders <button>

    Opens the dialog. Focus returns here when it closes.

    PropTypeDefaultDescription
    asChildbooleanfalseUse your own button element.
    Data attributeValues
    data-state"open" | "closed"

    DialogContentrenders <dialog>

    The native dialog element, opened with showModal(). Named by DialogTitle and described by DialogDescription.

    Data attributeValues
    data-state"open" | "closed"

    DialogTitlerenders <h2>

    Names the dialog.

    DialogDescriptionrenders <p>

    Describes the dialog.

    DialogCloserenders <button>

    Closes the dialog.

    PropTypeDefaultDescription
    asChildbooleanfalseUse your own button element.
    Built by Syncclouds. Syncclouds UI is in development.