ntpopups
v1.3.7
Published
Ease and powerful popup library for React
Maintainers
Readme
ntPopups 🚀
Modern, powerful, and easy-to-use popup library for React
Create elegant, fully customizable, and responsive popups with theme support, internationalization, and custom components—all in a simple yet powerful way.
Live Demo • Full Documentation • GitHub
✨ Features
- 🎨 Theme Support - Built-in light and dark themes
- 🌍 Internationalization - Multi-language support (EN, PT-BR)
- 📱 Fully Responsive - Perfect on mobile and desktop
- 🎭 Ready-to-Use Popups - Generic alerts, confirmations, forms, image cropper, and more
- 🔧 Highly Customizable - Create your own popup components
- ⚡ Lightweight & Fast - Optimized with zero extra dependencies
- ♿ Accessible - WCAG 2.1 compliant with keyboard navigation
📦 Installation
npm install ntpopupsOr with Yarn:
yarn add ntpopups🚀 Quick Start
React Setup
Wrap your application with NtPopupProvider:
// App.jsx
import { NtPopupProvider } from 'ntpopups';
import 'ntpopups/dist/styles.css';
function App() {
return (
<NtPopupProvider language="en" theme="white">
{/* Your app content */}
</NtPopupProvider>
);
}
export default App;Next.js Setup
App Router
Create a Client Component for the provider:
// components/Providers.jsx
'use client';
import { NtPopupProvider } from 'ntpopups';
import 'ntpopups/dist/styles.css';
export default function Providers({ children }) {
return (
<NtPopupProvider language="en" theme="white">
{children}
</NtPopupProvider>
);
}Use it in your root layout:
// app/layout.jsx
import Providers from '@/components/Providers';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Pages Router
// pages/_app.jsx
import { NtPopupProvider } from 'ntpopups';
import 'ntpopups/dist/styles.css';
function MyApp({ Component, pageProps }) {
return (
<NtPopupProvider language="en" theme="white">
<Component {...pageProps} />
</NtPopupProvider>
);
}
export default MyApp;🎯 Basic Usage
Import the hook and start creating popups:
import useNtPopups from 'ntpopups';
function MyComponent() {
const { openPopup } = useNtPopups();
const handleClick = () => {
openPopup('generic', {
data: {
title: 'Hello!',
message: 'This is a simple popup.',
}
});
};
return <button onClick={handleClick}>Open Popup</button>;
}🧩 Built-in Popups
Generic - Simple Message
Display informational messages, warnings, or notifications.
openPopup('generic', {
data: {
title: 'Warning',
message: 'This is an informative message.',
closeLabel: 'Got it',
icon: '⚠️'
}
});Props:
| Property | Type | Description |
|----------|------|-------------|
| title | ReactNode | Popup title |
| message | ReactNode | Main message content |
| closeLabel | ReactNode | Close button text |
| icon | ReactNode | Icon next to title (default: "ⓘ") |
Confirm - User Confirmation
Get user confirmation before critical actions.
openPopup('confirm', {
data: {
title: 'Delete Item?',
message: 'This action cannot be undone. Continue?',
cancelLabel: 'Cancel',
confirmLabel: 'Yes, delete',
confirmStyle: 'Danger',
icon: '❓',
onChoose: (confirmed) => {
if (confirmed) {
console.log('User confirmed!');
}
}
}
});Props:
| Property | Type | Description |
|----------|------|-------------|
| title | ReactNode | Popup title |
| message | ReactNode | Confirmation message |
| cancelLabel | ReactNode | Cancel button text |
| confirmLabel | ReactNode | Confirm button text |
| confirmStyle | 'default' | 'Secondary' | 'Success' | 'Danger' | Confirm button style |
| icon | ReactNode | Header icon |
| onChoose | (confirmed: boolean) => void | Callback receiving true (confirm) or false (cancel) |
Crop Image - Image Editor
Built-in image cropping with zoom, rotation, and format options.
openPopup('crop_image', {
requireAction: false,
data: {
image: fileOrUrl, // File object or URL/base64 string
format: 'circle', // 'circle' or 'square'
aspectRatio: '1:1', // Format: "width:height"
onCrop: (result) => {
console.log('Blob:', result.blob);
console.log('Base64:', result.base64);
console.log('File:', result.file);
}
}
});Props:
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| image | File | string | - | Image file or URL/base64 |
| format | 'circle' | 'square' | 'circle' | Crop format |
| aspectRatio | string | "1:1" | Aspect ratio (e.g., "16:9", "4:3") |
| minZoom | number | 1 | Minimum zoom level |
| maxZoom | number | 4 | Maximum zoom level |
| onCrop | Function | - | Callback with { blob, base64, file } |
💡 Tip: Set requireAction: true to remove the cancel button and make cropping mandatory.
Form - Dynamic Forms
Create powerful, validated forms with multiple input types.
openPopup('form', {
data: {
title: 'User Registration',
message: 'Fill in the fields below:',
doneLabel: 'Submit',
icon: '📝',
components: [
{
id: 'name',
type: 'text',
label: 'Full Name',
placeholder: 'Enter your name',
required: true,
minLength: 3,
maxLength: 50
},
{
id: 'email',
type: 'email',
label: 'Email',
placeholder: '[email protected]',
required: true,
},
{
id: 'bio',
type: 'textarea',
label: 'Biography',
placeholder: 'Tell us about yourself...',
maxLength: 200,
disableResize: true
},
{
id: 'accept',
type: 'checkbox',
label: 'I accept the terms',
defaultValue: false
},
// Inline fields (side-by-side)
[
{
id: 'city',
type: 'text',
label: 'City',
placeholder: 'New York'
},
{
id: 'state',
type: 'text',
label: 'State',
placeholder: 'NY',
maxLength: 2
}
]
],
onSubmit: (values) => {
console.log('Form data:', values);
// { name: "...", email: "...", bio: "...", accept: true, city: "...", state: "..." }
},
onChange: ({ changedComponentState, formState }) => {
const { id, value, isValid } = changedComponentState;
const { values, isValid: formValid } = formState;
console.log(`Field "${id}" changed to:`, value);
console.log('Is valid:', isValid);
console.log('All form values:', values);
console.log('Form is valid:', formValid);
}
}
});Main Props:
| Property | Type | Description |
|----------|------|-------------|
| title | ReactNode | Form title |
| message | ReactNode | Optional description |
| doneLabel | ReactNode | Submit button text |
| icon | ReactNode | Header icon |
| components | Array | List of form fields (see below) |
| onSubmit | (values: object) => void | Callback with all form values |
| onChange | (event: object) => void | Fired when any field changes |
Available Field Types
{
id: 'username',
type: 'text',
label: 'Username',
placeholder: 'Enter username...',
defaultValue: '',
required: false,
disabled: false,
minLength: 3,
maxLength: 50,
matchRegex: '^[A-Z].*' // Regex pattern
}{
id: 'description',
type: 'textarea',
label: 'Description',
placeholder: 'Enter description...',
defaultValue: '',
required: false,
disabled: false,
disableResize: false, // Prevent user resizing
minLength: 10,
maxLength: 500,
matchRegex: '.*'
}{
id: 'email',
type: 'email',
label: 'Email',
placeholder: '[email protected]',
defaultValue: '',
required: true,
disabled: false
}{
id: 'age',
type: 'number',
label: 'Age',
placeholder: 'Enter your age...',
defaultValue: 18,
required: false,
disabled: false,
min: 0,
max: 120
}{
id: 'password',
type: 'password',
label: 'Password',
placeholder: 'Enter password...',
defaultValue: '',
required: true,
minLength: 8,
maxLength: 100
}{
id: 'accept_terms',
type: 'checkbox',
label: 'I accept the terms of use',
defaultValue: false,
disabled: false,
required: true // Must be checked if required
}{
id: 'gender',
type: 'radio',
label: 'Gender',
options: ['Male', 'Female', { label: 'Other', value: 'other' }],
required: true,
defaultValue: ''
}{
id: 'country',
type: 'select',
label: 'Country',
options: ['USA', 'Canada', { label: 'Other', value: 'world' }],
required: true,
defaultValue: ''
}{
id: 'birthdate',
type: 'date',
label: 'Birthdate',
minDate: new Date('1900-01-01'),
maxDate: new Date('2024-12-31'),
required: true,
defaultValue: new Date('2000-01-01')
}{
id: 'appointment_time',
type: 'time',
label: 'Appointment Time',
required: false
}{
id: 'file_upload',
type: 'file',
label: 'Upload File',
accept: '.jpg,.png,.pdf', // Accepted file types
multiple: false, // Allow multiple files
required: false
}💡 Tips:
- Set
requireAction: trueto hide the cancel button - Submit button is disabled until all fields are valid
- Invalid fields automatically show a red border
- Supports inline fields by nesting arrays
Custom Form Components
Extend forms with your own specialized input types while maintaining full validation and state management.
Why Use Custom Components?
Perfect for:
- Specialized inputs (color picker, rich text editor, autocomplete)
- Complex UI patterns (multi-step inputs, dynamic lists)
- External library integration (date pickers, file uploaders)
- API inputs (product selectors, user pickers)
Creating a Custom Component
1. Define the Component Type
customComponents: {
"color": {
// Empty value for validation
emptyValue: null,
// Optional custom validator
validator: (value, componentData) => {
if (value && !/^#[0-9A-Fa-f]{6}$/.test(value)) {
return "Invalid hex color format";
}
return null; // Valid
},
// Your React component
render: (props) => <ColorPicker {...props} />
}
}2. Component Props
Your component receives:
{
id: string, // Unique field ID
value: any, // Current value
disabled: boolean, // Disabled state
required: boolean, // Required field
placeholder: string, // Placeholder text
changeValue: (val) => void, // Update form value
valid: boolean, // Validation state
autoFocus: boolean, // Auto-focus flag
data: object // All field config (including custom props)
}3. Implementation Example
function ColorPickerComponent(props) {
const [preview, setPreview] = useState(props.value || '#000000');
// Sync with form value
useEffect(() => {
setPreview(props.value || '#000000');
}, [props.value]);
const handleChange = (e) => {
const newColor = e.target.value;
setPreview(newColor);
props.changeValue(newColor); // Update form
};
return (
<div style={{ display: 'flex', gap: '10px', opacity: props.disabled ? 0.5 : 1 }}>
<input
type="color"
value={preview}
onChange={handleChange}
disabled={props.disabled}
style={{ border: props.valid ? '2px solid #ccc' : '2px solid red' }}
/>
<input
type="text"
value={preview}
onChange={handleChange}
disabled={props.disabled}
placeholder={props.placeholder}
style={{ border: props.valid ? '1px solid #ccc' : '1px solid red' }}
/>
</div>
);
}
// Usage in form
openPopup('form', {
data: {
title: 'Theme Settings',
customComponents: {
'color': {
emptyValue: null,
validator: (value) => {
if (value && !/^#[0-9A-Fa-f]{6}$/.test(value)) {
return 'Invalid hex color';
}
return null;
},
render: (props) => <ColorPickerComponent {...props} />
}
},
components: [
{
id: 'primary_color',
type: 'color',
label: 'Primary Color',
placeholder: '#3B82F6',
required: true,
defaultValue: '#3B82F6'
}
],
onSubmit: (values) => {
console.log('Selected color:', values.primary_color);
}
}
});Best Practices
✅ DO:
- Always use
props.changeValue()to update values - Sync local UI state with
props.valueusinguseEffect - Use
props.validfor visual feedback - Access custom props from
props.data
❌ DON'T:
- Don't manage the value in local state only
- Don't forget to handle
disabledandrequiredprops - Don't mutate
props.valuedirectly
Validation
Built-in Required Check: If required: true, the form checks if value equals emptyValue.
Custom Validator: Add additional validation rules:
validator: (value, componentData) => {
const { maxItems } = componentData;
if (value && value.length > maxItems) {
return `Maximum ${maxItems} items allowed`;
}
return null; // Valid
}Note: Custom validators run after the required check.
HTML - Custom Content
Render custom HTML or React components in a popup.
openPopup('html', {
data: {
html: <h1>Hello World!</h1>
}
});Or with access to closePopup:
openPopup('html', {
data: {
html: ({ closePopup }) => (
<div>
<h1>Custom Content</h1>
<button onClick={() => closePopup()}>Close</button>
</div>
)
}
});Props:
| Property | Type | Description |
|----------|------|-------------|
| html | ReactNode | Function | Custom content or render function |
🎨 Creating Custom Popups
Build your own popup components with full control over UI and behavior.
1. Basic Structure
export default function MyCustomPopup({
// Provided by library
closePopup, // Function to close popup
popupstyles, // Predefined CSS classes
requireAction, // Boolean - requires user action to close
// Your custom props
data = {}
}) {
return (
<>
{/* Header */}
<div className={popupstyles.header}>
<div className={popupstyles.icon}>ⓘ</div>
<span>Custom Popup Title</span>
</div>
{/* Body */}
<div className={popupstyles.body}>
<p>Your custom content here</p>
</div>
{/* Footer */}
<div className={popupstyles.footer}>
<button
className={popupstyles.baseButton}
base-button-style="0"
onClick={() => closePopup(true)}
>
Confirm
</button>
</div>
</>
);
}2. Styling System
Button Styles
Use className={popupstyles.baseButton} with these attributes:
base-button-style (string):
"0"- Primary (default)"1"- Secondary"2"- Text only"3"- Success (green)"4"- Danger (red)
base-button-no-flex (string): "true" | "false" (default)
Example:
<button
className={popupstyles.baseButton}
base-button-style="4"
base-button-no-flex="true"
>
Delete
</button>Form Elements with ntpopups-css="true"
Apply ntPopups styling to native form elements:
Supported elements:
<input>(text, email, password, number, date, time, radio)<textarea><select><a>
Additional attributes:
valid="false"- Show error state (red border)noresize="true"- Disable textarea resizing (textarea only)
Examples:
<input type="text" ntpopups-css="true" placeholder="Username" />
<input type="email" ntpopups-css="true" valid="false" />
<textarea ntpopups-css="true" noresize="true" />
<select ntpopups-css="true">
<option>Option 1</option>
</select>
<a href="#" ntpopups-css="true">Link</a>3. Complete Example
// components/popups/MyCustomPopup.jsx
import styles from './mystyles.module.css';
export default function MyCustomPopup({
closePopup,
popupstyles,
requireAction,
data: {
message = 'Default message',
customProp1,
customProp2 = 'Amazing library!',
onConfirm = () => {}
} = {}
}) {
const handleConfirm = () => {
onConfirm(customProp1 + customProp2);
closePopup(true); // true = user action
};
return (
<>
<div className={popupstyles.header}>
<div className={popupstyles.icon}>💡</div>
<span>Custom Popup</span>
</div>
<div className={popupstyles.body}>
<p>{message}</p>
<h3>Property 1: {customProp1}</h3>
<button
className={styles.myCustomButton}
onClick={() => alert(customProp2)}
>
{customProp2}
</button>
</div>
<div className={popupstyles.footer}>
{!requireAction && (
<button
className={popupstyles.baseButton}
base-button-style="1"
onClick={() => closePopup()}
>
Cancel
</button>
)}
<button
className={popupstyles.baseButton}
onClick={handleConfirm}
>
Confirm
</button>
</div>
</>
);
}💡 Tip: When requireAction = true, closePopup() only works with closePopup(true).
4. Register the Component
import { NtPopupProvider } from 'ntpopups';
import MyCustomPopup from './components/MyCustomPopup';
import AnotherCustomPopup from './components/AnotherPopup';
function App() {
return (
<NtPopupProvider
language="en"
theme="white"
customPopups={{
'my_custom': MyCustomPopup,
'another_custom': AnotherCustomPopup
}}
>
{/* Your app */}
</NtPopupProvider>
);
}5. Use Your Custom Popup
const { openPopup } = useNtPopups();
openPopup('my_custom', {
data: {
message: 'Hello from custom popup!',
customProp1: 'Value 1',
customProp2: 'Value 2',
onConfirm: (result) => {
console.log('Confirmed:', result);
}
},
requireAction: true,
maxWidth: '600px'
});⚙️ Configuration
Provider Props
<NtPopupProvider
language="en" // 'en' | 'ptbr'
theme="white" // 'white' | 'dark'
customPopups={{}} // Your custom popup components
config={{
defaultSettings: {
all: { // Applied to all popups
closeOnEscape: true,
closeOnClickOutside: true,
},
generic: { // Specific to generic popup
closeOnClickOutside: false,
timeout: 20000
},
confirm: { // Override for confirm popup
closeOnClickOutside: false
},
my_custom: { // Your custom popup defaults
requireAction: true
}
}
}}
>Popup Settings
Settings applicable to any popup (built-in or custom):
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| id | string | auto | Unique popup ID |
| closeOnEscape | boolean | true | Close on ESC key |
| closeOnClickOutside | boolean | true | Close on backdrop click |
| requireAction | boolean | false | Requires internal action to close |
| timeout | number | 0 | Auto-close after milliseconds |
| keepLast | boolean | false | Keep previous popup visible |
| allowPageBodyScroll | boolean | false | Allow page scrolling |
| interactiveBackdrop | boolean | false | Allow backdrop interactions |
| hiddenBackdrop | boolean | false | Hide backdrop |
| hiddenHeader | boolean | false | Hide header |
| hiddenFooter | boolean | false | Hide footer |
| disableAnimation | boolean | false | Disable open/close animation |
| width | string | - | CSS width (e.g., '400px') |
| maxWidth | string | - | CSS max-width (e.g., '800px') |
| minWidth | string | - | CSS min-width (e.g., '200px') |
| height | string | - | CSS height (e.g., '50dvh') |
| maxHeight | string | - | CSS max-height (e.g., '80dvh') |
| minHeight | string | - | CSS min-height (e.g., '20dvh') |
| onOpen | (id: string) => void | - | Callback on open |
| onClose | (hasAction: boolean, id: string) => void | - | Callback on close |
Example:
openPopup('generic', {
closeOnEscape: false,
requireAction: true,
timeout: 5000,
maxWidth: '400px',
onOpen: (id) => console.log('Opened:', id),
onClose: (hasAction, id) => {
console.log('Closed with action?', hasAction);
},
data: {
message: 'Auto-closes in 5 seconds'
}
});🎭 Hook API
const {
openPopup, // (type, settings) => PopupData | null
closePopup, // (id?, hasAction?) => void
updatePopup, // (id, settings) => PopupData | null
closeAllPopups, // () => void
isPopupOpen, // (id) => boolean
getPopup, // (id) => PopupData | null
popups, // PopupData[] - Array of active popups
language // 'en' | 'ptbr'
} = useNtPopups();Methods
openPopup(type, settings)
Opens a popup and returns its data (including unique ID).
const popup = openPopup('confirm', {
data: { message: 'Continue?' }
});
console.log(popup.id); // "popup_abc123"closePopup(id?, hasAction?)
Closes a specific popup or the last opened one.
closePopup(); // Close last popup, no action
closePopup(true); // Close last popup, with action
closePopup('popup_123', true); // Close specific popup, with actionupdatePopup(id, newSettings)
Updates settings of an open popup.
const popup = openPopup('generic', {
data: { message: 'Loading...' }
});
updatePopup(popup.id, {
data: { message: 'Complete!' }
});closeAllPopups()
Closes all open popups immediately.
closeAllPopups();isPopupOpen(id)
Checks if a popup is currently open.
if (isPopupOpen('my_popup')) {
console.log('Still open!');
}getPopup(id)
Retrieves data for an open popup.
const popup = getPopup('popup_123');
console.log(popup?.settings);💡 Usage Examples
Delete Confirmation
const confirmDelete = (itemId) => {
openPopup('confirm', {
data: {
title: 'Delete Item',
message: 'This action cannot be undone. Continue?',
icon: '🗑️',
confirmLabel: 'Delete',
confirmStyle: 'Danger',
onChoose: async (confirmed) => {
if (confirmed) {
await deleteItem(itemId);
openPopup('generic', {
data: { message: 'Item deleted!' },
timeout: 3000
});
}
}
},
closeOnClickOutside: false
});
};Avatar Upload & Crop
const handleAvatarUpload = (file) => {
openPopup('crop_image', {
data: {
image: file,
format: 'circle',
onCrop: async (result) => {
const formData = new FormData();
formData.append('avatar', result.file);
await api.post('/users/avatar', formData);
setAvatarUrl(result.base64);
openPopup('generic', {
data: {
title: 'Success!',
message: 'Avatar updated.',
icon: '✅'
},
timeout: 3000
});
}
},
requireAction: true
});
};Feedback Form
const openFeedbackForm = () => {
openPopup('form', {
data: {
title: 'Send Feedback',
message: 'Your opinion matters!',
icon: '💬',
components: [
{
id: 'name',
type: 'text',
label: 'Name',
required: true,
minLength: 2
},
{
id: 'email',
type: 'email',
label: 'Email',
required: true
},
{
id: 'message',
type: 'textarea',
label: 'Message',
required: true,
minLength: 10,
maxLength: 500
},
{
id: 'contact',
type: 'checkbox',label: 'You may contact me about this',
defaultValue: true
}
],
onSubmit: async (data) => {
await api.post('/feedback', data);
openPopup('generic', {
data: {
title: 'Thank you!',
message: 'Feedback sent successfully.',
icon: '🎉'
},
timeout: 4000
});
}
},
maxWidth: '600px'
});
};Multi-Step Wizard
const registrationWizard = () => {
const steps = ['personal', 'address', 'preferences'];
let currentStep = 0;
let formData = {};
const stepConfigs = {
personal: {
title: 'Personal Information (1/3)',
components: [
{
id: 'name',
type: 'text',
label: 'Full Name',
required: true,
minLength: 3
},
[
{
id: 'ssn',
type: 'text',
label: 'SSN',
required: true,
matchRegex: '^\\d{9}$'
},
{
id: 'phone',
type: 'text',
label: 'Phone',
required: true
}
]
]
},
address: {
title: 'Address (2/3)',
components: [
{
id: 'zipcode',
type: 'text',
label: 'Zip Code',
required: true,
matchRegex: '^\\d{5}$'
},
{
id: 'street',
type: 'text',
label: 'Street',
required: true
},
[
{
id: 'number',
type: 'text',
label: 'Number',
required: true
},
{
id: 'complement',
type: 'text',
label: 'Complement'
}
]
]
},
preferences: {
title: 'Preferences (3/3)',
components: [
{
id: 'newsletter',
type: 'checkbox',
label: 'Receive newsletter',
defaultValue: true
},
{
id: 'notifications',
type: 'checkbox',
label: 'Receive notifications',
defaultValue: true
},
{
id: 'notes',
type: 'textarea',
label: 'Additional Notes',
placeholder: 'Anything else...',
maxLength: 200
}
]
}
};
const openStep = (step) => {
const config = stepConfigs[step];
const isLastStep = step === 'preferences';
openPopup('form', {
id: `wizard_${step}`,
data: {
...config,
icon: '📝',
doneLabel: isLastStep ? 'Finish' : 'Next',
onSubmit: (values) => {
formData = { ...formData, ...values };
if (isLastStep) {
finishRegistration(formData);
} else {
currentStep++;
openStep(steps[currentStep]);
}
}
}
});
};
const finishRegistration = async (data) => {
await api.post('/registration', data);
openPopup('generic', {
data: {
title: 'Registration Complete!',
message: 'Welcome! Your account is ready.',
icon: '🎊'
},
timeout: 5000
});
};
openStep(steps[0]);
};Loading Indicator
const performLongAction = async () => {
const loading = openPopup('generic', {
id: 'loading_popup',
data: {
title: 'Processing...',
message: 'Please wait while we complete your request.',
icon: '⏳'
},
requireAction: true,
hiddenFooter: true
});
try {
await performOperation();
closePopup(loading.id, true);
openPopup('generic', {
data: {
title: 'Success!',
message: 'Operation completed.',
icon: '✅'
},
timeout: 3000
});
} catch (error) {
closePopup(loading.id, true);
openPopup('generic', {
data: {
title: 'Error',
message: `An error occurred: ${error.message}`,
icon: '❌'
}
});
}
};Onboarding Tour
const startOnboardingTour = () => {
const steps = [
{
title: 'Welcome! 👋',
message: "Let's take a quick tour of the main features.",
icon: '🚀'
},
{
title: 'Dashboard',
message: 'View all your metrics in real-time here.',
icon: '📊'
},
{
title: 'Settings',
message: 'Customize your experience in settings.',
icon: '⚙️'
},
{
title: 'Ready to Go!',
message: "You're all set. Enjoy!",
icon: '🎉'
}
];
let currentStep = 0;
const showStep = () => {
const step = steps[currentStep];
const isLastStep = currentStep === steps.length - 1;
openPopup('generic', {
id: 'tour_step',
data: {
title: step.title,
message: step.message,
icon: step.icon,
closeLabel: isLastStep ? 'Get Started' : 'Next'
},
onClose: (hasAction) => {
if (hasAction && !isLastStep) {
currentStep++;
setTimeout(showStep, 300);
} else if (hasAction && isLastStep) {
localStorage.setItem('tour_complete', 'true');
}
}
});
};
showStep();
};Notification with Actions
// Custom notification popup component
const NotificationPopup = ({ closePopup, popupstyles, data }) => {
const { type, title, message, actions = [] } = data;
const icons = {
success: '✅',
error: '❌',
warning: '⚠️',
info: 'ℹ️'
};
return (
<>
<div className={popupstyles.header}>
<div className={popupstyles.icon}>{icons[type]}</div>
<span>{title}</span>
</div>
<div className={popupstyles.body}>
<p>{message}</p>
</div>
<div className={popupstyles.footer}>
{actions.map((action, index) => (
<button
key={index}
className={popupstyles.baseButton}
base-button-style={action.style || "0"}
onClick={() => {
action.callback?.();
closePopup(true);
}}
>
{action.label}
</button>
))}
</div>
</>
);
};
// Register in provider
<NtPopupProvider customPopups={{ notification: NotificationPopup }}>
{/* ... */}
</NtPopupProvider>
// Usage
openPopup('notification', {
data: {
type: 'warning',
title: 'New Message',
message: 'You have a new message from John Smith.',
actions: [
{
label: 'Dismiss',
style: '1',
callback: () => console.log('Dismissed')
},
{
label: 'View Now',
callback: () => navigateTo('/messages/123')
}
]
},
timeout: 8000
});Advanced Form Validation
const createAccountForm = () => {
openPopup('form', {
data: {
title: 'Create Account',
icon: '🔐',
components: [
{
id: 'username',
type: 'text',
label: 'Username',
placeholder: 'Minimum 3 characters',
required: true,
minLength: 3,
maxLength: 20,
matchRegex: '^[a-zA-Z0-9_]+$'
},
{
id: 'email',
type: 'email',
label: 'Email',
placeholder: '[email protected]',
required: true
},
{
id: 'password',
type: 'password',
label: 'Password',
placeholder: 'Min 8 chars, 1 uppercase, 1 number',
required: true,
minLength: 8,
matchRegex: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$'
},
{
id: 'bio',
type: 'textarea',
label: 'Bio (Optional)',
placeholder: 'Tell us about yourself...',
maxLength: 150,
disableResize: true
},
[
{
id: 'terms',
type: 'checkbox',
label: 'I accept the Terms of Service',
required: true
},
{
id: 'privacy',
type: 'checkbox',
label: 'I accept the Privacy Policy',
required: true
}
]
],
onSubmit: async (data) => {
try {
await api.post('/auth/register', data);
openPopup('generic', {
data: {
title: 'Account Created!',
message: 'Check your email to verify your account.',
icon: '📧'
}
});
} catch (error) {
openPopup('generic', {
data: {
title: 'Error',
message: error.response?.data?.message || 'Failed to create account.',
icon: '❌'
}
});
}
}
},
maxWidth: '550px'
});
};🎨 Visual Customization
CSS Variables
ntPopups provides 100+ CSS variables for complete visual control. All variables use the --ntpopups-* prefix.
Typography
--ntpopups-font-family: "Segoe UI", Arial, sans-serif;
--ntpopups-font-size-base: 18px;
--ntpopups-font-size-header: 24px;
--ntpopups-font-size-button: 14px;
--ntpopups-font-weight-header: 400;
--ntpopups-font-weight-normal: 400;
--ntpopups-font-weight-semibold: 500;
--ntpopups-font-weight-bold: 700;
--ntpopups-line-height-base: 1.5;
--ntpopups-line-height-header: 1.3;Colors
Primary:
--ntpopups-color-primary: #5f54f0;
--ntpopups-color-primary-hover: #4f43f5;
--ntpopups-color-primary-active: #3f33e5;
--ntpopups-color-primary-disabled: #a39fd8;
--ntpopups-color-primary-light: #e8e6fc;Secondary:
--ntpopups-color-secondary: #2a2a2a;
--ntpopups-color-secondary-hover: #363636;
--ntpopups-color-secondary-active: #4e555b;
--ntpopups-color-secondary-disabled: #b8bfc6;
--ntpopups-color-secondary-light: #e9ecef;Semantic:
--ntpopups-color-success: #28a745;
--ntpopups-color-success-hover: #218838;
--ntpopups-color-danger: #dc3545;
--ntpopups-color-danger-hover: #c82333;
--ntpopups-color-warning: #ffc107;
--ntpopups-color-info: #17a2b8;Text:
--ntpopups-color-text: rgba(64, 64, 64, 0.95);
--ntpopups-color-text-secondary: rgba(14, 14, 14, 0.6);
--ntpopups-color-text-muted: rgba(14, 14, 14, 0.4);
--ntpopups-color-text-light: #f8f9fa;
--ntpopups-color-text-on-primary: #ffffff;Backgrounds
--ntpopups-bg-default: linear-gradient(...);
--ntpopups-bg-overlay: rgba(0, 0, 0, 0.459);
--ntpopups-bg-footer: #f0f0f0;
--ntpopups-bg-header: linear-gradient(...);
--ntpopups-bg-body: linear-gradient(...);
--ntpopups-bg-button-primary: var(--ntpopups-color-primary);
--ntpopups-bg-button-secondary: var(--ntpopups-color-secondary);Borders & Radius
--ntpopups-border-width: 1px;
--ntpopups-border-width-thick: 2px;
--ntpopups-border-style: solid;
--ntpopups-border-color: rgba(0, 0, 0, 0.07);
--ntpopups-border-radius: 10px;
--ntpopups-border-radius-sm: 5px;
--ntpopups-border-radius-lg: 15px;
--ntpopups-border-radius-xl: 20px;
--ntpopups-border-radius-button: 5px;Shadows
--ntpopups-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
--ntpopups-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.1);
--ntpopups-shadow-lg: 0 20px 50px rgba(0, 0, 0, 0.2);
--ntpopups-shadow-button: 0 2px 4px rgba(0, 0, 0, 0.1);
--ntpopups-shadow-button-hover: 0 4px 8px rgba(0, 0, 0, 0.15);Spacing
--ntpopups-spacing-xs: 5px;
--ntpopups-spacing-sm: 10px;
--ntpopups-spacing-md: 15px;
--ntpopups-spacing-lg: 20px;
--ntpopups-spacing-xl: 30px;
--ntpopups-spacing-2xl: 40px;
--ntpopups-padding-header: var(--ntpopups-spacing-lg);
--ntpopups-padding-body: var(--ntpopups-spacing-lg);
--ntpopups-padding-footer: var(--ntpopups-spacing-sm);
--ntpopups-padding-button: var(--ntpopups-spacing-md) var(--ntpopups-spacing-lg);
--ntpopups-gap-buttons: var(--ntpopups-spacing-sm);
--ntpopups-gap-header-icon: 8px;Form Inputs
--ntpopups-input-bg: #ffffff;
--ntpopups-input-border: var(--ntpopups-border-color);
--ntpopups-input-border-focus: var(--ntpopups-color-primary);
--ntpopups-input-text-color: var(--ntpopups-color-text);
--ntpopups-input-placeholder-color: var(--ntpopups-color-text-muted);
--ntpopups-input-padding: var(--ntpopups-spacing-sm) var(--ntpopups-spacing-md);
--ntpopups-input-border-radius: var(--ntpopups-border-radius-sm);Dimensions
--ntpopups-width-min: 300px;
--ntpopups-width-max: 1000px;
--ntpopups-width-default: fit-content;
--ntpopups-height-max: 90dvh;
--ntpopups-button-min-width: 80px;
--ntpopups-button-height: auto;Transitions
--ntpopups-transition-duration: 0.2s;
--ntpopups-transition-duration-fast: 0.1s;
--ntpopups-transition-duration-slow: 0.3s;
--ntpopups-transition-easing: ease-in-out;
--ntpopups-transition-easing-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);Responsive
--ntpopups-mobile-padding: 15px;
--ntpopups-mobile-font-size: 14px;How to Customize
Create a CSS file and override the variables:
/* styles/custom-ntpopups.css */
.ntpopups-overlay {
/* Brand colors */
--ntpopups-color-primary: #ff6b6b;
--ntpopups-color-primary-hover: #ff5252;
/* Typography */
--ntpopups-font-family: 'Poppins', sans-serif;
--ntpopups-font-size-base: 16px;
/* Rounded design */
--ntpopups-border-radius: 20px;
--ntpopups-border-radius-button: 10px;
/* Softer shadows */
--ntpopups-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
/* Generous spacing */
--ntpopups-padding-body: 30px;
/* Darker backdrop */
--ntpopups-bg-overlay: rgba(0, 0, 0, 0.7);
}
/* Dark theme customization */
.ntpopups-dark-theme {
--ntpopups-color-primary: #bb86fc;
--ntpopups-bg-overlay: rgba(0, 0, 0, 0.9);
--ntpopups-bg-body: #1e1e1e;
}Import in your app:
import 'ntpopups/dist/styles.css';
import './styles/custom-ntpopups.css';🎯 CSS Classes
All classes use the .ntpopups-* prefix for easy styling.
Structure Classes
.ntpopups-main /* Root container */
.ntpopups-[theme]-theme /* Theme-specific (e.g., .ntpopups-dark-theme) */
.ntpopups-overlay /* Backdrop container */
.ntpopups-container /* Popup container */
.ntpopups-header /* Header section */
.ntpopups-icon /* Header icon */
.ntpopups-body /* Body/content section */
.ntpopups-footer /* Footer section */
.ntpopups-basebutton /* Base button style */Component-Specific Classes
Buttons:
.ntpopups-basebutton /* Base button */
.ntpopups-confirm-button /* Confirm popup button */Form:
.ntpopups-form-body /* Form container */
.ntpopups-form-row /* Field row */
.ntpopups-form-component-container /* Individual field wrapper */
.ntpopups-form-message /* Form message */Image Cropper:
.ntpopups-cropimage-header /* Crop header */
.ntpopups-cropimage-main /* Main container */
.ntpopups-cropimage-container /* Canvas container */
.ntpopups-cropimage-container-grab /* Grab cursor state */
.ntpopups-cropimage-container-grabbing /* Grabbing cursor state */
.ntpopups-cropimage-full-canvas /* Full canvas */
.ntpopups-cropimage-canvas /* Crop canvas */
.ntpopups-cropimage-canvas-circle /* Circle crop canvas */
.ntpopups-cropimage-hidden-image /* Hidden image element */
.ntpopups-cropimage-zoom-section /* Zoom controls section */
.ntpopups-cropimage-zoom-controls /* Zoom controls wrapper */
.ntpopups-cropimage-zoom-slider /* Zoom slider */
.ntpopups-cropimage-zoom-icon /* Zoom icon */
.ntpopups-cropimage-zoom-icon-small /* Small zoom icon */
.ntpopups-cropimage-zoom-icon-large /* Large zoom icon */
.ntpopups-cropimage-resetbutton /* Reset button */Usage Example
/* Customize all buttons */
.ntpopups-basebutton {
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 600;
}
/* Custom header gradient */
.ntpopups-header {
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
color: white;
}
/* Increase form spacing */
.ntpopups-form-body {
gap: 25px;
}
/* Style specific popup type */
.ntpopups-overlay[data-popup-type="confirm"] .ntpopups-body {
font-size: 18px;
text-align: center;
}📱 Responsive & Accessible
Responsive Design
- Mobile-First Approach - Optimized for small screens first
- Touch-Friendly - Full gesture support (drag, pinch, zoom)
- Adaptive Components - Elements resize intelligently
Mobile Optimizations:
- Reduced padding for better space usage
- Adjusted font sizes for readability
- Minimum 44px touch targets
- Optimized scrolling behavior
Accessibility
- Keyboard Navigation - Full support (ESC, Tab, Enter, Space)
- WCAG 2.1 Compliant - Minimum AA contrast ratios
- Focus Management - Clear focus indicators
- Semantic HTML - Proper heading hierarchy
- ARIA Labels - Screen reader friendly
- No Color-Only Information - Multiple visual cues
- Zoom Support - Works up to 200% zoom
Keyboard Shortcuts:
ESC- Close popup (ifcloseOnEscapeis enabled)Tab/Shift+Tab- Navigate between interactive elementsEnter/Space- Activate buttons- Focus automatically trapped within popup
🎓 Best Practices
✅ Recommended
1. Handle Async Operations
onSubmit: async (data) => {
try {
await api.post('/save', data);
// Success handling
} catch (error) {
// Error handling
console.error('Save failed:', error);
}
}2. Validate on Server Never trust client-side validation alone.
3. Use Timeouts Wisely
// ✅ Good - Success notification
openPopup('generic', {
data: { message: 'Saved!' },
timeout: 3000
});
// ❌ Bad - Critical action
openPopup('confirm', {
data: { message: 'Delete everything?' },
timeout: 5000 // User might miss it!
});4. RequireAction for Critical Actions
// ✅ Good - Delete confirmation
openPopup('confirm', {
requireAction: true,
data: { message: 'Delete account?' }
});
// ❌ Bad - Simple notification
openPopup('generic', {
requireAction: true, // Unnecessary friction
data: { message: 'Welcome!' }
});5. Cleanup on Unmount
useEffect(() => {
return () => {
closeAllPopups(); // Clean up when component unmounts
};
}, []);6. Provide Clear Feedback
// Show loading state
const loading = openPopup('generic', {
data: { title: 'Saving...', message: 'Please wait' },
requireAction: true,
hiddenFooter: true
});
await saveData();
closePopup(loading.id, true);
// Show success
openPopup('generic', {
data: { title: 'Saved!', icon: '✅' },
timeout: 3000
});⚠️ Avoid
1. Multiple Simultaneous Popups
// ❌ Confusing for users
openPopup('generic', { keepLast: true });
openPopup('confirm', { keepLast: true });
openPopup('form', { keepLast: true });
// ✅ Use sequential flow instead
openPopup('confirm', {
data: {
onChoose: (confirmed) => {
if (confirmed) {
openPopup('form', { /* ... */ });
}
}
}
});2. Overly Long Forms
// ❌ 20 fields in one popup
// ✅ Split into multi-step wizard
// ❌ Very large forms
components: [/* 25 fields */]
// ✅ Multi-step approach
const wizard = ['step1', 'step2', 'step3'];3. Timeout on Important Actions
// ❌ User might lose important popup
openPopup('confirm', {
timeout: 5000,
data: { message: 'Confirm deletion?' }
});
// ✅ No timeout for critical actions
openPopup('confirm', {
data: { message: 'Confirm deletion?' }
});4. Missing Error Handling
// ❌ No error handling
onSubmit: async (data) => {
await api.post('/save', data);
}
// ✅ Proper error handling
onSubmit: async (data) => {
try {
await api.post('/save', data);
} catch (error) {
openPopup('generic', {
data: {
title: 'Error',
message: error.message,
icon: '❌'
}
});
}
}🔧 Troubleshooting
Popup Doesn't Open
Symptom: openPopup() returns null
Solutions:
- ✓ Verify popup type is registered in
customPopups - ✓ Check browser console for errors
- ✓ Ensure
NtPopupProviderwraps your component - ✓ Confirm you're calling
openPopupinside a component (not at module level)
Styles Not Applied
Symptom: Popup appears unstyled
Solutions:
- ✓ Import CSS:
import 'ntpopups/dist/styles.css' - ✓ Check import order (library CSS before custom CSS)
- ✓ In Next.js App Router, import in Client Component with
'use client' - ✓ Clear build cache and restart dev server
Form Doesn't Validate
Symptom: Submit button stays disabled
Solutions:
- ✓ Verify
requiredfields have values - ✓ Check
matchRegexpatterns are correct - ✓ Ensure
minLength/maxLengthconstraints are met - ✓ Remember:
disabledfields are not validated - ✓ Use
onChangeto debug validation state
Popup Won't Close
Symptom: ESC key or backdrop click doesn't work
Solutions:
- ✓ Check
closeOnEscapesetting (default:true) - ✓ Check
closeOnClickOutsidesetting (default:true) - ✓ If
requireAction: true, useclosePopup(id, true)with action - ✓ Verify no JavaScript errors preventing event handlers
Next.js Hydration Error
Symptom: "Hydration mismatch" warning in console
Solution:
// ✅ Mark provider component as Client Component
'use client';
import { NtPopupProvider } from 'ntpopups';
import 'ntpopups/dist/styles.css';
export default function Providers({ children }) {
return (
<NtPopupProvider language="en" theme="white">
{children}
</NtPopupProvider>
);
}TypeScript Errors
Symptom: Type errors with custom popups
Solution:
// Define types for your custom popup data
interface MyCustomData {
message: string;
onConfirm?: () => void;
}
openPopup('my_custom', {
data: {
message: 'Hello',
onConfirm: () => console.log('Confirmed')
} as MyCustomData
});📚 Additional Resources
- Live Demo - https://ntpopups.nemtudo.me/demo
- Full Documentation - https://ntpopups.nemtudo.me
- GitHub Repository - https://github.com/Nem-Tudo/ntPopups
- npm Package - https://www.npmjs.com/package/ntpopups
📄 License
MIT License - Free to use in personal and commercial projects.
See the LICENSE file for details.
🤝 Contributing
Contributions are welcome! Here's how you can help:
- Report Issues - Found a bug? Open an issue
- Suggest Features - Have an idea? Share it in issues
- Submit PRs - Improvements and fixes are appreciated
- Share Examples - Show off your creative use cases
Development Setup
# Clone the repository
git clone https://github.com/Nem-Tudo/ntPopups.git
# Install dependencies
npm install
# Start development server
npm run dev
# Build library
npm run build⭐ Show Your Support
If ntPopups helped you build better user experiences, please:
- ⭐ Star the repo on GitHub
- 📢 Share it with other developers
- 💬 Provide feedback tohelp improve the library
- 🐛 Report issues you encounter
- 💡 Suggest features you'd like to see
🙏 Acknowledgments
Special thanks to:
- The React community for inspiration and feedback
- All contributors who helped improve this library
- Users who reported bugs and suggested features
- Everyone who starred the project and spread the word
📞 Support & Community
- Questions? Open a GitHub Discussion
- Bug Reports GitHub Issues
- Feature Requests GitHub Issues
🎯 Common Patterns
Global Error Handler
// utils/errorHandler.js
import useNtPopups from 'ntpopups'
export function useErrorHandler() {
const { openPopup } = useNtPopups()
const handleError = (error) => {
openPopup('generic', {
data: {
title: 'Error',
message: error.message || 'An unexpected error occurred',
icon: '❌',
closeLabel: 'OK'
}
})
}
return { handleError }
}
// Usage in components
const { handleError } = useErrorHandler()
try {
await riskyOperation()
} catch (error) {
handleError(error)
}Confirmation Hook
// hooks/useConfirm.js
import { useCallback } from 'react'
import useNtPopups from 'ntpopups'
export function useConfirm() {
const { openPopup } = useNtPopups()
const confirm = useCallback((options) => {
return new Promise((resolve) => {
openPopup('confirm', {
data: {
title: options.title || 'Confirm',
message: options.message,
confirmLabel: options.confirmLabel || 'Confirm',
cancelLabel: options.cancelLabel || 'Cancel',
confirmStyle: options.style || 'default',
icon: options.icon || '❓',
onChoose: (confirmed) => resolve(confirmed)
},
closeOnClickOutside: false
})
})
}, [openPopup])
return confirm
}
// Usage
const confirm = useConfirm()
const handleDelete = async () => {
const confirmed = await confirm({
title: 'Delete Item',
message: 'This action cannot be undone',
confirmLabel: 'Delete',
style: 'Danger'
})
if (confirmed) {
await deleteItem()
}
}Toast Notifications
// utils/toast.js
import useNtPopups from 'ntpopups'
export function useToast() {
const { openPopup } = useNtPopups()
const toast = {
success: (message) => {
openPopup('generic', {
data: {
message,
icon: '✅',
closeLabel: 'OK'
},
timeout: 3000,
hiddenHeader: true
})
},
error: (message) => {
openPopup('generic', {
data: {
message,
icon: '❌',
closeLabel: 'OK'
},
timeout: 5000,
hiddenHeader: true
})
},
info: (message) => {
openPopup('generic', {
data: {
message,
icon: 'ℹ️',
closeLabel: 'OK'
},
timeout: 3000,
hiddenHeader: true
})
},
warning: (message) => {
openPopup('generic', {
data: {
message,
icon: '⚠️',
closeLabel: 'OK'
},
timeout: 4000,
hiddenHeader: true
})
}
}
return toast
}
// Usage
const toast = useToast()
toast.success('Profile updated successfully!')
toast.error('Failed to save changes')
toast.info('You have 3 new messages')
toast.warning('Your session will expire soon')📖 FAQ
Can I use ntPopups with TypeScript?
Yes! ntPopups is built with TypeScript support. All types are exported and you can use them in your project:
import useNtPopups, { PopupSettings } from 'ntpopups'
const settings: PopupSettings = {
data: {
title: 'Hello',
message: 'World'
}
}How do I change the language dynamically?
The language prop on NtPopupProvider can be changed dynamically:
const [language, setLanguage] = useState('en')
<NtPopupProvider language={language} theme="white">
<button onClick={() => setLanguage('ptbr')}>
Switch to Portuguese
</button>
</NtPopupProvider>Can I use custom fonts?
Yes! Simply override the font CSS variable:
.ntpopups-overlay {
--ntpopups-font-family: 'Your Custom Font', sans-serif;
}How do I make a popup fullscreen on mobile?
Use CSS to customize the container:
@media (max-width: 768px) {
.ntpopups-container {
width: 100vw !important;
height: 100vh !important;
max-width: 100vw !important;
max-height: 100vh !important;
border-radius: 0 !important;
}
}Can I nest popups?
While technically possible with keepLast: true, it's not recommended for UX reasons. Instead, close the first popup and open the second:
openPopup('confirm', {
data: {
onChoose: (confirmed) => {
if (confirmed) {
openPopup('form', { /* ... */ })
}
}
}
})How do I prevent closing a popup?
Set both closeOnEscape and closeOnClickOutside to false, and use requireAction: true:
openPopup('generic', {
closeOnEscape: false,
closeOnClickOutside: false,
requireAction: true,
data: { /* ... */ }
})🌟 Showcase
Built something amazing with ntPopups? We'd love to feature it! Share your project:
- Create a pull request to add it to this section
- Tweet it with #ntPopups
- Open an issue with your project link
🚀 ntPopups
Easy and powerful popup library for React
Made with ❤️ by Nem Tudo
Get Started • Live Demo • GitHub
If this library helped you, please consider:
⭐ Starring the repo • 🐛 Reporting issues • 💡 Suggesting features • 🤝 Contributing
© 2024 Nem Tudo. Licensed under MIT.
