Responsive dialog
ProA dialog that suits the screen it opens on: a centred Dialog when the viewport is breakpoint px wide or wider (640 unless the app says otherwise), and a Drawer from the bottom edge when it is narrower. The form is chosen as it opens and kept while it is open, so turning a phone or resizing a window doesn't redraw what the person was doing or lose what they typed. Server-rendered, it is the centred dialog until hydration ends. Focus goes into it when it opens and back to the trigger when it closes, in either form.
Build it from its parts as you would a Dialog: ResponsiveDialogTrigger, then ResponsiveDialogContent holding a header (title and description), an optional ResponsiveDialogBody for content that can grow, and a footer.
onDismissAttempt (on the root) and unsaved (on the content) work in both forms, as on Dialog and Drawer: the app is asked before Escape, a press outside, the × or a drag closes it, and unsaved work is confirmed in a bar first.
Version 5.2.0· other versions
This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0 and 4.2.0: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
Loading the preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/responsive-dialog
Usage
"use client"
import * as React from "react"
import { Button } from "@/components/candor/ui/button"
import { Input } from "@/components/candor/ui/input"
import { Label } from "@/components/candor/ui/label"
import {
ResponsiveDialog,
ResponsiveDialogBody,
ResponsiveDialogClose,
ResponsiveDialogContent,
ResponsiveDialogDescription,
ResponsiveDialogFooter,
ResponsiveDialogHeader,
ResponsiveDialogTitle,
ResponsiveDialogTrigger,
} from "@/components/candor/ui/responsive-dialog"
export default function Example() {
const [name, setName] = React.useState("Refunds team")
const [saved, setSaved] = React.useState("Refunds team")
return (
// A centred dialog from 640px wide, a drawer from the bottom below it. Narrow the window, or open it on a phone.
<ResponsiveDialog onOpenChange={(open) => open && setName(saved)}>
<ResponsiveDialogTrigger render={<Button variant="outline">Rename workspace</Button>} />
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Rename workspace</ResponsiveDialogTitle>
<ResponsiveDialogDescription>Everyone in {saved} sees the new name. Links to it keep working.</ResponsiveDialogDescription>
</ResponsiveDialogHeader>
<ResponsiveDialogBody>
<div className="flex flex-col gap-2 py-1">
<Label htmlFor="workspace-name">Name</Label>
<Input id="workspace-name" value={name} onChange={(e) => setName(e.target.value)} />
</div>
</ResponsiveDialogBody>
<ResponsiveDialogFooter>
<ResponsiveDialogClose render={<Button variant="outline">Cancel</Button>} />
<ResponsiveDialogClose render={<Button onClick={() => setSaved(name.trim() || saved)}>Rename</Button>} />
</ResponsiveDialogFooter>
</ResponsiveDialogContent>
</ResponsiveDialog>
)
}<ResponsiveDialog>
| Prop | Type | Default | Description |
|---|---|---|---|
| onDismissAttempt | ((attempt: { reason: DrawerDismissReason }) => boolean | void) | undefined | — | Called when the person tries to close it by Escape, a press outside, the × or (as a drawer) a drag, before it closes. Return `false` to keep it open; anything else lets it close, or shows the unsaved bar when the content has `unsaved`. |
| open | boolean | undefined | — | Whether it is open, when the app holds the state. Leave it out to let the dialog hold it. |
| defaultOpen | boolean | undefined | false | Whether it starts open, when the dialog holds the state. Defaults to false. |
| onOpenChange | ((open: boolean) => void) | undefined | — | Called when it opens or closes. |
| breakpoint | number | undefined | 640 | The viewport width, in px, from which it is a centred dialog; narrower, a drawer from the bottom. Defaults to 640. |
| childrenrequired | React.ReactNode | — | The trigger and the content. |
<ResponsiveDialogBody>
The part that scrolls when its content is taller than the window allows, between the header and the footer, in either form. While it overflows it takes focus and is named by the title, so a keyboard can scroll it.
No props.
Also takes the props of React.ComponentProps<"div">.
<ResponsiveDialogClose>
A control that closes it; event.preventDefault() in its onClick keeps it open, as on Radix.
No props.
Also takes the props of React.ComponentProps<typeof DialogClose>.
<ResponsiveDialogContent>
Its window: a centred dialog, or a drawer from the bottom, as the viewport was when it opened (data-form says which). The close (×) button is in the corner of both.
| Prop | Type | Default | Description |
|---|---|---|---|
| unsaved | string | undefined | — | What a dismissal would lose, in a few words ("your reply"): while set, a dismissal asks first, in a bar in the window. |
| size | "default" | "wide" | "full" | undefined | — | The dialog's width on a wide screen: "default", "wide" or "full" (see DialogContent). The drawer is the screen's width. |
| showCloseButton | boolean | undefined | true | Shows the close (×) button in the corner. Defaults to true. |
| className | string | undefined | — | Classes for the window, merged with Candor's. |
| childrenrequired | React.ReactNode | — | The header, body and footer. |
<ResponsiveDialogDescription>
The sentence under the title that says what it is for, read with its name.
No props.
Also takes the props of React.ComponentProps<typeof DialogDescription>.
<ResponsiveDialogFooter>
The row at the bottom for its actions.
No props.
Also takes the props of React.ComponentProps<"div">.
<ResponsiveDialogHeader>
The block at the top for its title and description.
No props.
Also takes the props of React.ComponentProps<"div">.
<ResponsiveDialogTitle>
Its heading, which names it to screen readers.
No props.
Also takes the props of React.ComponentProps<typeof DialogTitle>.
<ResponsiveDialogTrigger>
The control that opens it; focus returns to it when it closes.
No props.
Also takes the props of React.ComponentProps<typeof DialogTrigger>.