
Security News
Node.js Drops Bug Bounty Rewards After Funding Dries Up
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.
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
npm install ntpopups
Or with Yarn:
yarn add ntpopups
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;
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/_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;
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>;
}
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: "β") |
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) |
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.
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: 'your@email.com',
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 |
{
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: 'your@email.com',
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:
requireAction: true to hide the cancel buttonExtend forms with your own specialized input types while maintaining full validation and state management.
Perfect for:
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);
}
}
});
β DO:
props.changeValue() to update valuesprops.value using useEffectprops.valid for visual feedbackprops.dataβ DON'T:
disabled and required propsprops.value directlyBuilt-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.
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 |
Build your own popup components with full control over UI and behavior.
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>
</>
);
}
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>
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>
// 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).
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>
);
}
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'
});
<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
}
}
}}
>
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'
}
});
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();
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 action
updatePopup(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);
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
});
};
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
});
};
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'
});
};
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]);
};
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: 'β'
}
});
}
};
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();
};
// 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
});
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: 'your@email.com',
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'
});
};
ntPopups provides 100+ CSS variables for complete visual control. All variables use the --ntpopups-* prefix.
--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;
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;
--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);
--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;
--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);
--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;
--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);
--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;
--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);
--ntpopups-mobile-padding: 15px;
--ntpopups-mobile-font-size: 14px;
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';
All classes use the .ntpopups-* prefix for easy styling.
.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 */
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 */
/* 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;
}
Mobile Optimizations:
Keyboard Shortcuts:
ESC - Close popup (if closeOnEscape is enabled)Tab / Shift+Tab - Navigate between interactive elementsEnter / Space - Activate buttons1. 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
});
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: 'β'
}
});
}
}
Symptom: openPopup() returns null
Solutions:
customPopupsNtPopupProvider wraps your componentopenPopup inside a component (not at module level)Symptom: Popup appears unstyled
Solutions:
import 'ntpopups/dist/styles.css''use client'Symptom: Submit button stays disabled
Solutions:
required fields have valuesmatchRegex patterns are correctminLength/maxLength constraints are metdisabled fields are not validatedonChange to debug validation stateSymptom: ESC key or backdrop click doesn't work
Solutions:
closeOnEscape setting (default: true)closeOnClickOutside setting (default: true)requireAction: true, use closePopup(id, true) with actionSymptom: "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>
);
}
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
});
MIT License - Free to use in personal and commercial projects.
See the LICENSE file for details.
Contributions are welcome! Here's how you can help:
# 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
If ntPopups helped you build better user experiences, please:
Special thanks to:
// 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)
}
// 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()
}
}
// 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')
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'
}
}
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>
Yes! Simply override the font CSS variable:
.ntpopups-overlay {
--ntpopups-font-family: 'Your Custom Font', sans-serif;
}
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;
}
}
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', { /* ... */ })
}
}
}
})
Set both closeOnEscape and closeOnClickOutside to false, and use requireAction: true:
openPopup('generic', {
closeOnEscape: false,
closeOnClickOutside: false,
requireAction: true,
data: { /* ... */ }
})
Built something amazing with ntPopups? We'd love to feature it! Share your project:
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.
FAQs
Ease and powerful popup library for React
We found that ntpopups demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Β It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the projectβs GitHub releases.