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

@jmeirinkmarimed/form-popup

v1.0.1

Published

A React email capture modal with HubSpot submission and web-component support

Readme

Form Popup

Email-capture modal for React apps and plain HTML sites. Ships as a React component and as a <form-popup> custom element (IIFE for static pages). Submits to HubSpot, then shows a confirmation screen. After a successful submit, the popup stays dismissed on later visits via localStorage.

Features

  • Form screen, then confirmation screen after submit
  • Persists completion in localStorage (formPopupSubmitted:<formId>) so returning visitors do not see it again
  • Maps email, name, phone, state, and one extra property to HubSpot field names of your choosing
  • Four form layouts: email + state, email only, name + email, email + phone
  • Optional GDPR processing consent and HubSpot communication/subscription checkboxes
  • Optional invisible Google reCAPTCHA (v2) before HubSpot submit
  • Customizable copy, state list, colors, and fonts
  • Style isolation via Shadow DOM on the web-component path

Install

npm install @jmeirinkmarimed/form-popup

Peer dependency: React 17 or 18.

React

import { FormPopup } from '@jmeirinkmarimed/form-popup';

function App() {
  return (
    <FormPopup
      formType="emailState"
      hubspotPortalId="YOUR_PORTAL_ID"
      hubspotFormId="YOUR_FORM_GUID"
      emailProperty="email"
      stateProperty="state"
      additionalPropertyName="signup_source"
      additionalPropertyValue="homepage_popup"
      consentToProcessText="I agree to allow Example Company to store and process my personal data."
      formTitle="Sign up for 10% off your first purchase!"
      formSubtitle="Enter your email to join our mailing list."
      states={[
        "Delaware",
        "Maryland",
        "Massachusetts",
        "Illinois",
        "Missouri",
        "Just Browsing",
      ]}
      confirmationTitle="Use code WELCOME10 for 10% off!"
      confirmationMessage="You're officially on the list for sweet updates delivered straight to your inbox."
      confirmationButtonText="Shop Betty's"
      confirmationButtonUrl="https://shopbettys.com"
      recaptchaSiteKey="YOUR_INVISIBLE_RECAPTCHA_SITE_KEY"
      waitForAgeGate
    />
  );
}

HTML

<script src="https://unpkg.com/@jmeirinkmarimed/form-popup/dist/form-popup.min.js"></script>

<form-popup
  form-type="emailState"
  hubspot-portal-id="YOUR_PORTAL_ID"
  hubspot-form-id="YOUR_FORM_GUID"
  email-property="email"
  state-property="state"
  additional-property-name="signup_source"
  additional-property-value="homepage_popup"
  consent-to-process-text="I agree to allow Example Company to store and process my personal data."
  communication-consents='[{"subscriptionTypeId":999,"text":"I agree to receive other communications from Example Company."}]'
  form-title="Sign up for 10% off your first purchase!"
  form-subtitle="Enter your email to join our mailing list."
  states="Delaware,Maryland,Massachusetts,Illinois,Missouri,Just Browsing"
  confirmation-title="Use code WELCOME10 for 10% off!"
  confirmation-message="You're officially on the list for sweet updates delivered straight to your inbox."
  confirmation-button-text="Shop Betty's"
  confirmation-button-url="https://shopbettys.com"
  recaptcha-site-key="YOUR_INVISIBLE_RECAPTCHA_SITE_KEY"
  wait-for-age-gate="true"
></form-popup>

Attribute names are kebab-case versions of the React props. states is a comma-separated list in HTML. communicationConsents is a JSON array in HTML.

Props / attributes

| Prop | Type | Default | Description | |------|------|---------|-------------| | formType | "email" | "emailName" | "emailPhone" | "emailState" | "emailState" | Which fields to show | | hubspotPortalId | string | — | HubSpot portal ID (required). Use "demo" to skip the API in local previews. | | hubspotFormId | string | — | HubSpot form GUID fields are submitted to (required) | | emailProperty | string | "email" | HubSpot property name for the email address | | firstNameProperty | string | "firstname" | HubSpot property name for first name (emailName) | | phoneProperty | string | "phone" | HubSpot property name for phone (emailPhone) | | stateProperty | string | "state" | HubSpot property name for the selected state | | additionalPropertyName | string | — | Extra HubSpot property name (for lists/segments) | | additionalPropertyValue | string | — | Value sent for additionalPropertyName | | consentToProcessText | string | — | GDPR processing-consent checkbox label. Omit to skip consent UI and legalConsentOptions | | communicationConsents | CommunicationConsent[] | string | — | Optional marketing opt-ins. React: array. HTML: JSON array. Each item: subscriptionTypeId, text, optional required | | formTitle | string | — | Form-screen headline (required) | | formSubtitle | string | — | Form-screen supporting line | | states | string[] | string | — | Radio options for emailState. React: array. HTML: comma-separated | | submitButtonText | string | "Submit" | Form submit label | | emailPlaceholder | string | "Email *" | Email input placeholder | | firstNamePlaceholder | string | "First name *" | First name placeholder (emailName) | | phonePlaceholder | string | "Phone *" | Phone placeholder (emailPhone) | | stateLabel | string | "Select A State *" | State radio group label | | confirmationTitle | string | — | Confirmation-screen headline (required) | | confirmationMessage | string | — | Confirmation-screen supporting line | | confirmationButtonText | string | — | Confirmation CTA label (hidden if omitted) | | confirmationButtonUrl | string | — | Confirmation CTA destination | | recaptchaText | string | "This site is protected by reCAPTCHA." | Footer disclaimer when Google reCAPTCHA is off. Hide with "" or "false". Ignored when recaptchaSiteKey is set (required branding is shown instead). | | recaptchaSiteKey | string | — | Google reCAPTCHA v2 Invisible site key. Runs an invisible challenge before HubSpot submit | | backgroundColor | string | #d4ecec | Modal card background | | overlayColor | string | rgba(0, 0, 0, 0.55) | Page overlay | | textColor | string | #111111 | Text color | | buttonColor | string | #000000 | Button background | | buttonTextColor | string | #ffffff | Button text | | buttonHoverColor | string | #333333 | Button hover background | | buttonHoverTextColor | string | #ffffff | Button hover text | | fontFamily | string | system sans | Body / UI font stack | | headingFontFamily | string | Georgia / serif | Headline and submit font stack | | waitForAgeGate | boolean | false | Wait for @jmeirinkmarimed/age-gate: localStorage.ageVerified === "true" and #age-gate-modal gone | | waitForStorageKey | string | — | Wait until this localStorage key equals waitForStorageValue | | waitForStorageValue | string | "true" | Expected value for waitForStorageKey | | waitUntilHidden | string | — | Wait until this selector matches no overlay (light DOM or <age-gate> shadow root) | | waitUntilHiddenHost | string | "age-gate" | Shadow-host selector for waitUntilHidden |

Form types

| formType | Fields | |------------|--------| | emailState (default) | Email + state radios — original Betty's layout | | email | Email only | | emailName | First name + email | | emailPhone | Email + phone |

states is only used for emailState. First name and phone HubSpot property names default to HubSpot's firstname and phone.

Behavior

  • Closing the popup (X, overlay click, or Escape) hides it for the current page view only.
  • After a successful HubSpot submit, localStorage is set so the popup does not return on later visits. The confirmation screen still shows until the user closes it or follows the CTA.
  • Set hubspotPortalId="demo" in local examples to skip the network call and still reach the confirmation screen.
  • HubSpot forms with a required GDPR processing checkbox need consentToProcessText (copy the checkbox label from the form). Optional email-subscription checkboxes go in communicationConsents, using each checkbox’s HubSpot subscription type ID. Leave both unset for forms with no legal-consent options.
  • Invisible Google reCAPTCHA: create a reCAPTCHA v2 Invisible key, pass it as recaptchaSiteKey / recaptcha-site-key, and turn off HubSpot’s form CAPTCHA. The Forms API returns FORM_HAS_RECAPTCHA_ENABLED if HubSpot’s own CAPTCHA is on. The Google badge is hidden; required Privacy Policy and Terms links are shown in the footer.
  • Both this popup and @jmeirinkmarimed/age-gate use a very high z-index. Put <age-gate> on the page and set wait-for-age-gate="true" so the form waits until ageVerified is "true" and #age-gate-modal has unmounted. Do not treat a missing overlay as done — the gate may not have rendered yet. Returning visitors already in localStorage see the form immediately. Put <age-gate> before <form-popup> in the theme. When reCAPTCHA is on, the overlay sits just below Google’s challenge iframe so the challenge can appear on top.

Custom fonts

Import fonts in your app (or <link> / @font-face in HTML). Set headingFontFamily / fontFamily to match. The Betty's preview loads Alegreya for headlines.

Local development

This repo uses Bun only (bun.lock). Do not commit package-lock.json.

bun install
bun run dev              # playground → http://localhost:5173
bun run build            # dist/ (ES + CJS + UMD + IIFE + .d.ts)
bun run examples:serve   # previews :8080, playground :5173, react-consumer :5174
bun run lint

HTML previews live in examples/previews/ and load a locally built form-popup.min.js (gitignored; created by examples:serve). Customize a preview here, then copy the <form-popup> markup onto the live site.

Publishing

bun run build produces:

| File | Purpose | |------|---------| | dist/form-popup.es.js | ESM (React peer externals) | | dist/form-popup.cjs.js | CommonJS | | dist/form-popup.umd.js | UMD | | dist/form-popup.min.js | Standalone IIFE (React bundled) for <script> / unpkg | | dist/index.d.ts | TypeScript declarations |

Importing the package registers <form-popup> as a side effect. The IIFE build also exposes window.React / window.ReactDOM.

See CONTRIBUTING.md for contribution notes.