npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-marketing-popups

v1.2.0

Published

Headless React Popout components and trigger hooks

Downloads

155

Readme

react-marketing-popups

NPM Version NPM Downloads Bundle Size

A lightweight, framework-agnostic React UX library for high-converting popouts, banners, slide-ins, and timed/behaviour-based triggers.

Perfect for marketing teams, e-commerce flows, onboarding funnels, exit-intent modals, upsells, and announcements.

This library uses animate.css for animations. it is a powerful CSS-only animation library and has been bundled with only the needed animations keeping it minimal.

Demo site: https://oluyoung.github.io/marketing-popups

Storybook demo: https://oluyoung.github.io/react-marketing-popups


Features

  • Popouts (Modal)
  • Banners (Full-width horizontal; full-height vertical)
  • SlideIn panels (Left and right)
  • Persistence layer (localStorage)
  • Trigger Hooks
    • Timer
    • Scroll
    • Inactivity
    • Exit Intent

Installation

npm install react-marketing-popups
# or
yarn add react-marketing-popups

Styles

Components ship with built-in styles. Import the CSS that matches what you use.

All components

import 'react-marketing-popups/style.css';

Per-component (recommended for tree-shaking)

Only load the CSS for the components you actually use:

import 'react-marketing-popups/Banner/style.css';
import 'react-marketing-popups/Popout/style.css';
import 'react-marketing-popups/SlideIn/style.css';

Import these once at your app entry point (e.g. main.tsx or _app.tsx), before rendering any components.

Overriding styles

Since you control where the CSS import sits in your own stylesheet cascade, you can override any style by placing your rules after the import:

// main.tsx
import 'react-marketing-popups/Popout/style.css';
import './my-overrides.css'; // your rules win

All components also accept className props (containerClassName, contentClassName, etc.) for targeted overrides without touching global CSS.


Quick Start

Without persistence

Pass onOpenChange directly to a trigger hook — it calls setOpen(true) when the trigger fires.

import { useState } from "react";
import { Popout, useTimerTrigger } from "react-marketing-popups";

export function Example() {
  const [open, setOpen] = useState(false);
  useTimerTrigger({ ms: 3000, onOpenChange: setOpen });

  return (
    <Popout id="popout-test" open={open} onOpenChange={setOpen}>
      <div style={{ padding: 20 }}>
        <h4>Special Offer: 10% Off Today Only!</h4>
      </div>
    </Popout>
  );
}

With persistence

Use useTriggerPersistence to track whether the popup has been seen. Gate the open prop with hasSeen() so the popup only shows once per user.

import { useState } from "react";
import { Popout, useTimerTrigger, useTriggerPersistence } from "react-marketing-popups";

export default function Example() {
  const [open, setOpen] = useState(false);
  const [ok, setOk] = useState(false);
  const [fired] = useTimerTrigger({ ms: 3000, onOpenChange: setOpen });
  const { hasSeen } = useTriggerPersistence({ id: 'popout-test', fired, isOk: ok, open });

  return (
    <Popout
      id="popout-test"
      open={open && !hasSeen()}
      onOpenChange={setOpen}
      isOk={ok}
      closeOnOk
    >
      <div style={{ padding: 20 }}>
        <h4>Special Offer: 10% Off Today Only!</h4>
        <button onClick={() => setOk(true)}>Claim offer</button>
      </div>
    </Popout>
  );
}

Components Overview

The library includes three components:

  1. Popout – modal centered on screen
  2. Banner – full-width horizontal banner (top or bottom) or full-height vertical banner (left or right)
  3. SlideIn – panel sliding in from left or right

Banner, Popout and SlideIn are core components — they are controlled manually via open / onOpenChange.


Shared Props

All components share these props. *required

| Prop | Type | Description | Default | | --- | --- | --- | --- | | id* | string | Unique key for persistence tracking | | | open* | boolean | Controls whether the component is visible | false | | onOpenChange* | (open: boolean) => void | Called when open state changes | | | children* | ReactNode | Content rendered inside the component | | | isOk | boolean | Signals the user completed the desired action | false | | closeOnOk | boolean | Close the component when isOk becomes true | false | | duration | number | Animation duration in ms | 300 | | closeBtnClassname | string | className for the close button | |


1. Popout (Modal)

Import

import { Popout } from "react-marketing-popups";

Description

A smooth animated modal centered on screen. Controlled manually via open and onOpenChange.

Example

import { useState } from "react";
import { Popout } from "react-marketing-popups";

export default function Example() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>Open Popout</button>
      <Popout id="my-popout" open={open} onOpenChange={setOpen}>
        <div style={{ padding: 20 }}>
          <h4>Special Offer: 10% Off Today Only!</h4>
        </div>
      </Popout>
    </>
  );
}

Props Table

| Prop | Type | Description | Default | | --- | --- | --- | --- | | lockScroll | boolean | Locks body scroll while open | false | | closeOnOverlay | boolean | Close modal on overlay click | true | | overlayClassName | string | className for the overlay element | | | contentClassName | string | className for the content container | | | elemProps | { overlayElProps?: HTMLAttributes<HTMLDivElement>, containerElProps?: HTMLAttributes<HTMLDivElement> } | Props for overlay and container elements | | | animation | "fade" | "zoom" | "bounce" | Animation effect | "zoom" |


2. Banner

Full-width horizontal or full-height vertical marketing banner.

Import

import { Banner } from "react-marketing-popups";

Example

<Banner id="my-banner" open={open} onOpenChange={setOpen} position="bottom">
  Free Shipping Ends Today!
</Banner>

Props Table

| Prop | Type | Description | Default | | --- | --- | --- | --- | | position | "top" | "bottom" | "left" | "right" | Banner placement | "bottom" | | animation | "fade" | "slide" | "bounce" | Animation effect | "slide" | | containerClassName | string | className for the root element | | | contentClassName | string | className for the content container | | | elemProps | { containerElProps?: HTMLAttributes<HTMLDivElement>, contentElProps?: HTMLAttributes<HTMLDivElement> } | Props for root and content elements | |


3. SlideIn

A fixed panel that slides in from the left or right edge of the screen.

Import

import { SlideIn } from "react-marketing-popups";

Example

<SlideIn id="my-slidein" open={open} onOpenChange={setOpen} position="right">
  Check out our latest features!
</SlideIn>

Props Table

| Prop | Type | Description | Default | | --- | --- | --- | --- | | position | "left" | "right" | Slide direction | "left" | | animation | "fade" | "slide" | "bounce" | Animation effect | "slide" | | wrapperClassName | string | className for the root element | | | containerClassName | string | className for the container element | | | contentClassName | string | className for the content element | | | elemProps | { wrapperElProps?: ComponentProps<'div'>, containerElProps?: HTMLAttributes<HTMLDivElement>, contentElProps?: HTMLAttributes<HTMLDivElement> } | Props for wrapper, container, and content elements | |


Hooks

Trigger hooks call onOpenChange(true) when they fire, directly setting your open state. Combine them with useTriggerPersistence to prevent re-showing after the user has already seen or engaged with the popup.


useTimerTrigger({ ms?, onOpenChange? })

Fires once after a delay, calling onOpenChange(true).

| Arg | Type | Description | Default | | --- | --- | --- | --- | | ms | number | Delay before firing | 3000 | | onOpenChange | (value: boolean) => void | Called with true when timer fires | no-op |

Returns [fired, setFired].

import { useState } from "react";
import { Popout, useTimerTrigger } from "react-marketing-popups";

export default function MyTimerPopout() {
  const [open, setOpen] = useState(false);
  useTimerTrigger({ ms: 4000, onOpenChange: setOpen });

  return (
    <Popout id="popout-timer" open={open} onOpenChange={setOpen}>
      <div style={{ padding: 20 }}>
        <h4>Special Offer: 10% Off Today Only!</h4>
      </div>
    </Popout>
  );
}

useScrollTrigger({ percent?, onOpenChange? })

Fires when the user scrolls past a percentage of the page, calling onOpenChange(true).

| Arg | Type | Description | Default | | --- | --- | --- | --- | | percent | number | Scroll depth threshold | 50 | | onOpenChange | (value: boolean) => void | Called with true when threshold is crossed | no-op |

Returns [fired, setFired].

import { useState } from "react";
import { Popout, useScrollTrigger } from "react-marketing-popups";

export default function MyScrollPopout() {
  const [open, setOpen] = useState(false);
  useScrollTrigger({ percent: 50, onOpenChange: setOpen });

  return (
    <Popout id="popout-scroll" open={open} onOpenChange={setOpen}>
      <div style={{ padding: 20 }}>
        <h4>Enjoying the content? Subscribe for more.</h4>
      </div>
    </Popout>
  );
}

useInactivityTrigger({ ms?, onOpenChange? })

Fires after the user is inactive for a given duration, calling onOpenChange(true).

| Arg | Type | Description | Default | | --- | --- | --- | --- | | ms | number | Inactivity duration before firing | 30000 | | onOpenChange | (value: boolean) => void | Called with true when inactivity is detected | no-op |

Returns [fired, setFired].

import { useState } from "react";
import { Popout, useInactivityTrigger } from "react-marketing-popups";

export default function MyInactivityPopout() {
  const [open, setOpen] = useState(false);
  useInactivityTrigger({ ms: 10000, onOpenChange: setOpen });

  return (
    <Popout id="popout-inactivity" open={open} onOpenChange={setOpen}>
      <div style={{ padding: 20 }}>
        <h4>Still there? Here's a bonus just for you.</h4>
      </div>
    </Popout>
  );
}

useExitTrigger({ topZonePx?, delayMs?, once?, onOpenChange? })

Fires when the mouse moves toward the top edge of the viewport (exit intent), calling onOpenChange(true).

| Arg | Type | Description | Default | | --- | --- | --- | --- | | topZonePx | number | Pixels from top edge that define the exit zone | 50 | | delayMs | number | Delay before firing after exit intent detected | 0 | | once | boolean | Fire only once | true | | onOpenChange | (value: boolean) => void | Called with true on exit intent | no-op |

Returns [fired, setFired].

import { useState } from "react";
import { Popout, useExitTrigger } from "react-marketing-popups";

export default function MyExitPopout() {
  const [open, setOpen] = useState(false);
  useExitTrigger({ onOpenChange: setOpen });

  return (
    <Popout id="popout-exit" open={open} onOpenChange={setOpen}>
      <div style={{ padding: 20 }}>
        <h4>Wait — don't leave without your discount!</h4>
      </div>
    </Popout>
  );
}

useTriggerPersistence({ id, fired, open, isOk? })

Tracks whether a triggered popup has been seen. Returns hasSeen() which you gate your open prop with — the popup will not reappear once the user has seen or engaged with it.

Marks as seen when:

  • isOk becomes true (user completed desired action)
  • fired && !open (popup was closed after the trigger fired)

| Prop | Type | Description | | --- | --- | --- | | id | string | Unique persistence key | | fired | boolean | Trigger fired state from a trigger hook | | open | boolean | Current open state | | isOk | boolean | Marks as seen when the user completes the desired action |

Returns { hasSeen, markSeen, clear }.

const [fired] = useTimerTrigger({ ms: 3000, onOpenChange: setOpen });
const { hasSeen } = useTriggerPersistence({ id: 'my-popout', fired, open, isOk: ok });

// Gate open with hasSeen():
<Popout open={open && !hasSeen()} ...>

usePersistence(key)

localStorage-backed helper for tracking whether a component has been seen. Use this when you need full manual control over persistence (e.g. in a CorePersistenceView pattern with a button-triggered popup).

| Arg | Type | Description | | --- | --- | --- | | key | string | Unique identifier |

Returned API

| Method | Description | | --- | --- | | hasSeen() | Returns true if the key has been marked as seen | | markSeen() | Marks the key as seen | | clear() | Removes the key |

const { hasSeen, markSeen, clear } = usePersistence("banner-offer");

// Use in a manually-triggered component:
<Banner
  open={open && !hasSeen()}
  onOpenChange={setOpen}
  onClose={markSeen}
>

Storybook

Launch storybook locally with npm run storybook

License

N/A

Contributions

PRs, issues, feature requests, and improvements are welcome!