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

@demoloop/react

v1.0.0

Published

React components for embedding Demoloop AI demos.

Readme

@demoloop/react

React components for embedding Demoloop AI demos in your site.

Drop in a provider, a floating CTA, an inline button, or a full Meet-style hero section — all wired to the same start card, which Demoloop's own script opens over your page.

🔗 Live demo → react-demo.demoloop.ai — every component on one page.

Install

npm install @demoloop/react

Peer dependencies: react >=18, react-dom >=18. No other runtime deps — no Tailwind, no framer-motion, no icon library to install. The components ship their own plain CSS and SVG icons, inject them into <head> once on first mount, and stay out of your design system's way.

Usage

Wrap your app (or any subtree) with DemoloopProvider, then place any of the buttons or sections inside it.

import {
  DemoloopProvider,
  DemoloopFloatingButton,
  DemoloopButton,
  DemoloopSection,
} from "@demoloop/react";

export default function Layout({ children }) {
  return (
    <DemoloopProvider agentId="agt_xxx">
      {children}
      <DemoloopFloatingButton position="right-center-low" />
    </DemoloopProvider>
  );
}

That's it — no CSS imports, no Tailwind content-path configuration, no plugin wiring.

The microphone and your Permissions-Policy

The demo runs in a frame from https://app.demoloop.ai, and the visitor talks to it, so that frame needs the microphone. Demoloop's script and DemoloopEmbed delegate it themselves (allow="microphone …" on the frame). Most sites need nothing else.

If your site sends a Permissions-Policy header (or <meta http-equiv>) that mentions microphone, its allowlist must include both your own origin and ours:

Permissions-Policy: microphone=(self "https://app.demoloop.ai")

self is the part that is easy to leave out, and leaving it out breaks the demo silently. A frame only gets a feature that the page around it is allowed too. With microphone=("https://app.demoloop.ai"), your page is not allowed the microphone, so neither is the demo inside it. The browser then refuses without ever showing a prompt, and the demo tells the visitor the page will not let it ask (they can still type in the chat). microphone=() blocks it the same way. A header with no microphone entry is fine: the default already allows your page, and the iframe's allow passes it on.

Components

DemoloopProvider

Holds the offer (where demos may run, and what for) and exposes open() / close() to all children.

| Prop | Type | Default | Description | | ------------- | --------------------- | ----------------------------- | ---------------------------------------------------- | | agentId | string | — | The agent. One agent per page. With none, triggers render but open() does nothing. | | baseUrl | string | https://app.demoloop.ai | Override for self-hosting. A trailing /d (what 0.x asked for) is ignored. | | copy | DemoloopCopyInput | English | Every string the SDK renders, as one typed object. See Copy. | | theme | "light" \| "dark" | "dark" | Look of the SDK's own triggers. The start card follows your page. | | lang | "en-US" \| "es-ES" \| string | — | The language the start card opens in (the visitor can change it there). Without it, your page's <html lang>. | | email | string | — | Who the visitor is, when your app knows. With it the card does not ask for an email. Unverified, as a typed address is. | | name | string | — | Their name, passed on with email (and only with it), so the agent never asks for it. | | surfaces | ("call" \| "in-app")[] | ["call"] | Where demos may run, in the order to offer them. See Surfaces and modes. | | modes | ("tour" \| "help")[] | ["tour"] | What demos may be for, in the order to offer them. | | feature | string | — | Default focused feature id: tours are narrowed to that feature. | | onOpen | (info: DemoloopOpenInfo) => void | — | Called every time a trigger opens the start card. See Tracking. | | loadScript | "mount" \| "open" | "mount" | When Demoloop's script loads. "open" loads it only once a demo is started (or one is already running in the tab). See One script, every demo. |

DemoloopFloatingButton

Fixed pill anchored to one of 8 positions. Hidden on mobile.

| Prop | Type | Default | | ------------ | ----------------- | -------------------- | | position | DemoloopPosition | — (required) | | label | string | "See it live" | | subtitle | string | "Do an AI demo now"| | shortLabel | string | "Live demo" |

DemoloopPosition is "right-center-low" | "right-center" | "right-bottom" | "right-top" | "left-center-low" | "left-center" | "left-bottom" | "left-top".

DemoloopFloatingOrb

Floating "AI aura" orb pinned to a bottom corner — a glowing, morphing gradient ring around the agent avatar (à la ElevenLabs). Clicking it opens Demoloop's start card.

| Prop | Type | Default | | ----------------- | ------------------------------- | ------------------ | | position | "right-bottom" \| "left-bottom" | "right-bottom" | | label | string | "Talk to our AI" ("Need help?" when it offers help only) | | content | "avatar" \| "sparkles" \| "orb" | "avatar" | | surfaces | ("call" \| "in-app")[] | the provider's | | modes | ("tour" \| "help")[] | the provider's | | feature | string | the provider's | | draggable | boolean | true | | persistPosition | boolean | true | | dismissible | boolean | true | | closeLabel | string | "Hide for now" |

Visitors can drag the orb anywhere on screen when it covers something they need — press and move, and it stays put (clamped to the viewport, and re-clamped on resize). With keyboard focus, the arrow keys nudge it (hold Shift for bigger steps). A drag never triggers the click, so the demo only opens on a real tap. The dragged position is remembered in localStorage, scoped to the agent and corner; set persistPosition={false} to reset it on every page load, or draggable={false} to pin the orb in place.

A small close button appears on the orb on hover or focus (always visible on touch, where there is no hover). Clicking it hides the orb for that browser session only — the dismissal lives in sessionStorage, scoped to the agent and corner, so the orb is back on the visitor's next visit or in a new tab. Pass dismissible={false} to make the orb un-closeable, and closeLabel to translate the button's accessible label.

The label shows as a tooltip on hover/focus. Set content="orb" for the pure living-gradient sphere with no avatar or icon. Set modes={["help"]} to turn the orb into a floating customer-support bubble, or pass a feature id to open a targeted single-feature tour — see Surfaces and modes.

DemoloopButton

Drop-in inline button. Accepts standard <button> attributes plus children, className, showIcon. With no children it reads copy.inlineButton.label ("See it live").

DemoloopSection

Full-bleed section with a blurred Meet-style backdrop. Renders the centered title + subtitle as the foreground composition.

| Prop | Type | Default | | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | title | string | "You ask. It shows you." | | subtitle | string | "An AI agent uses the product while you watch. Pick a feature, change your mind, interrupt anytime. It adapts." | | buttonLabel | string | "Start the demo" |

DemoloopLiveSection

Mock of the actual live-call view as a marketing section. Headlines / surrounding copy are intentionally not part of this component — wrap it in your own section markup.

| Prop | Type | Default | | ------------- | -------- | --------------------- | | buttonLabel | string | "Join the live demo" | | chromeLabel | string | "live demo" | | agentName | string | "Demo agent" |

DemoloopPreviewSection

Centered card with a browser-chrome strip, agent avatar, tagline, and a "Start the demo" button. Headlines / surrounding copy are intentionally not part of this component — wrap it in your own section markup.

| Prop | Type | Default | | -------------- | --------- | ------------------------------------------------ | | displayName | string | "Your demo agent" | | tagline | string | "Will share a website and talk with you live" | | buttonLabel | string | "Start the demo" | | chromeLabel | string | "Live demo session" | | showChrome | boolean | true |

DemoloopEmbed

Inline live-demo iframe of the agent's hosted start page — no overlay. Use this when you want the demo to live directly in a page rather than behind a CTA.

| Prop | Type | Default | Description | | ------------- | ------------------- | ----------- | ------------------------------------------------------------------------ | | aspectRatio | string | "16/10" | CSS aspect ratio. Ignored when height is set. | | height | string \| number | — | Explicit height (number is px). Useful for full-bleed embeds. | | framed | boolean | true | Wraps the iframe in a themed rounded card with ring and shadow. | | title | string | "Demo" | Iframe accessibility title. |

Until the provider has an agentId the card renders empty, with no iframe. The iframe's url carries iframe=1, which tells the demo it is embedded so it lays itself out edge to edge.

<DemoloopProvider agentId="agt_xxx">
  <DemoloopEmbed aspectRatio="16/9" />
</DemoloopProvider>

useDemoloop()

Hook returning { agentId, url, avatarUrl, theme, surfaces, modes, open, close }. Use it to build your own trigger. url is the agent's hosted start page as a link (what DemoloopEmbed frames, and what a plain <a> can point at): it carries lang, email, theme and feature when set, and mode=support when the first mode is "help".

open(options?):

  • Opens Demoloop's start card for the provider's surfaces and modes, unless options override them for that click. It asks the visitor only what is left open, and goes straight in when there is nothing to choose: one surface, one mode and a known email start the call at once.
  • options is { source?: string; surfaces?: DemoloopSurface[]; modes?: DemoloopMode[]; feature?: string }. source is passed to the provider's onOpen callback (defaults to "custom", see Tracking).
  • Returns a Promise<boolean>: true once the open is handed to the page's script, false when it could not be (no agent, the script would not load, or a close() came first). It never rejects, so a trigger that ignores it is fine; a custom one can say when it did not open.
  • There is no agentId argument: Demoloop's script runs one agent per page.

close() takes the card, or the demo running, off the page. Call it when the user signs out, too: unmounting the provider does not end a running demo, on purpose, since your app's own navigation unmounts it.

One script, every demo

Every demo, a call over your page or the agent in your visitor's own tab, opens through one script: Demoloop's https://app.demoloop.ai/embed/v1.js. DemoloopProvider loads it (from your baseUrl's origin, once per page) and every trigger calls its window.Demoloop.open({ surfaces, modes, feature, lang }), which shows Demoloop's start card in a frame over your page. The SDK draws no modal of its own.

  • Mount the provider on every page of your app, not only where a trigger is: the script is what brings a running demo back after a reload or a full-page navigation.
  • loadScript="open" keeps the script off your pages until it is used: it loads on the first open, or at mount when a demo is already running in this tab (so a reload still rejoins it). Nothing of Demoloop's loads for a visitor who never starts a demo. The trade-off: Demoloop's install check (the Deploy page) only hears from browsers that started one.
  • email and name are passed on as who the visitor is (name only with an email), so they are not asked for either. They are read when the open reaches the script, so props that arrive while it loads still count. Without an email the provider identifies nobody, and whatever your app told the script directly (window.Demoloop.identify) stays.
  • One agent per page. The script runs the first agent it loaded for; a provider for another agent does not switch it (the console says so once).
  • onOpen fires once the open was handed to the script, never for a script that failed to load.

Content-Security-Policy

If your page sends a CSP, it must allow https://app.demoloop.ai in script-src (the script), frame-src (the start card and the demo are frames) and connect-src (the script reports to it). A blocked script (a CSP without them, or an ad blocker) leaves the triggers doing nothing but log the error; the next open tries again.

script-src 'self' https://app.demoloop.ai;
frame-src 'self' https://app.demoloop.ai;
connect-src 'self' https://app.demoloop.ai;

Add those sources to the directives you already have; the lines above only show the Demoloop part. The microphone needs the Permissions-Policy described above.

Surfaces and modes

An open offers two things, both set on the provider and overridable per trigger or per click:

  • surfaces: where the demo runs.
    • "call" (default): the agent shares its screen on your demo account, on a call over your page. Runs anywhere.
    • "in-app": the agent in the visitor's own signed-in tab, on their real account, with a small call widget over the page. Only where in-app is turned on for the agent and your app's address is listed in Demoloop (the exact origin, https://app.example.com; list each subdomain). Elsewhere it is left out of what the visitor is offered. The agent works your app as the visitor would, so it can do anything they can: what it may not do is set with the agent's guardrails in Demoloop.
  • modes: what the demo is for.
    • "tour" (default): a guided walkthrough of the product.
    • "help": a reactive helpdesk, where the agent answers the visitor's questions instead of running a tour.

Several values are offered in the order given, and the card asks the visitor to pick. One surface, one mode and an email leave nothing to ask, so a call starts at once.

// A support bubble for the signed-in app
<DemoloopProvider agentId="agt_xxx" surfaces={["in-app"]} modes={["help"]} email={user.email} name={user.name}>
  {children}
  <DemoloopFloatingOrb /> {/* label defaults to "Need help?" */}
</DemoloopProvider>

Per trigger, over the provider's:

<DemoloopProvider agentId="agt_xxx">
  {children}
  {/* A targeted single-feature tour */}
  <DemoloopFloatingOrb feature="wbf_xxx" label="See this feature" />
</DemoloopProvider>
// Your own trigger, opting into a mode for this click
const { open } = useDemoloop();
<button onClick={() => open({ source: "pricing_help", modes: ["help"] })}>
  Ask a question
</button>

A feature narrows a tour to one feature (or page): it is the id of a feature in Demoloop, perfect for a link you drop straight into an email. A help session ignores it.

Copy

Every string the SDK renders lives in one typed object, DemoloopCopy. Pass any subset as copy on the provider; the rest stays English.

import { DemoloopProvider, type DemoloopCopyInput } from "@demoloop/react";

const es: DemoloopCopyInput = {
  floatingButton: { label: "Ver la demo", subtitle: "Haz una demo con IA ahora", shortLabel: "Demo" },
  floatingOrb: { moveHint: "muévelo con las flechas" },
};

<DemoloopProvider agentId="agt_xxx" copy={es}>

The groups are floatingButton, floatingOrb, inlineButton, section, liveSection, previewSection and embed. The start card and the call are Demoloop's own, in the language the card opens in (lang), so there is no group for them. Because the type is closed, a translation typed as the whole DemoloopCopy is one the compiler checks: every string you have not translated is an error you see, rather than a stray English word you find in production. That includes strings a new version adds, so an upgrade tells you what is left to translate:

import type { DemoloopCopy } from "@demoloop/react";

// Every group, every key. Leave one out and this does not compile.
const es: DemoloopCopy = {
  floatingButton: { label: "Ver la demo", /* … */ },
  // …
};

Spreading defaultCopy into it ({ ...defaultCopy, floatingOrb: { ...defaultCopy.floatingOrb, close: "Cerrar" } }) fills every hole with English and switches that check off. That is fine for a deliberate partial translation, which DemoloopCopyInput already covers.

Individual components still take their own label props, and those win for that one instance — two floating buttons with different labels on one page is a layout decision, not a language. useDemoloopCopy() returns the resolved copy if you are building your own trigger.

Tracking

DemoloopProvider accepts an onOpen callback that fires every time a trigger opens the start card, no matter which component triggered it. It receives a { source, agentId, surfaces, modes, feature } info object (what was offered, in order; agentId is the agent the page's script actually runs) so you can wire analytics in one place:

<DemoloopProvider
  agentId="agt_xxx"
  onOpen={({ source, agentId }) => {
    posthog.capture("demo_opened", { source, agentId });
  }}
>
  {children}
</DemoloopProvider>

Built-in source values:

| Component | source | | ------------------------- | ------------------- | | DemoloopFloatingButton | "floating_button" | | DemoloopFloatingOrb | "floating_orb" | | DemoloopLiveSection | "live_section" | | DemoloopSection | "section" | | DemoloopPreviewSection | "preview_section" | | DemoloopButton | "inline_button" | | useDemoloop().open() | "custom" |

When calling open() directly from your own button, you can supply a more specific source:

const { open } = useDemoloop();
<button onClick={() => open({ source: "hero_button" })}>
  Live demo
</button>

Styling and theming

All styles are plain CSS scoped under dl-* class names, injected into <head> once on first mount. To customize, override the class rules in your own stylesheet — they have low specificity (single-class selectors), so any rule loaded after our injected <style data-demoloop> will win. Theme variants are selected via [data-theme="light"] / [data-theme="dark"].

Since 0.8.0 nothing of the SDK's animates a property the browser has to repaint or lay out on every frame, and nothing animates where nobody can see it:

  • The live section's CTA glow and the join control's glow are clipped, blurred pills with static cones spinning inside them (.dl-aura-spin, two in the CTA, one in the join control). They used to turn an @property angle inside conic-gradient() and morph border-radius, which re-rastered and re-blurred them 60 times a second. The join glow only spins while hovered.
  • The faux cursor is a full-size layer moved with transform (it used to animate top/left).
  • The speaking tile's pulse is an ::after ring whose opacity pulses (it used to animate box-shadow).
  • The orb's counter-spinning cone is a sibling element, .dl-floating-orb__aura-counter, instead of the aura's ::before.
  • The CTA and join buttons have darker fills and no backdrop-filter.
  • The live section marks itself data-dl-offscreen while scrolled away, and everything in it holds still.
  • Reduced motion stops every looping animation.

On an idle page the live section went from about 2s of main-thread work per 10s to 0.2s on screen and nothing off screen. Hooks that no longer do anything: --dl-aura-angle (gone), .dl-live-section__cta-aura::before and .dl-floating-orb__aura::before (move to .dl-aura-spin--counter / .dl-floating-orb__aura-counter), and a background on .dl-live-section__cta-aura or .dl-tile__join-aura (the colours are on .dl-aura-spin now, inside a .dl-aura-clip). The speaking tile's ring is drawn inside the tile, so it lost its outer halo. Under reduced motion the ping and pulse halos are hidden rather than resting as discs.

Upgrading from 0.x

1.0 removes the SDK's own modal: every demo opens through Demoloop's script and its start card (see One script, every demo). Most embeds need only the renames below.

| 0.x | 1.0 | | --- | --- | | surface="hosted" (default) | surfaces={["call"]} (default) | | surface="in-app" | surfaces={["in-app"]} | | mode="demo" | modes={["tour"]} (default) | | mode="support" | modes={["help"]} | | open(agentId, { source, feature, mode }) | open({ source, feature, modes }): one agent per page, so no agentId argument | | useDemoloop().surface / .mode | useDemoloop().surfaces / .modes | | onOpen({ source, agentId, mode }) | onOpen({ source, agentId, surfaces, modes, feature }) | | baseUrl="https://app.demoloop.ai/d" | baseUrl="https://app.demoloop.ai" (the old value still works) |

  • The modal is gone, with everything of it: allowFullscreen, confirmFullscreen, confirmClose, onClose, copy.modal, the .dl-modal-* classes and the data-dl-modal-open attribute. Demoloop's card does its own loading, closing and leaving, in the language set by lang.
  • DemoloopEmbed and the url from useDemoloop() still frame / link the agent's hosted start page.
  • Your CSP must allow https://app.demoloop.ai in script-src, frame-src and connect-src: a hosted-only 0.x embed needed only frame-src.
  • loadScript and name now apply to every demo, not just in-app ones.

License

MIT