Dialog
A native modal with controlled Escape handling, focus return, and a considered surface.
Live preview
Adjust a setting to update the preview and usage code together.
Usage & source
"use client";
import type { CSSProperties } from "react";
import { Dialog } from "./components/stepkit/Dialog";
import "./components/stepkit/styles.css";
const themeStyle = {"--stepkit-bg":"#f7f7f8","--stepkit-surface":"#ffffff","--stepkit-surface-muted":"#f0f0f3","--stepkit-fg":"#17181c","--stepkit-muted":"#626572","--stepkit-border":"#e3e4e9","--stepkit-border-strong":"#c8cad3","--stepkit-code":"#f1f1f4","--stepkit-shadow":"0 20px 60px rgba(31, 26, 64, .10)","--stepkit-accent":"#6950df","--stepkit-accent-strong":"#5137c7","--stepkit-accent-soft":"#eeeafd","--stepkit-accent-on-soft":"#5137c7","--stepkit-accent-text":"#ffffff","--stepkit-error":"#b43145","--stepkit-placeholder":"#626572","--stepkit-radius":"16px","--stepkit-space":"16px","--stepkit-control-height":"44px","--stepkit-font-sans":"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif"} as CSSProperties;
export default function Example() {
return (
<div className="stepkit-theme" data-stepkit-preset="modern" style={themeStyle}>
<Dialog
eyebrow={"Quick preview"}
title={"Before you continue"}
body={"This is a small, focused dialog."}
triggerText={"Open dialog"}
/>
</div>
);
}Dependencies
[email protected] [email protected]
/*
* Stepkit 0.2.0
* Copyright (c) 2026 Vladislav Stepanov
*
* Original Stepkit portions in this release are licensed under the MIT License. Adapted portions identify their upstream licence and copyright in the copied source and in `NOTICE-MAGICUI.md` where applicable.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
"use client";
import { useEffect, useId, useRef, useState, type ReactNode } from "react";
import { Button } from "./Button";
import "./styles.css";
export interface DialogProps {
eyebrow?: string;
title?: string;
body?: string;
triggerText?: string;
closeText?: string;
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
children?: ReactNode;
className?: string;
}
export function Dialog({
title = "Before you continue",
eyebrow,
body = "This is a small, focused dialog.",
triggerText = "Open dialog",
closeText = "Close",
defaultOpen = false,
open: controlledOpen,
onOpenChange,
children,
className = ""
}: DialogProps) {
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
const isControlled = controlledOpen !== undefined;
const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
const dialogRef = useRef<HTMLDialogElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const closeRequestedRef = useRef(false);
const titleId = useId();
const bodyId = useId();
const setOpen = (next: boolean) => {
if (!next) closeRequestedRef.current = true;
if (!isControlled) setUncontrolledOpen(next);
onOpenChange?.(next);
};
useEffect(() => {
const node = dialogRef.current;
if (!node) return;
if (isOpen && !node.open) {
node.showModal();
} else if (!isOpen && node.open) {
node.close();
}
}, [isOpen]);
useEffect(() => {
const node = dialogRef.current;
if (!node) return;
const handleClose = () => {
const requested = closeRequestedRef.current;
closeRequestedRef.current = false;
if (!isControlled) setUncontrolledOpen(false);
if (!isControlled && !requested) onOpenChange?.(false);
triggerRef.current?.focus();
};
node.addEventListener("close", handleClose);
return () => node.removeEventListener("close", handleClose);
}, [isControlled, onOpenChange]);
return (
<div className={`stepkit stepkit-dialog${className ? ` ${className}` : ""}`}>
<Button ref={triggerRef} className="stepkit-modal-trigger" label={triggerText} onClick={() => setOpen(true)} />
<dialog ref={dialogRef} className="stepkit stepkit-modal-panel" aria-modal="true" aria-labelledby={titleId} aria-describedby={bodyId} onCancel={(event) => { event.preventDefault(); setOpen(false); }}>
{eyebrow ? <p className="stepkit-card__eyebrow">{eyebrow}</p> : null}
<h2 className="stepkit-modal-title" id={titleId}>{title}</h2>
<p className="stepkit-modal-body" id={bodyId}>{body}</p>
{children}
<div className="stepkit-modal-actions">
<Button label={closeText} onClick={() => setOpen(false)} />
</div>
</dialog>
</div>
);
}
export default Dialog;
components/Dialog.tsxcomponents/Button.tsxcomponents/styles.cssInstall
Use the released registry entry or download the complete archive.
npx [email protected] add https://vstepanov.com/stepkit/r/dialog.jsonIntegration prompt
Install Stepkit Dialog 0.2.0 from the registry and preserve the included MIT notice. npx [email protected] add https://vstepanov.com/stepkit/v0.2.0/r/dialog.json Install the tested runtime dependencies exactly: npm install [email protected] [email protected] Keep the component source, shared styles, NOTICE.md, and provenance files together. Apply the theme tokens on a wrapper rather than passing theme keys as component props. Usage: ```tsx "use client"; import type { CSSProperties } from "react"; import { Dialog } from "./components/stepkit/Dialog"; import "./components/stepkit/styles.css"; const themeStyle = {"--stepkit-bg":"#f7f7f8","--stepkit-surface":"#ffffff","--stepkit-surface-muted":"#f0f0f3","--stepkit-fg":"#17181c","--stepkit-muted":"#626572","--stepkit-border":"#e3e4e9","--stepkit-border-strong":"#c8cad3","--stepkit-code":"#f1f1f4","--stepkit-shadow":"0 20px 60px rgba(31, 26, 64, .10)","--stepkit-accent":"#6950df","--stepkit-accent-strong":"#5137c7","--stepkit-accent-soft":"#eeeafd","--stepkit-accent-on-soft":"#5137c7","--stepkit-accent-text":"#ffffff","--stepkit-error":"#b43145","--stepkit-placeholder":"#626572","--stepkit-radius":"16px","--stepkit-space":"16px","--stepkit-control-height":"44px","--stepkit-font-sans":"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif"} as CSSProperties; export default function Example() { return ( <div className="stepkit-theme" data-stepkit-preset="modern" style={themeStyle}> <Dialog eyebrow={"Quick preview"} title={"Before you continue"} body={"This is a small, focused dialog."} triggerText={"Open dialog"} /> </div> ); } ``` Selected bounded settings: {"eyebrow":"Quick preview","title":"Before you continue","body":"This is a small, focused dialog.","triggerText":"Open dialog"}. Selected theme: {"mode":"light","accent":"violet","radius":"soft","density":"comfortable","preset":"modern"}.
Props
| Name | Type | Default | Description |
|---|---|---|---|
title | string | "Before you continue" | Dialog heading. |
body | string | "This is a small, focused dialog." | Dialog description. |
open | boolean | undefined | Optional controlled open state. |
onOpenChange | (open: boolean) => void | undefined | Controlled state callback. |
Accessibility & limits
- Uses the native dialog element with modal semantics.
- Escape requests a controlled close and focus returns to the trigger after close.
- Long content remains scrollable.
Limitations
- The dialog assumes a browser environment when opened.
Provenance
Original source under the MIT licence.
Original Stepkit implementation.
Required notices: NOTICE.md, PROVENANCE-stepkit-dialog.md
- 3 source files in the release
- Version 0.2.0