sweetalert2-react-content: The Complete Guide to React Components in Alerts
SweetAlert2 is already one of the most polished React modal libraries in the ecosystem — beautiful defaults, zero-fuss API, accessible markup out of the box. But it has one classic limitation: its content options accept plain HTML strings, not live React JSX. You can shove a raw <input> tag in there, sure, but the moment you want a controlled component, a custom hook, or literally any piece of real React logic, plain HTML falls apart immediately.
That is exactly the gap
sweetalert2-react-content
fills. It is a thin adapter — technically a mixin — that patches SweetAlert2 so its title, html, footer, and content options accept JSX directly. Under the hood it creates a React portal and mounts your component into the dialog’s DOM node, keeping the full React rendering pipeline intact. State, context, hooks, animations — all of it works exactly as you would expect inside a normal React tree.
This guide covers everything from installation to advanced patterns: stateful forms inside popups, hook-driven dialogs, integration with libraries like React Hook Form, and the subtle lifecycle differences you need to know to avoid the classic “modal closes before state updates” footgun. Whether you are just getting started or migrating an existing codebase, you will leave with a working mental model and copy-pasteable code.
What sweetalert2-react-content Actually Does
Before touching the terminal, it is worth understanding the architecture. SweetAlert2 is framework-agnostic — it does not know React exists. It builds a DOM structure, injects your content into designated slots, fires promise-based lifecycle events, and tears everything down on close. The library itself is plain JavaScript. This is a feature, not a bug: it keeps the core small and universal. But it also means that if you inject a React component as an HTML string, React never mounts it — you get a dead HTML snapshot.
sweetalert2-react-content solves this with a mixin pattern. You call withReactContent(Swal) and receive an enhanced MySwal instance whose popup options understand JSX. When SweetAlert2 creates the modal DOM node, the mixin intercepts it and calls ReactDOM.createPortal (or createRoot in v5 for React 18) to mount your JSX inside the correct slot. When the modal closes, the mixin unmounts the React tree cleanly. The public API of SweetAlert2 stays completely unchanged — you still call MySwal.fire(), await its promise, and read result.isConfirmed. The only difference is that your content is now a living React component.
This architecture also means the adapter is genuinely lightweight. It adds no global state, no Redux store, no context provider, and no opinion about your styling. It is a surgical patch on the SweetAlert2 instance you hand it. You can have multiple MySwal instances in the same app if you need different theme configurations, and each will render React content independently. That level of composability is rarer than it should be in the React UI ecosystem.
Installation and Initial Setup
The sweetalert2-react-content installation is straightforward, but version alignment matters. The adapter is a peer-dependency consumer: it expects specific ranges of both sweetalert2 and react. Getting this wrong produces cryptic runtime errors rather than helpful install warnings, so match these versions before you start.
- React 18 + SweetAlert2 ≥ 11: use
sweetalert2-react-content@5 - React 17 + SweetAlert2 ≥ 11: use
sweetalert2-react-content@4 - React 16 + SweetAlert2 ≥ 10: use
sweetalert2-react-content@3
Install both the core library and the adapter in a single command:
# npm
npm install sweetalert2 sweetalert2-react-content
# yarn
yarn add sweetalert2 sweetalert2-react-content
# pnpm
pnpm add sweetalert2 sweetalert2-react-content
SweetAlert2 ships its own CSS. You need to import it once — typically in your root index.tsx or a global stylesheet. If you are using a custom theme or CSS-in-JS, you can skip this and provide all styles yourself, but for the vast majority of projects the default stylesheet is fine and covers accessibility-critical styles you do not want to recreate from scratch.
// index.tsx or App.tsx
import 'sweetalert2/dist/sweetalert2.min.css';
With that in place, create your enhanced Swal instance. The convention is to do this once in a dedicated module and import it wherever you need a dialog. Centralising the instance also lets you apply global default options — custom classes, timer behaviour, button labels — without repeating yourself across fifty call sites. This is a small architectural decision that pays significant maintenance dividends on any project larger than a side project.
// lib/MySwal.ts
import Swal from 'sweetalert2';
import withReactContent from 'sweetalert2-react-content';
const MySwal = withReactContent(Swal);
export default MySwal;
Your First sweetalert2-react-content Example
Once MySwal is in place, using React JSX in a dialog is as natural as writing a component. Pass your JSX directly to the title or html properties of the fire() configuration object. SweetAlert2 will render it as a live React tree, not a static string. The following example fires a confirmation dialog with a custom React title including an icon component from react-icons and a styled paragraph body.
import MySwal from '../lib/MySwal';
import { FiAlertTriangle } from 'react-icons/fi';
async function handleDelete(id: string) {
const result = await MySwal.fire({
title: (
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<FiAlertTriangle color="#f59e0b" /> Are you sure?
</span>
),
html: (
<p>
This will permanently delete item <strong>{id}</strong>.
You cannot undo this action.
</p>
),
icon: undefined, // disable built-in icon, using custom one above
showCancelButton: true,
confirmButtonText: 'Yes, delete it',
cancelButtonText: 'Cancel',
confirmButtonColor: '#ef4444',
});
if (result.isConfirmed) {
await deleteItem(id);
MySwal.fire('Deleted!', 'The item has been removed.', 'success');
}
}
Notice that result.isConfirmed is still the standard SweetAlert2 result shape — nothing about the async, promise-based flow changes just because the content is now JSX. This backward-compatibility is intentional and means you can adopt the adapter incrementally: swap dialogs one at a time as you need React features, rather than migrating everything at once.
The html property accepts any valid JSX, including component trees of arbitrary depth. You could pass a full <ProductCard /> component as the body of a confirmation dialog, complete with its own props, context subscriptions, and side effects. SweetAlert2 will mount and unmount it correctly through the portal lifecycle. The one caveat: any effects that fire on unmount (useEffect cleanup) will run when the dialog closes, so design those effects with that in mind.
React Form Modals: Collecting Input from Users
The most common real-world use case for React interactive alerts is collecting user input — a rename dialog, a settings form, a quick-add panel — without routing to a new page. SweetAlert2’s built-in input option handles simple text inputs, but the moment you need validation, multiple fields, or a custom field type, you hit a wall. With sweetalert2-react-content, you can mount a full React alert form inside the modal and communicate the result back to the caller.
The key pattern is using a ref to expose a getter from the form component. When SweetAlert2 fires the preConfirm callback, you call the ref’s getter to retrieve the form values, validate them, and either return the values (allowing the dialog to close) or throw/return false (keeping it open with an error state). This gives you full control over the confirmation lifecycle while keeping validation logic entirely inside the React component.
import React, { useRef, useState, useImperativeHandle, forwardRef } from 'react';
import MySwal from '../lib/MySwal';
// ---- Form component ----
interface RenameFormRef {
getValues: () => { name: string } | null;
}
const RenameForm = forwardRef<RenameFormRef, { initial: string }>(
({ initial }, ref) => {
const [name, setName] = useState(initial);
const [error, setError] = useState('');
useImperativeHandle(ref, () => ({
getValues() {
if (!name.trim()) {
setError('Name cannot be empty.');
return null;
}
return { name: name.trim() };
},
}));
return (
<div style={{ textAlign: 'left' }}>
<label htmlFor="rename-input">New name</label>
<input
id="rename-input"
className="swal2-input"
value={name}
onChange={e => { setName(e.target.value); setError(''); }}
/>
{error && <p style={{ color: '#ef4444', fontSize: '0.85rem' }}>{error}</p>}
</div>
);
}
);
// ---- Caller ----
async function openRenameDialog(currentName: string) {
const formRef = React.createRef<RenameFormRef>();
const result = await MySwal.fire({
title: 'Rename Item',
html: <RenameForm ref={formRef} initial={currentName} />,
showCancelButton: true,
confirmButtonText: 'Rename',
preConfirm: () => {
const values = formRef.current?.getValues();
if (!values) return false; // keep dialog open
return values;
},
});
if (result.isConfirmed && result.value) {
console.log('New name:', result.value.name);
}
}
Using the swal2-input class on your inputs is optional but recommended — it inherits SweetAlert2’s input styling, so custom fields blend visually with dialogs that mix built-in and custom inputs. You can naturally override this with your own design system classes. The preConfirm hook returning false is SweetAlert2’s official mechanism for blocking dialog closure; combined with React state-driven validation UI, it produces a native-feeling React form modal with zero additional libraries.
Managing State with sweetalert2-react-content Hooks
Since your modal content is a real React component, you have access to the entire hooks API. useState, useReducer, useContext, useEffect, useMemo — all of them work as expected. This matters most for React interactive alerts that need to react to user input in real time: character counters, live search results inside a dialog, multi-step wizards, or progress indicators during async operations.
A practical pattern is the multi-step modal wizard: a dialog that moves through several screens without closing and reopening. Implement this as a single component with internal step state, using SweetAlert2’s willClose and didOpen callbacks to sync the dialog footer buttons with the current step. Because the component is fully stateful, back-and-forward navigation preserves field values automatically — something nearly impossible to achieve with SweetAlert2’s native HTML string approach.
function MultiStepWizard() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({ email: '', plan: '' });
const updateField = (field: string, value: string) =>
setFormData(prev => ({ ...prev, [field]: value }));
if (step === 1) return (
<div>
<h4>Step 1: Your email</h4>
<input className="swal2-input" placeholder="you@example.com"
value={formData.email}
onChange={e => updateField('email', e.target.value)} />
<button className="swal2-confirm swal2-styled"
onClick={() => setStep(2)}>Next →</button>
</div>
);
return (
<div>
<h4>Step 2: Choose plan</h4>
{['Free', 'Pro', 'Enterprise'].map(p => (
<label key={p} style={{ display: 'block', marginBottom: '0.5rem' }}>
<input type="radio" value={p} checked={formData.plan === p}
onChange={() => updateField('plan', p)} /> {p}
</label>
))}
<button onClick={() => setStep(1)}>← Back</button>
</div>
);
}
// Fire it
await MySwal.fire({
html: <MultiStepWizard />,
showConfirmButton: false, // wizard manages its own flow
allowOutsideClick: false,
});
One important sweetalert2-react-content state consideration: the React component lives inside a SweetAlert2-managed DOM node, not inside your app’s root React tree. This means it does not automatically inherit context values from providers sitting above <App /> — Redux, React Query, Theme providers, and so on. The fix is simple: wrap your JSX in the same providers before passing it to MySwal.fire(). It feels slightly ceremonial, but it is explicit and predictable, which is exactly the trade-off you want in dialog logic.
Context Providers and sweetalert2-react-content: The Portal Problem
As noted above, the portal nature of the adapter means your modal component does not automatically sit inside your application’s provider tree. This catches developers off guard roughly once per project. The symptom is always the same: you call a custom hook inside the modal, it throws because it cannot find its context, and you stare at the stack trace wondering why a perfectly normal hook is broken. The solution is to wrap the JSX you pass into MySwal.fire() with whatever providers the component needs.
import { QueryClientProvider, useQueryClient } from '@tanstack/react-query';
import { ThemeProvider } from './ThemeContext';
import { queryClient } from '../lib/queryClient';
await MySwal.fire({
html: (
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<MyComplexModalContent />
</ThemeProvider>
</QueryClientProvider>
),
});
If you fire dialogs frequently throughout your app, wrapping every call is tedious and error-prone. The cleaner pattern is to create a custom fireSwal utility that always wraps in your core providers automatically. You pass just the inner component, and the utility handles the boilerplate. This also centralises the list of required providers, so when you add a new one globally, you update one function rather than hunting across twenty dialog call sites.
A more advanced option is to use the target SweetAlert2 option to mount the dialog inside a specific DOM node that is already part of your React tree — effectively sidestepping the portal problem entirely. The dialog then inherits context naturally. The trade-off is that the dialog’s stacking context and z-index behaviour becomes tied to that container, which requires careful CSS management. For most applications, the wrapper utility is the right default.
Async Operations, Loading States, and showLoading
A common pattern in React alert dialogs is triggering an async operation — a network request, a file upload, an expensive computation — and showing progress feedback inside the dialog without closing it prematurely. SweetAlert2 exposes Swal.showLoading() and Swal.hideLoading() for this purpose, and they compose cleanly with React state inside your modal component.
The canonical approach is to call MySwal.showLoading() inside a useEffect or an event handler when your async work begins, then MySwal.hideLoading() when it completes. Because MySwal is a module-level singleton, you can call these methods from anywhere — inside the component, in a preConfirm callback, or in the parent that fired the dialog. This flexibility is useful but requires discipline: make sure you always pair showLoading with a hideLoading in both the success and error branches, or users will stare at a spinner forever.
await MySwal.fire({
title: 'Upload File',
html: <FileUploadForm />,
showConfirmButton: true,
confirmButtonText: 'Upload',
preConfirm: async () => {
MySwal.showLoading();
try {
const result = await uploadToS3(selectedFile);
return result;
} catch (err) {
MySwal.hideLoading();
MySwal.showValidationMessage(`Upload failed: ${err.message}`);
return false;
}
},
});
MySwal.showValidationMessage() is worth highlighting here. It renders an error message in SweetAlert2’s built-in validation slot below the content area — consistent styling, accessible role attributes, no extra markup needed. Combine it with preConfirm returning false and you have a complete async validation loop: the dialog stays open, shows the error, lets the user correct and retry, and closes only on success. This covers 90% of real-world React form modal requirements with no third-party validation library needed.
TypeScript Integration and Type Safety
sweetalert2-react-content ships full TypeScript definitions. The withReactContent function is generic: it accepts a SweetAlert2 instance type and returns an augmented type where title, html, footer, and content accept ReactChild in addition to their original string types. In practice this means your IDE will autocomplete JSX in those positions without any manual type augmentation.
Type safety becomes especially valuable in preConfirm. SweetAlert2’s result.value is typed as any by default. You can narrow it by explicitly typing the return value of preConfirm:
interface FormValues { name: string; role: string; }
const result = await MySwal.fire<FormValues>({
html: <UserForm ref={formRef} />,
preConfirm: (): FormValues | false => {
const values = formRef.current?.getValues();
return values ?? false;
},
});
// result.value is now FormValues | undefined — fully typed
if (result.isConfirmed && result.value) {
console.log(result.value.name); // string ✓
}
This generic annotation on fire<T> threads the type through the entire promise chain. It is one of the cleanest TypeScript patterns in the SweetAlert2 ecosystem and makes refactoring dialog data structures significantly safer — change the interface, fix the compile errors, done. No runtime surprises, no sneaky any escaping through a dialog boundary.
Testing React Alert Dialogs
Testing dialog logic is one of those areas where teams often give up and skip coverage entirely. With sweetalert2-react-content, testing is more tractable than you might expect, though it does require a specific setup. The recommended approach is to mock MySwal at the module level in your test files, replacing fire() with a jest mock that returns a preset result. This lets you test the code that calls the dialog without actually rendering SweetAlert2 at all.
// __mocks__/MySwal.ts
const MySwal = {
fire: jest.fn().mockResolvedValue({ isConfirmed: true, value: { name: 'Test' } }),
};
export default MySwal;
// In your test
import MySwal from '../lib/MySwal';
import { openRenameDialog } from '../utils/dialogs';
jest.mock('../lib/MySwal');
it('calls rename API after confirmed dialog', async () => {
await openRenameDialog('Old Name');
expect(renameSpy).toHaveBeenCalledWith({ name: 'Test' });
});
For integration tests that need to actually render and interact with the form component inside the dialog — testing validation messages, multi-step navigation, and so on — use React Testing Library. Mount the inner component directly in your test (outside of SweetAlert2) and test it in isolation. This is cleaner than trying to pierce SweetAlert2’s portal in an integration test and is a better architectural signal: if your form component is hard to test outside of a dialog, it probably has too much coupling to the SweetAlert2 API and should be refactored.
The one scenario that genuinely requires portal testing is validating that preConfirm wires correctly to formRef.current.getValues(). Write a focused integration test using @testing-library/user-event to fill the form and click confirm, then assert on result.value. Keep these tests small and specific — they are testing the integration contract, not the form logic, which belongs in unit tests.
Performance Considerations and Cleanup
For most applications, sweetalert2-react-content is essentially zero-cost: dialogs are transient, component trees inside them are short-lived, and portal mounting/unmounting is fast. Where you can accidentally create performance issues is with components that subscribe to expensive data sources or set up polling inside useEffect. Because the component mounts when the dialog opens and unmounts when it closes, these subscriptions are naturally scoped to the dialog lifetime — which is correct behaviour, as long as you write proper cleanup in your effect return function.
A subtler concern is repeated calls to withReactContent(Swal). Each call creates a new enhanced instance. If you accidentally call it inside a component render function instead of at module level, you create a new instance on every render, which is wasteful even if not catastrophically harmful. Always call withReactContent once at module scope and export the result. This is worth adding to your project’s code review checklist if you have multiple developers working with dialogs.
Finally, be deliberate about dialog queuing. SweetAlert2 only shows one dialog at a time by default; if you fire a second dialog while one is already open, the first closes immediately. In flows that chain dialogs — confirm, then success, then redirect — this is intentional and fine. In event-driven flows where dialogs might fire concurrently from independent user actions, you should guard against accidental queueing with Swal.isVisible() checks or a simple application-level dialog lock. This is not a sweetalert2-react-content-specific concern, but it is a common source of confusing UX in React applications that use dialogs heavily.
FAQ
How do I pass React state into a SweetAlert2 modal?
Use sweetalert2-react-content to mount a stateful React component as the modal content via the html property. Manage state inside the component with useState or useReducer exactly as you normally would. For two-way communication between the component and the dialog’s preConfirm callback, expose values via useImperativeHandle and a ref created in the caller with React.createRef(). SweetAlert2 treats the content as a React portal, so all standard React patterns apply inside it.
Can I put a form with validation inside a SweetAlert2 dialog?
Yes — this is one of the primary use cases for sweetalert2-react-content. Render any React form component inside the html option, handle validation with React state or a library like React Hook Form. In the preConfirm callback, call your ref’s getter: if validation fails, return false (the dialog stays open) or call MySwal.showValidationMessage('...') to display an error. Return the validated values to let SweetAlert2 close the dialog and pass them as result.value in the resolved promise.
Is sweetalert2-react-content compatible with React 18?
Yes. Version 5.x of sweetalert2-react-content uses React 18’s createRoot API, replacing the deprecated ReactDOM.render. Install sweetalert2 ≥ 11 alongside sweetalert2-react-content ≥ 5 to get full React 18 compatibility, including concurrent rendering features. If you are on React 17, use sweetalert2-react-content@4 to avoid peer-dependency warnings and the legacy render path.