Index of the post
- What you will build
- Step 1. Write semantic HTML
- Step 2. Add clean, responsive CSS
- Step 3. Wire up JavaScript for open, close, and focus trap
- Accessibility checklist
- UX and content tips that increase conversions
- Simple patterns for common triggers
- Exit intent on desktop
- Delay until the user engages
- One-time per session
- Measure and improve with A/B testing
- Embedding on your site
- Common mistakes to avoid
- Going further
Published at: 21 Aug 2025
You do not need a framework to build a great popup. With a few lines of semantic HTML, modern CSS, and vanilla JavaScript you can create a fast, accessible, and conversion-friendly modal that works on mobile and desktop. In this guide you will code a reusable popup component, learn practical triggers and timing, and see how to optimize it for real users.
Before you start, keep in mind that a popup is a UX pattern that can help or hurt your results. It pays to follow solid guidelines like these best practices for popups and to plan sensible timing windows, as explained in when to show a popup. If you are completely new to the concept, this quick primer on what is a pop-up window sets the stage.
What you will build
A modal popup that:
-
Opens by clicking a button and can also auto-trigger on time or scroll.
-
Locks page scroll while open and restores it on close.
-
Traps focus for keyboard users, supports Esc to close, and lets users click the overlay to dismiss.
-
Animates in and out without jank.
-
Is easy to customize and reuse.
If you want a deeper, hands-on learning path while you build, bookmark the I Love PopUps Academy and specific lessons like how to create popups without coding or how to analyze your popup data.
Step 1. Write semantic HTML
Start with a trigger button and a dialog container. Use role="dialog"
and aria-modal="true"
so assistive technologies recognize it correctly. Use hidden
to keep it out of the accessibility tree when closed.
<button id="openPopup" class="btn">Open offer</button> <div id="popup" class="popup" role="dialog" aria-modal="true" aria-labelledby="popupTitle" aria-describedby="popupDesc" hidden> <div class="popup__overlay" data-close></div> <div class="popup__content" role="document"> <button class="popup__close" aria-label="Close popup" data-close>×</button> <h2 id="popupTitle">Get 10% off your first order</h2> <p id="popupDesc">Join our list for tips, deals, and updates. Unsubscribe anytime.</p> <form class="popup__form" action="#" method="post" novalidate> <label for="email">Email</label> <input id="email" type="email" required placeholder="you@example.com" autocomplete="email"> <button type="submit" class="btn btn--primary">Subscribe</button> <small class="form-hint">No spam. One-click opt out.</small> </form> </div> </div>
Why semantic structure matters: it improves accessibility and SEO, and reduces the amount of JavaScript you need. For design and UX ideas that keep conversions front and center, skim these best practices to create popups that convert.
Step 2. Add clean, responsive CSS
Use CSS variables for easy theming, and prefer opacity
plus transform
transitions for smooth animations. The overlay uses position: fixed
to cover the viewport and block interactions with the page.
:root { --popup-bg: #ffffff; --overlay-bg: rgba(0, 0, 0, 0.5); --radius: 12px; --max-w: 520px; --gap: 16px; } .popup[hidden] { display: none; } .popup__overlay { position: fixed; inset: 0; background: var(--overlay-bg); opacity: 0; transition: opacity 200ms ease; } .popup__content { position: fixed; inset: 0; display: grid; place-items: center; padding: 24px; } .popup__content > * { width: min(var(--max-w), calc(100vw - 32px)); background: var(--popup-bg); border-radius: var(--radius); box-shadow: 0 10px 30px rgba(0,0,0,.15); padding: 24px; transform: translateY(20px) scale(.98); opacity: 0; transition: transform 220ms ease, opacity 220ms ease; } .popup.open .popup__overlay { opacity: 1; } .popup.open .popup__content > * { transform: translateY(0) scale(1); opacity: 1; } .popup__close { position: absolute; top: 10px; right: 12px; background: transparent; border: 0; font-size: 24px; cursor: pointer; } .popup__form { display: grid; gap: var(--gap); margin-top: 8px; } .btn { cursor: pointer; padding: 10px 16px; border: 1px solid #111; background: #fff; } .btn--primary { background: #111; color: #fff; border-color: #111; }
Tip: avoid scroll jumps by applying scroll lock only to the root element when the popup is open. Avoid overly aggressive animations that can cause motion sickness.
Step 3. Wire up JavaScript for open, close, and focus trap
The script will:
-
Toggle the
hidden
attribute and.open
class. -
Remember the previously focused element and restore focus on close.
-
Trap focus inside the dialog using a simple loop.
-
Close on overlay click or Esc.
<script> (() => { const popup = document.getElementById('popup'); const openBtn = document.getElementById('openPopup'); const overlay = popup.querySelector('.popup__overlay'); const closeEls = popup.querySelectorAll('[data-close]'); let lastFocus = null; function getFocusable(container) { return [...container.querySelectorAll( 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])' )].filter(el => el.offsetParent !== null); } function openPopup() { lastFocus = document.activeElement; popup.hidden = false; popup.classList.add('open'); document.documentElement.style.overflow = 'hidden'; const focusables = getFocusable(popup); if (focusables.length) focusables[0].focus(); document.addEventListener('keydown', onKeydown); popup.addEventListener('keydown', trapFocus); } function closePopup() { popup.classList.remove('open'); popup.hidden = true; document.documentElement.style.overflow = ''; document.removeEventListener('keydown', onKeydown); popup.removeEventListener('keydown', trapFocus); if (lastFocus) lastFocus.focus(); } function onKeydown(e) { if (e.key === 'Escape') closePopup(); } function trapFocus(e) { if (e.key !== 'Tab') return; const f = getFocusable(popup); if (!f.length) return; const first = f[0], last = f[f.length - 1]; if (e.shiftKey && document.activeElement === first) { last.focus(); e.preventDefault(); } else if (!e.shiftKey && document.activeElement === last) { first.focus(); e.preventDefault(); } } openBtn.addEventListener('click', openPopup); overlay.addEventListener('click', closePopup); closeEls.forEach(el => el.addEventListener('click', closePopup)); // Optional triggers // 1) Timed trigger after 10 seconds setTimeout(() => { /* openPopup(); */ }, 10000); // 2) Scroll trigger at 50% const onScroll = () => { const scrolled = (window.scrollY + window.innerHeight) / document.documentElement.scrollHeight; if (scrolled > 0.5) { // openPopup(); window.removeEventListener('scroll', onScroll); } }; window.addEventListener('scroll', onScroll, { passive: true }); })(); </script>
You now have a robust baseline. To decide when to actually fire those optional triggers, revisit guidance on how to use popups on your website and the real-world impact of a popup on your website.
Accessibility checklist
Inclusive popups convert better. Aim for:
-
Correct roles and labels:
role="dialog" aria-modal="true"
witharia-labelledby
andaria-describedby
. -
Keyboard support: focus moves into the dialog, is trapped, and returns to the trigger on close. Esc closes.
-
Visible focus styles: do not remove default outlines without providing a clear alternative.
-
Screen reader friendly close button: provide
aria-label="Close popup"
and a large touch target. -
Motion preferences: respect
@media (prefers-reduced-motion: reduce)
and shorten or skip animations for those users.
If you want a deeper dive into performance and usability habits that boost conversions, the Academy’s lesson on how to get the most out of I Love PopUps collects field-tested tips.
UX and content tips that increase conversions
Follow these patterns from teams who build popups every day:
-
Offer something clear and specific. A tangible incentive beats vague copy almost every time.
-
Keep the form short. Email only often wins. If you must ask for more, justify it.
-
Timing is context. Entry popups can work on high-intent pages, while scroll-based or exit intent often suits content pages. See examples in when to show a popup.
-
Make dismissal easy. A visible close button and overlay click reduce frustration and bounces.
-
Avoid stacking. Do not show multiple popups at once. Use a queue.
-
Optimize mobile layout. Hit targets must be large, spacing generous, and content readable at small sizes.
-
Match design to the brand. Colors, tone, and imagery must feel native to the site. For creative ideas see these best practices for popups.
Simple patterns for common triggers
Here are lightweight recipes you can plug into the script above.
Exit intent on desktop
Use a mouseleave on the window top edge.
(function() { let fired = false; function exitIntent(e) { if (fired) return; if (e.clientY <= 0) { // openPopup(); fired = true; window.removeEventListener('mouseout', exitIntent); } } window.addEventListener('mouseout', exitIntent); })();
Delay until the user engages
Require two page interactions before showing.
let interactions = 0; function maybeShow() { interactions += 1; if (interactions === 2) { // openPopup(); document.removeEventListener('click', maybeShow); document.removeEventListener('scroll', maybeShow); } } document.addEventListener('click', maybeShow, { passive: true }); document.addEventListener('scroll', maybeShow, { passive: true });
One-time per session
Use sessionStorage
to avoid repeated prompts.
if (!sessionStorage.getItem('nl-popup')) { setTimeout(() => { // openPopup(); sessionStorage.setItem('nl-popup', '1'); }, 8000); }
Measure and improve with A/B testing
Even a small change in headline, image, or timing can shift conversion rates significantly. The fastest way to learn what works for your audience is to run A/B tests on your popups. Once you have traffic, keep a simple test cadence:
-
Focus on one variable at a time.
-
Run the test long enough to reach a trustworthy sample.
-
Keep winners and iterate.
After you have a couple of wins, learn how to analyze your popup data with the right metrics. Track views, CTR, submissions, and eventual downstream outcomes like purchases or signups.
Embedding on your site
If your site loads a single bundle or you use a builder, you can drop the HTML inside your template and load the script globally. For teams that prefer pasting one snippet, this short primer on how to install a script on your website helps you place code in the right spot without breaking critical render paths.
Common mistakes to avoid
-
Hiding the close button or making it too small.
-
Showing the same popup repeatedly in one session.
-
Triggering too early on pages where users are still orienting.
-
Using form validation that blocks keyboard navigation.
-
Overusing animations that conflict with
prefers-reduced-motion
.
Going further
If you enjoyed the hands-on approach here, you will likely appreciate these resources:
-
Quick patterns to create popups without coding.
-
A quick tour of how to use popups on your website.
-
Strategy advice on the impact of a popup on your website.
Ship your first version, collect data for a week, then iterate. Popups succeed when they feel timely, relevant, and respectful.