Claude Code से accessible modal dialog बनाना: React, dialog और focus
Claude Code से modal dialog बनाएं: dialog element, React code, focus handling, common pitfalls, tests और accessibility.
Modal dialog वह अस्थायी layer है जो मौजूदा page के ऊपर खुलती है और user से छोटा निर्णय या input मांगती है। सही modal सिर्फ बीच में दिखने वाला box नहीं है। सही modal background को inactive करता है, keyboard focus को dialog के अंदर ले जाता है, predictable तरीके से बंद होता है, और बंद होने के बाद focus उसी button पर लौटाता है जिसने उसे खोला था।
अगर आप Claude Code से सिर्फ “एक अच्छा modal बना दो” कहेंगे, तो दिखने में ठीक component मिल सकता है, लेकिन उपयोग में टूट सकता है: Escape काम नहीं करता, Tab background में चला जाता है, screen reader title नहीं पढ़ता, या mobile पर footer button कट जाता है। यह guide practical brief, runnable examples, real use cases, pitfalls और tests देती है।
Official references साथ रखें: browser primitive के लिए MDN <dialog> element, keyboard behavior के लिए WAI-ARIA APG Modal Dialog Pattern, और focus quality के लिए WCAG Focus Order तथा Focus Visible। संबंधित लेख: accessibility implementation, Radix UI, command palette और toast notifications।
Code लिखने से पहले निर्णय
Modal तब अच्छा है जब task छोटा हो और current context को रोकना जरूरी हो: delete confirmation, plan cancel, role change, invite form, checkout से पहले login, या command palette.
Long form, पूरा legal text, multi-page flow, aggressive ad और ऐसी सूचना जिसे user बाद में पढ़ सकता है, modal के लिए सही नहीं हैं। पहले तय करें: क्या page को रोकना जरूरी है? पहला focus कहां जाएगा? कौन सी action बंद करेगी? क्या 320px width पर footer usable है?
Plain words Claude Code को बेहतर दिशा देते हैं। Focus यानी keyboard की वर्तमान जगह। Focus trap यानी Tab movement को dialog के अंदर रखना। inert यानी background interactive नहीं है। ARIA यानी assistive technology को UI का अर्थ बताने वाले attributes।
flowchart TD
A["User trigger दबाता है"] --> B["dialog.showModal() से खोलें"]
B --> C["Focus title या पहली action पर भेजें"]
C --> D["Tab, Shift+Tab, Escape जांचें"]
D --> E["Confirm, cancel, backdrop अलग रखें"]
E --> F["Focus trigger पर लौटाएं"]
Claude Code के लिए brief
Styling से पहले behavior लिखें।
Existing React + TypeScript screen में modal dialog जोड़ें।
Requirements:
- Edit करने से पहले existing buttons, forms, CSS और tests पढ़ें।
- HTML dialog element को प्राथमिकता दें; अगर suitable नहीं है तो कारण बताएं।
- Open होने पर focus title या पहली useful action पर ले जाएं।
- Escape, cancel, confirm और backdrop click को अलग-अलग handle करें।
- Close होने पर focus उस button पर लौटाएं जिसने modal खोला।
- aria-labelledby use करें; short description हो तो aria-describedby use करें।
- outline remove न करें; visible focus के लिए :focus-visible use करें।
- 320px width पर content और footer buttons usable रहें।
- Handoff में failure cases और manual verification steps लिखें।
Allowed files:
- src/components/ModalDialog.tsx
- src/components/modal-dialog.css
- tests/modal-dialog.spec.ts
Runnable HTML example
इसे modal-demo.html के रूप में save करके browser में खोलें।
<!doctype html>
<html lang="hi">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Dialog demo</title>
<style>
body {
font-family: system-ui, sans-serif;
line-height: 1.7;
padding: 2rem;
}
button {
font: inherit;
border: 0;
border-radius: 6px;
padding: 0.7rem 1rem;
cursor: pointer;
}
.danger {
background: #dc2626;
color: white;
}
dialog {
width: min(calc(100vw - 2rem), 28rem);
border: 0;
border-radius: 8px;
padding: 0;
box-shadow: 0 24px 80px rgb(15 23 42 / 0.3);
}
dialog::backdrop {
background: rgb(15 23 42 / 0.58);
}
.modal-body {
padding: 1.25rem;
}
.button-row {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1.5rem;
}
:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 3px;
}
</style>
</head>
<body>
<main>
<h1>Project settings</h1>
<p>Modal सिर्फ उन actions के लिए use करें जो page को रोकनी चाहिए।</p>
<button id="open-dialog" class="danger" type="button">
Project delete करें
</button>
</main>
<dialog id="confirm-dialog" aria-labelledby="dialog-title">
<div class="modal-body">
<h2 id="dialog-title" tabindex="-1">इस project को delete करें?</h2>
<p>यह action undo नहीं हो सकती। जरूरत हो तो पहले data export करें।</p>
<div class="button-row">
<button id="cancel-dialog" type="button">Cancel</button>
<button id="confirm-delete" class="danger" type="button">
Delete
</button>
</div>
</div>
</dialog>
<script>
const openButton = document.querySelector("#open-dialog");
const dialog = document.querySelector("#confirm-dialog");
const title = document.querySelector("#dialog-title");
const cancelButton = document.querySelector("#cancel-dialog");
const confirmButton = document.querySelector("#confirm-delete");
openButton.addEventListener("click", () => {
dialog.showModal();
title.focus();
});
cancelButton.addEventListener("click", () => dialog.close("cancel"));
confirmButton.addEventListener("click", () => {
console.log("delete project");
dialog.close("confirm");
});
dialog.addEventListener("click", (event) => {
if (event.target === dialog) {
dialog.close("backdrop");
}
});
dialog.addEventListener("close", () => {
openButton.focus();
console.log(`closed by: ${dialog.returnValue || "unknown"}`);
});
</script>
</body>
</html>
Modal खोलने के लिए showModal() use करें। सिर्फ open attribute लगाने से background interactive रह सकता है।
Reusable React component
import * as React from "react";
import "./modal-dialog.css";
type ModalDialogProps = {
open: boolean;
title: string;
description?: string;
closeOnBackdrop?: boolean;
onClose: () => void;
children: React.ReactNode;
footer: React.ReactNode;
};
const focusableSelector = [
"a[href]",
"button:not([disabled])",
"input:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
"[tabindex]:not([tabindex='-1'])",
].join(",");
export function ModalDialog({
open,
title,
description,
closeOnBackdrop = true,
onClose,
children,
footer,
}: ModalDialogProps) {
const dialogRef = React.useRef<HTMLDialogElement>(null);
const titleRef = React.useRef<HTMLHeadingElement>(null);
const openerRef = React.useRef<HTMLElement | null>(null);
const titleId = React.useId();
const descriptionId = React.useId();
React.useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (open && !dialog.open) {
openerRef.current =
document.activeElement instanceof HTMLElement
? document.activeElement
: null;
dialog.showModal();
window.requestAnimationFrame(() => {
const preferred = dialog.querySelector<HTMLElement>("[data-autofocus]");
const firstFocusable = dialog.querySelector<HTMLElement>(
focusableSelector,
);
(preferred ?? firstFocusable ?? titleRef.current)?.focus();
});
}
if (!open && dialog.open) {
dialog.close();
}
}, [open]);
React.useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
function handleClose() {
onClose();
openerRef.current?.focus();
}
function handleClick(event: MouseEvent) {
if (event.target === dialog && closeOnBackdrop) {
onClose();
}
}
dialog.addEventListener("close", handleClose);
dialog.addEventListener("click", handleClick);
return () => {
dialog.removeEventListener("close", handleClose);
dialog.removeEventListener("click", handleClick);
};
}, [closeOnBackdrop, onClose]);
return (
<dialog
ref={dialogRef}
className="app-modal"
aria-labelledby={titleId}
aria-describedby={description ? descriptionId : undefined}
>
<div className="app-modal__body">
<div className="app-modal__header">
<h2 id={titleId} ref={titleRef} tabIndex={-1}>
{title}
</h2>
<button
type="button"
className="app-modal__icon"
aria-label="Dialog close करें"
onClick={onClose}
>
x
</button>
</div>
{description ? (
<p id={descriptionId} className="app-modal__description">
{description}
</p>
) : null}
<div className="app-modal__content">{children}</div>
<div className="app-modal__footer">{footer}</div>
</div>
</dialog>
);
}
.app-modal {
width: min(calc(100vw - 32px), 520px);
max-height: calc(100vh - 32px);
border: 0;
border-radius: 8px;
padding: 0;
color: #0f172a;
box-shadow: 0 24px 80px rgb(15 23 42 / 0.3);
}
.app-modal::backdrop {
background: rgb(15 23 42 / 0.58);
}
.app-modal__body {
display: grid;
gap: 16px;
padding: 24px;
}
.app-modal__header,
.app-modal__footer {
display: flex;
gap: 12px;
}
.app-modal__header {
align-items: flex-start;
justify-content: space-between;
}
.app-modal__footer {
flex-wrap: wrap;
justify-content: flex-end;
}
.app-modal__icon {
width: 36px;
height: 36px;
border: 0;
border-radius: 999px;
background: #e2e8f0;
cursor: pointer;
}
:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 3px;
}
@media (max-width: 480px) {
.app-modal__footer {
flex-direction: column-reverse;
}
.app-modal__footer button {
width: 100%;
}
}
तीन real use cases
| Use case | Modal क्यों सही है | Claude Code से क्या कहलवाएं |
|---|---|---|
| Delete, cancel, role change | Action undo करना मुश्किल है | Danger copy, double-submit guard, audit log |
| Invite, billing, short settings | Context छोड़े बिना task पूरा होता है | Validation, pending state, focus after success |
| Command palette या quick search | Navigation के बिना तेज action | Arrow keys, aria-activedescendant, empty state |
Dangerous action में backdrop click से close करना हमेशा सही नहीं होता। Short form में error आने पर modal खुला रखें और error readable बनाएं।
Promise-based confirmation
import * as React from "react";
import { createRoot } from "react-dom/client";
import { ModalDialog } from "./ModalDialog";
type ConfirmDialogOptions = {
title: string;
message: string;
confirmLabel?: string;
cancelLabel?: string;
danger?: boolean;
};
export function confirmDialog(
options: ConfirmDialogOptions,
): Promise<boolean> {
return new Promise((resolve) => {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
function finish(result: boolean) {
root.unmount();
container.remove();
resolve(result);
}
function ConfirmHost() {
return (
<ModalDialog
open
title={options.title}
description={options.message}
closeOnBackdrop={false}
onClose={() => finish(false)}
footer={
<>
<button type="button" onClick={() => finish(false)}>
{options.cancelLabel ?? "Cancel"}
</button>
<button
type="button"
data-autofocus
className={options.danger ? "danger" : "primary"}
onClick={() => finish(true)}
>
{options.confirmLabel ?? "Confirm"}
</button>
</>
}
>
<p>Continue करने से पहले details check करें।</p>
</ModalDialog>
);
}
root.render(<ConfirmHost />);
});
}
Common pitfalls
पहला pitfall है mouse-only close control। वास्तविक button use करें और icon-only button को accessible name दें।
दूसरा pitfall है title हटाना। Dialog को accessible name चाहिए, इसलिए aria-labelledby use करें।
तीसरा pitfall है outline: none बिना replacement। :focus-visible से focus दिखाएं।
चौथा pitfall है modal के ऊपर modal। इससे focus return और Escape unclear हो जाते हैं। Clear copy या undo बेहतर हो सकता है।
पांचवां pitfall है mobile overflow। max-height, overflow: auto और 320px manual check जरूरी है।
Playwright smoke test
import { expect, test } from "@playwright/test";
test("modal opens, closes, and returns focus", async ({ page }) => {
await page.goto("/settings");
const trigger = page.getByRole("button", { name: "Project delete करें" });
await trigger.click();
const dialog = page.getByRole("dialog", {
name: "इस project को delete करें?",
});
await expect(dialog).toBeVisible();
await page.keyboard.press("Tab");
await expect(page.locator(":focus")).toBeVisible();
await page.keyboard.press("Escape");
await expect(dialog).toBeHidden();
await expect(trigger).toBeFocused();
});
Manual QA में mouse न इस्तेमाल करें। Tab, Shift+Tab, Enter, Space, Escape से चलाएं, फिर NVDA या VoiceOver से title और buttons पढ़वाएं, और narrow mobile viewport देखें।
CTA और monetization
Modal अक्सर revenue path के पास होता है: checkout confirmation, consultation form, email capture, training request. इसलिए उसे aggressive ad जैसा न बनाएं। Clear, short और respectful रखें।
Teams के लिए Claude Code rollout, CLAUDE.md, accessible UI review और React workflow cleanup में Claude Code training and consultation मदद कर सकता है। Individual builders products और free cheatsheet से prompts, reviews और tests standardize कर सकते हैं।
Result
Masa ने small React settings screen में इस pattern को आजमाया। सबसे ज्यादा सुधार animation से नहीं, बल्कि acceptance criteria से आया: focus opener पर लौटे, dangerous action backdrop click से close न हो, और 320px width पर footer buttons usable रहें। इन तीन शर्तों से Claude Code की output review करना काफी आसान हुआ।
मुफ़्त PDF: Claude Code cheatsheet
Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.
हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.
लेखक के बारे में
Masa
Claude Code workflow और team adoption पर काम करने वाला engineer.
संबंधित लेख
Claude Code permission safety ladder: access धीरे-धीरे बढ़ाएं
read-only से limited edits, proof commands और deploy checks तक permission बढ़ाने की सुरक्षित ladder.
Claude Code Small PR Proof Pack: छोटे PR को review-ready बनाना
Claude Code PR के लिए diff, checks, public URL, CTA path और rollback वाला practical proof pack.
Claude Code Review Gate Before Commit: diff, test, public URL और CTA जांच
Claude Code से commit से पहले review gate बनाएं: diff, build, public URL, Gumroad, consultation, tests और unrelated files।