React

Install the package and start it from one file imported by your entry point.

Installed with the @userlily/js npm package · Written for React 19 with Vite

What works

ProductStatusNote
Analytics & custom eventsSupported
Feedback widgetSupported
Review badgeSupported
Web pushSupported
Roadmap embedSupported
Feedback, survey & roadmap linksSupported

1. Install the package

Terminal

npm install @userlily/js

2. Add your keys

Vite only exposes variables starting with VITE_ to the browser. They are built in - rebuild after changing them.

.env

VITE_USERLILY_HOST=https://dev.userlily.com
VITE_USERLILY_TRACKING_ID=YOUR_TRACKING_ID
VITE_USERLILY_CAMPAIGN=YOUR_CAMPAIGN_KEY
VITE_USERLILY_BROADCAST_KEY=YOUR_PROJECT_KEY

3. Create the userlily file

A module runs once, however often it is imported - so this starts every product exactly once. Delete the products you do not use.

src/userlily.js

import { init } from '@userlily/js'

init({
  host: import.meta.env.VITE_USERLILY_HOST,
  analytics: {
    trackingId: import.meta.env.VITE_USERLILY_TRACKING_ID,
  },
  widget: {
    campaign: import.meta.env.VITE_USERLILY_CAMPAIGN,
  },
  broadcast: {
    project: import.meta.env.VITE_USERLILY_BROADCAST_KEY,
  },
})

4. Import it from your entry point

Before the app renders. React Router navigation is recorded automatically.

src/main.jsx

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './userlily'
import App from './App'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

5. Send custom events

src/SignupButton.jsx

import { track } from '@userlily/js'

export default function SignupButton() {
  return (
    <button onClick={() => track('signup_completed')}>
      Sign up
    </button>
  )
}

6. Show the review badge

Place <ReviewBadge /> where it should appear.

src/ReviewBadge.jsx

// src/ReviewBadge.jsx
import { useEffect, useRef } from 'react'
import { mountSyndication } from '@userlily/js'

export default function ReviewBadge() {
  const ref = useRef(null)
  useEffect(() => {
    // Returns a cleanup that removes the badge again.
    return mountSyndication({
      host: import.meta.env.VITE_USERLILY_HOST,
      campaign: 'YOUR_CAMPAIGN_KEY',
      target: ref.current,
    })
  }, [])
  return <div ref={ref} />
}

7. Host the push worker

The worker must be served from your own domain at /userlily-sw.js, as JavaScript, with no redirect and over HTTPS. Each of these fails silently. Vite serves public/ at the site root.

Terminal - re-run after updating the package

npx --no userlily copy-sw public

8. Ask for push permission from your own button

Optional. Resolves false when the prompt cannot be shown.

src/NotifyButton.jsx

import { broadcast } from '@userlily/js'

// Shows the opt-in when "When to ask" is set to
// "Only when your own code asks".
export default function NotifyButton() {
  return (
    <button onClick={() => broadcast.prompt()}>
      Get notified
    </button>
  )
}

9. Embed the public roadmap

Turn on “Enable public roadmap” in Roadmap → Settings first.

src/RoadmapEmbed.jsx

import { useEffect, useRef } from 'react'

const ORIGIN = 'https://dev.userlily.com'

export default function RoadmapEmbed() {
  const ref = useRef(null)
  useEffect(() => {
    function onMessage(event) {
      if (event.origin !== ORIGIN) return
      const data = event.data
      if (data?.type !== 'userlily:roadmap:height') return
      if (ref.current && data.height > 0) {
        ref.current.style.height = data.height + 'px'
      }
    }
    window.addEventListener('message', onMessage)
    return () => window.removeEventListener('message', onMessage)
  }, [])
  return (
    <iframe
      ref={ref}
      src={ORIGIN + '/r/YOUR_ROADMAP_KEY?embed=1'}
      title="Product roadmap"
      loading="lazy"
      style={{ width: '100%', height: 720, border: 0 }}
    />
  )
}

Notes

  • Next.js, Remix and other server-rendering React frameworks: see the Next.js guide - init() must run in the browser, from an effect.

Every option, limit and troubleshooting step is in the main docs. Logos are trademarks of their owners and are shown only to indicate compatibility.