Dialog

A modal dialog for confirmations and short forms. It renders above the page, keeps keyboard focus inside, and returns focus to the trigger when it closes.

import { Dialog } from 'jarvis-react-ui';

Confirmation

The dialog is controlled: keep open in state and update it in onOpenChange. Put the buttons in actions; they appear in the footer, right-aligned.

import { useState } from 'react';
import { Button, Dialog, useToast } from 'jarvis-react-ui';

export default function Confirm() {
    const [open, setOpen] = useState(false);
    const { toast } = useToast();

    function purge() {
        setOpen(false);
        toast({ variant: 'success', title: 'Cache purged' });
    }

    return (
        <>
            <Button variant="danger" onClick={() => setOpen(true)}>
                Purge cache
            </Button>
            <Dialog
                open={open}
                onOpenChange={setOpen}
                title="Purge cache"
                description="All cached telemetry will be deleted."
                size="sm"
                actions={
                    <>
                        <Button variant="ghost" size="sm" onClick={() => setOpen(false)}>
                            Cancel
                        </Button>
                        <Button variant="danger" size="sm" onClick={purge}>
                            Purge
                        </Button>
                    </>
                }
            >
                This cannot be undone. Running sessions keep their data.
            </Dialog>
        </>
    );
}

Form

initialFocusRef chooses the element that gets focus when the dialog opens, here the input. Without it, the first focusable element (usually the close button) gets focus.

import { useRef, useState } from 'react';
import type { FormEvent } from 'react';
import { Button, Dialog, Input, Label } from 'jarvis-react-ui';

export default function Form() {
    const [open, setOpen] = useState(false);
    const [callsign, setCallsign] = useState('JARVIS');
    const inputRef = useRef<HTMLInputElement>(null);

    function submit(e: FormEvent<HTMLFormElement>) {
        e.preventDefault();
        setOpen(false);
    }

    return (
        <>
            <Button onClick={() => setOpen(true)}>Rename: {callsign}</Button>
            <Dialog
                open={open}
                onOpenChange={setOpen}
                title="Rename assistant"
                initialFocusRef={inputRef}
            >
                <form id="rename-form" onSubmit={submit} className="flex flex-col gap-2">
                    <Label htmlFor="callsign">Callsign</Label>
                    <Input
                        id="callsign"
                        ref={inputRef}
                        fullWidth
                        value={callsign}
                        onChange={(e) => setCallsign(e.target.value)}
                    />
                    <div className="flex justify-end gap-2 pt-2">
                        <Button
                            type="button"
                            variant="ghost"
                            size="sm"
                            onClick={() => setOpen(false)}
                        >
                            Cancel
                        </Button>
                        <Button type="submit" variant="primary" size="sm">
                            Save
                        </Button>
                    </div>
                </form>
            </Dialog>
        </>
    );
}

Sizes

sm, md (default) and lg cap the width at 360, 520 and 720px. Long content scrolls inside the body while the header and footer stay visible.

import { useState } from 'react';
import { Button, Dialog } from 'jarvis-react-ui';
import type { DialogSize } from 'jarvis-react-ui';

const SIZES: DialogSize[] = ['sm', 'md', 'lg'];

export default function Sizes() {
    const [size, setSize] = useState<DialogSize | null>(null);

    return (
        <>
            {SIZES.map((s) => (
                <Button key={s} onClick={() => setSize(s)}>
                    {s}
                </Button>
            ))}
            <Dialog
                open={size !== null}
                onOpenChange={() => setSize(null)}
                title={`Size ${size ?? ''}`}
                size={size ?? 'md'}
            >
                The panel is at most {size === 'sm' ? 360 : size === 'lg' ? 720 : 520}px wide and
                never wider than the viewport.
            </Dialog>
        </>
    );
}

Closing

The dialog calls onOpenChange(false) when the user presses Escape, clicks the backdrop or uses the close button. Set closeOnBackdrop={false} for dialogs that must be answered explicitly, and showClose={false} to hide the close button.

Accessibility

  • role="dialog" with aria-modal="true", named by title (aria-labelledby) and described by description (aria-describedby). Use hideTitle to keep the name while hiding the heading.
  • Focus moves into the dialog on open, Tab and Shift+Tab cycle inside it, and focus returns to the previously focused element on close.
  • Page scrolling is locked while the dialog is open.
  • The dialog is portaled to document.body, so ancestors with overflow: hidden or transform cannot clip it.

Sound

Opening plays menu_open, closing plays menu_close.

API

Prop Type Default Description
onOpenChange (required) (open: boolean) => void Called with false when the user closes the dialog (Escape, backdrop, close button).
open (required) boolean Whether the dialog is shown. The dialog is controlled.
title (required) ReactNode Heading of the dialog; also its accessible name.
actions ReactNode Footer content, typically buttons, aligned to the right.
children ReactNode Dialog body.
className string Additional class names for the dialog panel.
closeOnBackdrop boolean true Closes the dialog when the backdrop is clicked.
description ReactNode Optional text below the title; referenced by aria-describedby.
hideTitle boolean false Hides the title visually while keeping it as the accessible name.
initialFocusRef RefObject<HTMLElement | null> Element that receives focus when the dialog opens. Defaults to the first focusable element.
showClose boolean true Shows the close button in the header.
size 'sm' | 'md' | 'lg' 'md' Maximum width: sm 360px, md 520px, lg 720px.