react-marketing-popups
v1.2.0
Published
Headless React Popout components and trigger hooks
Downloads
155
Maintainers
Readme
react-marketing-popups
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-popupsStyles
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 winAll 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:
- Popout – modal centered on screen
- Banner – full-width horizontal banner (top or bottom) or full-height vertical banner (left or right)
- 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:
isOkbecomestrue(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!
