dialogkit
v0.0.2
Published
Dialog component and helpers for React
Downloads
0
Readme
dialogkit
A powerful and flexible dialog system for React with built-in animations and full TypeScript support.
Features
- 🎯 Fully Typed - Complete TypeScript generics support with type inference (
createDialog<TProps, TResult>) - 🎨 Customizable - Complete control over styling and behavior
- ⚡ Animated - Built-in animation presets (scale, fade, slide)
- 📱 Responsive - Pre-configured variants (bottom sheet, side sheet, prompts)
- 🎣 Hook-based - Simple and intuitive React hooks API
- 🔒 Accessible - Keyboard navigation (ESC to close) and focus management
Installation
# pnpm (recommended)
pnpm add dialogkit
# npm
npm install dialogkit
# yarn
yarn add dialogkitNote:
@dialogkit/coreis automatically installed as a dependency - no need to install it separately.
API
Generic Signature
All dialogs are created using TypeScript generics for complete type safety:
createDialog<TProps, TResult>(
render: (controller: DialogController<TProps, TResult>) => React.ReactNode,
config?: CreateDialogConfig
): DialogHook<TProps, TResult>Where:
TProps- The type of properties passed when opening the dialogTResult- The type of data returned when the dialog is submitted
Basic Usage
Creating a Simple Dialog
Fully typed with TypeScript generics - Use createDialog<TProps, TResult> for complete type safety:
import { createDialog } from 'dialogkit';
// Generic signature: createDialog<TProps, TResult>
const useConfirmDialog = createDialog<
{ message: string }, // Props type
boolean // Return type
>(({ props, submit, cancel }) => (
<div className="p-6 bg-white rounded-lg shadow-xl">
<h2 className="text-xl font-bold mb-4">Confirm</h2>
<p className="mb-6">{props.message}</p>
<div className="flex gap-2">
<button onClick={() => submit(true)}>Yes</button>
<button onClick={() => cancel()}>No</button>
</div>
</div>
));
// Use in component - fully typed!
function MyComponent() {
const { open } = useConfirmDialog();
const handleAction = async () => {
const result = await open({ message: 'Are you sure?' });
// result is fully typed based on your generic parameters
if (result.status === 'submitted') {
console.log('User confirmed:', result.data); // data is typed as boolean
} else if (result.status === 'cancelled') {
console.log('User cancelled');
}
};
return <button onClick={handleAction}>Delete</button>;
}Pre-configured Dialog Variants
Bottom Sheet
Perfect for mobile-style bottom sheets that slide up from the bottom.
import { createDialog, DialogConfigs } from 'dialogkit';
// Fully typed with generics - void for dialogs that don't return data
const useBottomSheet = createDialog<
{ title: string }, // Props type
void // No return data
>(
({ props, cancel }) => (
<div className="w-full bg-white p-6 rounded-t-xl">
<h2>{props.title}</h2>
<button onClick={cancel}>Close</button>
</div>
),
DialogConfigs.bottomSheet
);
// Usage
const { open } = useBottomSheet();
await open({ title: 'Settings' });Side Sheet
A drawer-style panel that slides in from the right.
import { createDialog, DialogConfigs } from 'dialogkit';
// Typed with createDialog<TProps, TResult>
const useSideSheet = createDialog<
{ content: string }, // Props type
void // No return data
>(
({ props, cancel }) => (
<div className="h-full w-96 bg-white p-6">
<h2>Details</h2>
<p>{props.content}</p>
<button onClick={cancel}>Close</button>
</div>
),
DialogConfigs.sideSheet
);
// Usage
const { open } = useSideSheet();
await open({ content: 'Detailed information here' });Prompt Dialog
Top-aligned dialogs perfect for notifications and alerts.
import { createDialog, DialogConfigs } from 'dialogkit';
const usePrompt = createDialog<{ message: string }, void>(
({ props, cancel }) => (
<div className="bg-white p-4 rounded-lg shadow-lg">
<p>{props.message}</p>
<button onClick={cancel}>Dismiss</button>
</div>
),
DialogConfigs.prompt
);
// Usage
const { open } = usePrompt();
await open({ message: 'Action completed successfully!' });Custom Animations
Using Built-in Animations
import { createDialog, animations } from 'dialogkit';
// Fade animation
const useFadeDialog = createDialog<{}, void>(
({ cancel }) => (
<div className="dialog">
<button onClick={cancel}>Close</button>
</div>
),
{
animation: animations.fade,
}
);
// Slide animations
const useSlideDialog = createDialog<{}, void>(({ cancel }) => <div>Content</div>, {
animation: animations.slideUp, // or slideDown, slideLeft, slideRight
});Custom Animation Configuration
import { createDialog } from 'dialogkit';
import { gsap } from 'gsap';
const useCustomDialog = createDialog<{}, void>(({ cancel }) => <div>Content</div>, {
animation: {
duration: 400,
enter: (element) => {
gsap.fromTo(element, { opacity: 0, y: -50 }, { opacity: 1, y: 0, duration: 0.4 });
},
exit: (element) => {
return gsap.to(element, {
opacity: 0,
y: -50,
duration: 0.4,
});
},
},
});Configuration Options
CreateDialogConfig
interface CreateDialogConfig {
// Backdrop configuration
backdrop?: {
backgroundColor?: string;
backdropFilter?: string;
zIndex?: number;
display?: 'flex' | 'block';
justifyContent?: 'center' | 'flex-start' | 'flex-end' | 'space-between';
alignItems?: 'center' | 'flex-start' | 'flex-end' | 'stretch';
customStyles?: React.CSSProperties;
};
// Behavior options
closeOnOutsideClick?: boolean; // Default: true
closeOnEscape?: boolean; // Default: true
lockBodyScroll?: boolean; // Default: true
// Animation configuration
animation?: AnimationConfig;
}Custom Backdrop
const useCustomDialog = createDialog<{}, void>(({ cancel }) => <div>Content</div>, {
backdrop: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
backdropFilter: 'blur(4px)',
zIndex: 1000,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
customStyles: {
padding: '20px',
},
},
});Controller API
The controller passed to your render function provides these methods:
interface OverlayController<TProps, TResult> {
// The props passed when opening the dialog
props: TProps;
// Submit the dialog with a result
submit: (data: TResult) => void;
// Cancel the dialog (no result)
cancel: () => void;
// Close the dialog (alias for cancel)
close: () => void;
}Result Handling
The open() method returns a promise that resolves to a result object:
type DialogResult<TResult> = { status: 'submitted'; data: TResult } | { status: 'cancelled' };
// Example usage
const result = await open({ message: 'Confirm?' });
if (result.status === 'submitted') {
console.log('Submitted with data:', result.data);
} else {
console.log('Cancelled');
}Available Animation Presets
import { animations } from 'dialogkit';
animations.scale; // Scale up/down (default)
animations.fade; // Fade in/out
animations.slideUp; // Slide up from bottom
animations.slideDown; // Slide down from top
animations.slideLeft; // Slide in from right
animations.slideRight; // Slide in from leftPre-configured Dialog Configs
import { DialogConfigs } from 'dialogkit';
DialogConfigs.default; // Centered, scale animation
DialogConfigs.bottomSheet; // Bottom-aligned, slide up
DialogConfigs.sideSheet; // Right-aligned, slide left
DialogConfigs.prompt; // Top-aligned, slide downAdvanced Examples
Form Dialog with Validation
import { createDialog } from 'dialogkit';
import { useState } from 'react';
interface FormData {
name: string;
email: string;
}
const useFormDialog = createDialog<{}, FormData>(({ submit, cancel }) => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleSubmit = () => {
if (!name || !email) {
setError('All fields are required');
return;
}
submit({ name, email });
};
return (
<div className="dialog">
<h2>User Information</h2>
{error && <p className="error">{error}</p>}
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
type="email"
/>
<button onClick={handleSubmit}>Submit</button>
<button onClick={cancel}>Cancel</button>
</div>
);
});Multi-step Dialog
import { createDialog } from 'dialogkit';
import { useState } from 'react';
const useWizardDialog = createDialog<{}, { step1: string; step2: string }>(({ submit, cancel }) => {
const [step, setStep] = useState(1);
const [data, setData] = useState({ step1: '', step2: '' });
const handleFinish = () => {
submit(data);
};
return (
<div className="wizard">
{step === 1 && (
<div>
<h2>Step 1</h2>
<input value={data.step1} onChange={(e) => setData({ ...data, step1: e.target.value })} />
<button onClick={() => setStep(2)}>Next</button>
<button onClick={cancel}>Cancel</button>
</div>
)}
{step === 2 && (
<div>
<h2>Step 2</h2>
<input value={data.step2} onChange={(e) => setData({ ...data, step2: e.target.value })} />
<button onClick={() => setStep(1)}>Back</button>
<button onClick={handleFinish}>Finish</button>
</div>
)}
</div>
);
});Confirmation Dialog with Async Operations
import { createDialog } from 'dialogkit';
import { useState } from 'react';
const useAsyncDialog = createDialog<{ itemId: string }, boolean>(({ props, submit, cancel }) => {
const [loading, setLoading] = useState(false);
const handleConfirm = async () => {
setLoading(true);
try {
await fetch(`/api/items/${props.itemId}`, { method: 'DELETE' });
submit(true);
} catch (error) {
console.error(error);
setLoading(false);
}
};
return (
<div className="dialog">
<h2>Delete Item?</h2>
<p>This action cannot be undone.</p>
<button onClick={handleConfirm} disabled={loading}>
{loading ? 'Deleting...' : 'Delete'}
</button>
<button onClick={cancel} disabled={loading}>
Cancel
</button>
</div>
);
});TypeScript
The library is fully typed and built with TypeScript. Use generics for complete type safety:
Generic Signature
createDialog<TProps, TResult>(
render: (controller: DialogController<TProps, TResult>) => React.ReactNode,
config?: CreateDialogConfig
)Full Type Safety Example
// Define your types
interface EditUserProps {
userId: number;
initialName: string;
initialEmail: string;
}
interface EditUserResult {
updated: boolean;
name: string;
email: string;
}
// Create fully typed dialog with createDialog<TProps, TResult>
const useEditUserDialog = createDialog<EditUserProps, EditUserResult>(
({ props, submit, cancel }) => {
// props is fully typed as EditUserProps
const [name, setName] = useState(props.initialName);
const [email, setEmail] = useState(props.initialEmail);
return (
<div>
<h2>Edit User {props.userId}</h2>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button onClick={() => submit({ updated: true, name, email })}>Save</button>
<button onClick={cancel}>Cancel</button>
</div>
);
}
);
// Usage with complete type inference
function MyComponent() {
const { open } = useEditUserDialog();
const handleEdit = async () => {
const result = await open({
userId: 123,
initialName: 'John',
initialEmail: '[email protected]',
});
// TypeScript knows the exact shape of result
if (result.status === 'submitted') {
// result.data is typed as EditUserResult
console.log(result.data.updated); // boolean
console.log(result.data.name); // string
console.log(result.data.email); // string
}
};
return <button onClick={handleEdit}>Edit User</button>;
}Simple Example with Primitives
// Simple types work great too
const useConfirm = createDialog<
{ message: string }, // Props
boolean // Result
>(({ props, submit }) => (
<div>
<p>{props.message}</p>
<button onClick={() => submit(true)}>Yes</button>
<button onClick={() => submit(false)}>No</button>
</div>
));Exported Types
import type { DialogController, DialogResult, AnimationConfig } from 'dialogkit';Peer Dependencies
react>= 16.8.0react-dom>= 16.8.0
Dependencies
@dialogkit/core- Core animation and overlay systemgsap- Animation library (via @dialogkit/core)
License
MIT
Contributing
Contributions are welcome! Please see the main repository for contributing guidelines.
Related Packages
- @dialogkit/core - Core components and animation system
Support
For issues and questions, please visit the GitHub repository.
