@rokku-x/react-hook-dialog
v1.2.4
Published
A React dialog hook library for confirmation dialogs, alerts, and modals with rich variants and full customization
Maintainers
Readme
react-hook-dialog
A lightweight, powerful, and flexible React dialog hook library for confirmation dialogs, alerts, and modals. Built on top of @rokku-x/react-hook-modal with a focus on dialog-specific features like action buttons, variants, and customizable styling.
Features
- ♿ Accessibility Focused - Keyboard navigation and ARIA support
- 🔄 Asynchronous - Async/await friendly dialog results
- 🎯 Hook-based API - Simple and intuitive
useHookDialog()hook - 🎨 Rich Variants - 7 button variants (primary, secondary, danger, success, warning, info, neutral)
- 📝 Dialog Actions - Flexible action button system with left/right positioning
- 💅 Full Customization - Injectable className and styles at every level
- ⌨️ Rich Configuration - Default configs with per-call overrides
- 📱 TypeScript Support - Full type safety out of the box
- 🧑🤝🧑 Multiple Dialogs - Support for multiple simultaneous dialogs
- 🛠️ Programmatic Control - Force actions and cancellations via dialog context
- 🖼️ Rich Content Support - Accepts React nodes for titles and content
- 📦 Lightweight - Minimal bundle size for fast load times
Installation
npm install @rokku-x/react-hook-dialog
# or
bun add @rokku-x/react-hook-dialog
# or
yarn add @rokku-x/react-hook-dialog
# or
pnpm add @rokku-x/react-hook-dialogQuick Start
1. Setup BaseModalRenderer or BaseDialogRenderer
You can either use the upstream BaseModalRenderer directly (from @rokku-x/react-hook-modal) or use the convenience wrapper BaseDialogRenderer provided by this package.
BaseModalRenderer(upstream): mount this at the root to render modal instances.BaseDialogRenderer(this package): a thin wrapper aroundBaseModalRendererthat lets you pass adefaultConfigprop to set default dialog options for all dialogs created byuseHookDialog().
Use BaseModalRenderer directly:
import { BaseModalRenderer } from '@rokku-x/react-hook-modal';
function App() {
return (
<>
<YourComponents />
<BaseModalRenderer />
</>
);
}Or use the wrapper BaseDialogRenderer from this package to set defaults:
import { BaseDialogRenderer } from '@rokku-x/react-hook-dialog';
function App() {
return (
<>
<YourComponents />
<BaseDialogRenderer defaultConfig={{ showCloseButton: false, backdropCancel: false }} />
</>
);
}Either component is fine — BaseDialogRenderer simply sets the dialog defaults for you so consumers of useHookDialog() don't have to provide them on each call.
Import Styles of Sub Modules
- Import the upstream package to let it inject styles for you:
import '@rokku-x/react-hook-modal';- Or import the upstream modal CSS (provided by
@rokku-x/react-hook-modal) at the project root:
import '@rokku-x/react-hook-dialog/react-hook-modal.css';
OR
import '@rokku-x/react-hook-modal/react-hook-modal.css';Choose the option that fits your build setup (explicit CSS import or automatic injection).
2. Use useHookDialog Hook
import { useHookDialog } from '@rokku-x/react-hook-dialog';
function MyComponent() {
const [requestDialog] = useHookDialog();
const handleConfirm = async () => {
const result = await requestDialog({
title: 'Confirm Action',
content: 'Are you sure you want to proceed?',
actions: [[
{ title: 'Cancel', isCancel: true },
{ title: 'Confirm', variant: 'primary' }
]]
});
console.log('User chose:', result);
};
return <button onClick={handleConfirm}>Open Dialog</button>;
}API Reference
useHookDialog
Main hook for displaying confirmation dialogs and alerts.
Parameters
useHookDialog(defaultConfig?: UseHookDialogConfig)| Parameter | Type | Description |
|---|---|---|
| defaultConfig | UseHookDialogConfig | Optional default configuration applied to all dialogs |
Returns
[requestDialog, getContext]| Return Value | Type | Description |
|---|---|---|
| requestDialog | (config: ConfirmConfig) => RequestDialogReturnType<ValidValue> | Open a dialog and receive a result. The return value is an augmented Promise (see RequestDialogReturnType<T>). |
| getContext | (id: string) => DialogInstanceContext | Retrieve the runtime context for an open dialog by its id (useful if you only have the id). |
// Augmented promise returned by `requestDialog`
type RequestDialogReturnType<T> = Promise<T> & { id: string; context: DialogInstanceContext };Important: awaiting the returned Promise resolves with the dialog result (
T). Theidandcontextproperties are available immediately after callingrequestDialog(...), allowing programmatic control while the dialog is open.
Dialog Context & Force Functions ✅
The augmented Promise exposes two helpers:
id: string— unique identifier for the dialog instancecontext: DialogInstanceContext— runtime control helpers
DialogInstanceContext methods:
| Method | Signature | Description |
|---|---|---|
| forceCancel | forceCancel(forceReject?: boolean = true): void | Close the dialog as a cancellation. Default: forceReject = true (the promise will be rejected by default). If set to false, the dialog follows the dialog's rejectOnCancel setting. |
| forceAction | forceAction(action: ModalAction): void | Programmatically trigger the specified action (resolves/rejects according to the action and dialog config). |
| forceDefault | forceDefault(): void | Trigger the dialog's default action (first action marked with isFocused). Throws if no default is defined. |
Quick example:
const [requestDialog, getContext] = useHookDialog();
const actions: ModalAction[][] = [[ { title: 'Cancel', isCancel: true }, { title: 'OK', value: true, isFocused: true } ]];
const p = requestDialog({
title: 'Confirm',
content: 'Proceed?',
actions
});
// id available immediately
console.log('dialog id:', p.id);
// cancel programmatically (rejects by default)
p.context.forceCancel();
// trigger a specific action by referencing the action in the array
getContext(p.id).forceAction(actions[0][1]); // triggers `okAction`
// or
p.context.forceAction(actions[0][0]); // triggers `cancelAction`Tip: use
forceAction(...)when you want to trigger a specific action object. UseforceDefault()to trigger the focused/default action (if defined). The returned Promise still resolves with the action'svalue(or rejects when cancelled).
Default Config Options
| Property | Type | Default | Description |
|---|---|---|---|
| backdropCancel | boolean | false | Allow closing via backdrop click |
| rejectOnCancel | boolean | true | Reject promise on cancel instead of resolving |
| defaultCancelValue | ValidValue | undefined | Value to return/reject on cancel |
| showCloseButton | boolean | false | Show X close button |
| classNames | DialogClassNames | undefined | Custom CSS classes |
| styles | DialogStyles | undefined | Custom inline styles |
| variantStyles | DialogVariantStyles | undefined | Custom variant button styles |
ConfirmConfig
Configuration for individual dialog calls.
| Property | Type | Description |
|---|---|---|
| title | React.ReactNode | Dialog title (string or React element) |
| content | React.ReactNode | Dialog content (string or React element) |
| actions | ModalAction[][] | Array of action button rows |
| backdropCancel | boolean | Allow closing via backdrop click |
| rejectOnCancel | boolean | Reject promise on cancel |
| defaultCancelValue | ValidValue | Value to return/reject on cancel |
| showCloseButton | boolean | Show X close button |
| classNames | DialogClassNames | Custom CSS classes for elements |
| styles | DialogStyles | Custom inline styles for elements |
| variantStyles | DialogVariantStyles | Custom variant button styles |
| isReturnSubmit | boolean | When true and content is a <form>, clicking a submit action returns the serialized form values as the dialog result |
ModalAction
Individual action button configuration.
| Property | Type | Required | Description |
|---|---|---|---|
| title | React.ReactNode | ✓ | Button label (string or React element) |
| value | ValidValue | | Value to return when clicked |
| isCancel | boolean | | Treat as cancel button (respects rejectOnCancel) |
| isOnLeft | boolean | | Position button on left side of row |
| isFocused | boolean | | Request initial focus when the dialog opens (highest focus priority) |
| isSubmit | boolean | | Render as type="submit" and trigger form submit if content is a <form> |
| noActionReturn | boolean | | Run onClick but do not perform default dialog action (handleAction) — useful for custom flows |
| variant | ModalVariant | | Visual style variant |
| className | string | | Additional CSS class applied to the button |
| style | React.CSSProperties | | Custom inline styles applied to the button (highest style priority) |
| onClick | ((event, action) => void) \| (() => void) | | Click handler called before default handling |
ModalVariant
Available button variants:
type ModalVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'warning' | 'info' | 'neutral';| Variant | Color | Text Color |
|---|---|---|
| primary | Blue (#2563eb) | White |
| secondary | Gray (#e5e7eb) | Black |
| danger | Red (#dc2626) | White |
| success | Green (#16a34a) | White |
| warning | Amber (#f59e0b) | Black |
| info | Sky (#0ea5e9) | White |
| neutral | Gray (#6b7280) | White |
Action Flags — Priority & Behavior 🔀
- Focus priority:
isFocused(per-action) > close button (if shown) > first action button > dialog container - Click ordering:
action.onClickis executed first. Then:- If
isSubmitis true andisReturnSubmitis also enabled (andcontentis a<form>), the form is serialized and its values are returned as the dialog result immediately (no native submit is triggered). - Else if
isSubmitis true, the form'srequestSubmit()is invoked (useful for native validation flows). - Else if
noActionReturnis true, the default dialog action is skipped (use to perform custom flows and close the dialog manually). - Otherwise
handleActionis called and the dialog resolves/rejects using the action'svalue.
- If
- isCancel: marks a cancel action — dialog resolves or rejects according to
rejectOnCancelanddefaultCancelValuesettings. - Placement:
isOnLeftpositions the action on the left side; other actions render on the right. - Style & class precedence (lowest → highest):
- built-in
baseVariantStyles(library defaults) ConfirmConfig.variantStyles(per-call variant overrides)ConfirmConfig.styles.actionButton(per-call default action button styles)ModalAction.style(per-action inline style — highest precedence)classNamevalues are appended so per-actionclassNameandConfirmConfig.classNames.actionButtonboth apply (per-action classes appear last).
- built-in
DialogClassNames
Customize CSS classes for all elements:
| Property | Type | Description |
|---|---|---|
| backdrop | string | Backdrop overlay |
| dialog | string | Dialog container |
| closeButton | string | Close button |
| title | string | Title element |
| content | string | Content container |
| actions | string | Actions container |
| actionsRow | string | Individual action row |
| actionButton | string | Action button |
DialogStyles
Customize inline styles for all elements:
| Property | Type | Description |
|---|---|---|
| backdrop | React.CSSProperties | Backdrop overlay styles |
| dialog | React.CSSProperties | Dialog container styles |
| closeButton | React.CSSProperties | Close button styles |
| title | React.CSSProperties | Title element styles |
| content | React.CSSProperties | Content container styles |
| actions | React.CSSProperties | Actions container styles |
| actionsRow | React.CSSProperties | Action row styles |
| actionButton | React.CSSProperties | Action button styles |
Components
BaseDialogRenderer 🔧
A convenience wrapper around the upstream BaseModalRenderer that accepts a defaultConfig prop allowing you to specify default UseHookDialogConfig for all dialogs created by useHookDialog().
import { BaseDialogRenderer } from '@rokku-x/react-hook-dialog';
<BaseDialogRenderer defaultConfig={{ showCloseButton: false, backdropCancel: false }} />Note:
BaseModalRendererfrom@rokku-x/react-hook-modalcan still be used directly if you prefer — this wrapper only addsdefaultConfigconvenience.
Multiple renderer instances 🔁
You can mount multiple modal renderers (either BaseDialogRenderer from this package or the upstream BaseModalRenderer) and give each a unique id. Use the instanceId option (in the hook's defaultConfig or per-call ConfirmConfig) to target which renderer/store should manage the dialog.
- Mount two renderers:
<BaseDialogRenderer id="primary" defaultConfig={{ showCloseButton: false }} />
<BaseDialogRenderer id="secondary" defaultConfig={{ showCloseButton: true }} />- Use the hook with a default instance:
const [requestDialog] = useHookDialog({ instanceId: 'secondary' });
await requestDialog({ title: 'Settings' });- Or target a renderer per call:
const [requestDialog] = useHookDialog();
await requestDialog({ title: 'Switch', instanceId: 'primary' });- Programmatic store access:
import storeDialog from '@rokku-x/react-hook-dialog';
const primaryStore = storeDialog('primary'); // returns the zustand store hook
const state = primaryStore.getState();
primaryStore.setState({ rendererDefaultConfig: { /* ... */ } });Note: The default instance id is
"default". Make sure you mount a renderer with the sameidif you want dialogs to be visible. If a renderer with the requested id is not mounted, the store will still be created and you can use it programmatically, but dialogs will not be rendered until a matching renderer is mounted.
Backdrop
Overlay component that wraps dialog windows.
<Backdrop
onClick={() => handleClose()}
className="custom-backdrop"
style={{ backdropFilter: 'blur(5px)' }}
>
{children}
</Backdrop>DialogWindow
Main dialog container component.
<DialogWindow
className="custom-dialog"
style={{ backgroundColor: '#f5f5f5' }}
>
{children}
</DialogWindow>Examples
Example 1: Basic Confirmation Dialog
import { useHookDialog } from '@rokku-x/react-hook-dialog';
function DeleteConfirm() {
const [requestDialog] = useHookDialog();
const handleDelete = async () => {
const result = await requestDialog({
title: 'Delete Item?',
content: 'This action cannot be undone.',
actions: [[
{ title: 'Cancel', isCancel: true, variant: 'secondary' },
{ title: 'Delete', variant: 'danger', value: true }
]]
});
if (result === true) {
console.log('Item deleted!');
}
};
return <button onClick={handleDelete}>Delete</button>;
}
Example 2: Multiple Action Rows
const [requestDialog] = useHookDialog();
await requestDialog({
title: 'Choose Action',
content: 'What would you like to do?',
actions: [
[{ title: 'Back', isOnLeft: true, variant: 'secondary' }],
[
{ title: 'Cancel', isCancel: true },
{ title: 'Save', variant: 'primary' }
]
]
});
Example 3: Custom Styling
const [requestDialog] = useHookDialog({
styles: {
dialog: {
borderRadius: '20px',
backgroundColor: '#f9fafb'
},
actionButton: {
fontWeight: 'bold'
}
},
classNames: {
dialog: 'my-custom-dialog'
}
});
await requestDialog({
title: 'Styled Dialog',
content: 'This dialog has custom styles'
});
Example 4: Custom Button Variants
const [requestDialog] = useHookDialog({
variantStyles: {
primary: {
backgroundColor: '#7c3aed', // Purple
color: '#fff'
}
}
});
await requestDialog({
title: 'Custom Colors',
actions: [[
{ title: 'Confirm', variant: 'primary' }
]]
});
Example 5: Button Click Handlers
await requestDialog({
title: 'Action Dialog',
actions: [[
{
title: 'Log to Console',
onClick: (e) => console.log('Button clicked!'),
variant: 'info'
},
{
title: 'Proceed',
variant: 'primary'
}
]]
});
Example 6: Rich Content
await requestDialog({
title: <span style={{ color: 'blue' }}>Custom <strong>Title</strong></span>,
content: (
<div>
<p>This dialog has rich content:</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
),
actions: [[
{ title: 'OK', variant: 'primary' }
]]
});
Example 7: Default Configuration
const [requestDialog] = useHookDialog({
showCloseButton: false,
backdropCancel: false,
styles: {
dialog: { maxWidth: '500px' }
}
});
// All subsequent dialogs will use these defaults
await requestDialog({
title: 'Will use defaults',
content: 'No close button, backdrop click disabled'
});
Example 8: Alert Dialog
async function showAlert(message: string) {
const [requestDialog] = useHookDialog();
await requestDialog({
title: 'Alert',
content: message,
actions: [[
{ title: 'OK', variant: 'primary' }
]]
});
}
showAlert('Operation completed successfully!');
Example 9: Multiple Choice with Different Values
const [requestDialog] = useHookDialog();
const handleSaveOptions = async () => {
const result = await requestDialog({
title: 'Save Options',
content: 'How would you like to save?',
actions: [[
{ title: 'Cancel', isCancel: true },
{ title: 'Save Draft', variant: 'secondary', value: 'draft' },
{ title: 'Publish', variant: 'primary', value: 'publish' }
]]
});
if (result === 'draft') {
console.log('Saving as draft...');
} else if (result === 'publish') {
console.log('Publishing...');
} else {
console.log('Cancelled');
}
};
Example 10: Numeric Rating Dialog
const [requestDialog] = useHookDialog();
const handleRating = async () => {
const rating = await requestDialog({
title: 'Rate Your Experience',
content: 'How would you rate our service?',
actions: [
[
{ title: '1 Star', variant: 'danger', value: 1 },
{ title: '2 Stars', variant: 'warning', value: 2 },
{ title: '3 Stars', variant: 'neutral', value: 3 }
],
[
{ title: '4 Stars', variant: 'info', value: 4 },
{ title: '5 Stars', variant: 'success', value: 5 }
]
],
showCloseButton: false,
backdropCancel: false
});
console.log(`User rated: ${rating} stars`);
// Send rating to API
};
Example 11: Conditional Actions Based on Result
const [requestDialog] = useHookDialog();
const handleFileOperation = async () => {
const action = await requestDialog({
title: 'File Actions',
content: 'What would you like to do with this file?',
actions: [[
{ title: 'Download', variant: 'info', value: 'download' },
{ title: 'Share', variant: 'primary', value: 'share' },
{ title: 'Delete', variant: 'danger', value: 'delete', isOnLeft: true }
]]
});
switch (action) {
case 'download':
// Download file logic
window.location.href = '/api/download/file.pdf';
break;
case 'share':
// Open share dialog
await requestDialog({
title: 'Share File',
content: 'File link copied to clipboard!',
actions: [[{ title: 'OK', variant: 'primary' }]]
});
break;
case 'delete':
// Confirm deletion
const confirm = await requestDialog({
title: 'Confirm Delete',
content: 'Are you sure? This cannot be undone.',
actions: [[
{ title: 'Cancel', isCancel: true },
{ title: 'Delete', variant: 'danger', value: true }
]]
});
if (confirm) {
console.log('File deleted');
}
break;
}
};
Example 12: Handle Cancel vs Reject
const [requestDialog] = useHookDialog({
rejectOnCancel: true // Reject promise on cancel
});
const handleWithErrorHandling = async () => {
try {
const result = await requestDialog({
title: 'Important Action',
content: 'This requires your confirmation.',
actions: [[
{ title: 'Cancel', isCancel: true },
{ title: 'Proceed', variant: 'primary', value: 'proceed' }
]]
});
if (result === 'proceed') {
console.log('User proceeded');
// Perform action
}
} catch (error) {
console.log('User cancelled or closed dialog');
// Handle cancellation
}
};
Example 13: Form Submission with Validation
const [requestDialog] = useHookDialog();
const handleFormSubmit = async (formData: any) => {
const action = await requestDialog({
title: 'Review Changes',
content: (
<div>
<p>You are about to submit the following changes:</p>
<ul>
<li>Name: {formData.name}</li>
<li>Email: {formData.email}</li>
</ul>
</div>
),
actions: [[
{ title: 'Edit', variant: 'secondary', value: 'edit' },
{ title: 'Cancel', isCancel: true },
{ title: 'Submit', variant: 'success', value: 'submit' }
]]
});
if (action === 'submit') {
// Submit form
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(formData)
});
await requestDialog({
title: 'Success',
content: 'Your changes have been saved!',
actions: [[{ title: 'OK', variant: 'success' }]]
});
} else if (action === 'edit') {
// Return to form
console.log('User wants to edit');
}
};
Example 14: Boolean Result with Custom Values
const [requestDialog] = useHookDialog();
const handleLogout = async () => {
const shouldLogout = await requestDialog({
title: 'Confirm Logout',
content: 'Are you sure you want to log out?',
actions: [[
{ title: 'Stay Logged In', variant: 'secondary', value: false },
{ title: 'Log Out', variant: 'danger', value: true }
]]
});
if (shouldLogout) {
// Perform logout
sessionStorage.clear();
window.location.href = '/login';
}
};
Example 15: Form Dialog Returning Values (isReturnSubmit)
const [requestDialog] = useHookDialog();
async function openProfileDialog() {
const result = await requestDialog({
title: 'Edit Profile',
content: (
<form>
<label>
Name
<input name="name" defaultValue="Alice" />
</label>
<label>
Email
<input name="email" defaultValue="[email protected]" />
</label>
</form>
),
actions: [[
{ title: 'Cancel', isCancel: true },
// `isSubmit` triggers native submit; when `isReturnSubmit` is enabled on the dialog config, the dialog returns the form values object
{ title: 'Save', isSubmit: true }
]],
isReturnSubmit: true
});
if (result && typeof result === 'object') {
// `result` is the object built from the submitted form
console.log('Form values:', result);
// e.g. result.name, result.email
}
}
Note:
isReturnSubmitoverridesnoActionReturnand returns the serialized form values as the actionvalue.isSubmitstill triggersrequestSubmit()to allow native validation flows.
Types
ValidValue
type ValidValue = string | number | boolean | undefined;The type of value returned from dialog actions.
DialogVariantStyles
type DialogVariantStyles = Partial<Record<ModalVariant, React.CSSProperties>>;Custom styles for each variant type.
Accessibility
- Backdrop close can be enabled with
backdropCancel: true - Close button can be shown with
showCloseButton: true - All buttons are keyboard accessible
- ARIA labels provided for interactive elements
- Supports custom ARIA attributes via className injection
Best Practices
- Mount
BaseModalRendererat root level - Required for modals to render - Use default configs for consistency - Set common styles/behaviors once
- Provide meaningful button labels - Users should know what each button does
- Use appropriate variants - Use
dangerfor destructive actions,successfor confirmations - Keep content concise - Dialogs should be focused and brief
- Handle both resolve and reject - Account for cancellation scenarios
- Use
isOnLeftfor secondary actions - Helps with visual hierarchy - Customize responsibly - Maintain accessibility and usability standards
Troubleshooting
Dialog not appearing
- Ensure
BaseModalRendereris mounted at root level - Check that
useHookDialog()is called within the component tree
Styles not applying
- Verify className/style props are passed to
ConfirmConfig - Check CSS specificity - inline styles take precedence
- Use browser dev tools to inspect applied styles
Promise never resolves
- Ensure action buttons have appropriate
valueor are configured as cancel buttons - Check that action click handlers don't prevent default behavior
Bundle Size
- ESM : ~6.69 kB gzipped (13.10 kB raw)
- CJS : ~7.35 kB gzipped (14.14 kB raw)
Measured with Vite build for the current branch.
License
MIT
