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

@criclabs/universal-floating-contact-button

v1.0.3

Published

Embeddable, framework-agnostic floating contact widget (Phone, LINE, Messenger, and more) built with Lit + Tailwind CSS, shipped as a single standalone script.

Readme

Universal Floating Contact Button

A tiny, embeddable "floating contact button" — the classic bottom-right bubble that expands into Phone / LINE / Messenger / WhatsApp / etc. buttons. Ships as one standalone <script> file (dist/widget.v1.js) that any website can drop in, with zero build step or framework dependency required on the host site (plain HTML, WordPress, Shopify, Webflow, LCS, whatever).

Built with Lit (Web Components + Shadow DOM) and Tailwind CSS compiled inline, bundled by Vite into a single IIFE.

Why Shadow DOM?

The widget renders inside a Shadow Root, so:

  • The host site's global CSS can never restyle/break the widget (no more button { border-radius: 0 !important } fights).
  • The widget's own styles never leak out and affect the host page.

See index.html for a dev page that intentionally ships hostile global CSS to prove this isolation.

Project structure

├── src/
│   ├── components/widget.ts   # <contact-widget-element> — the Lit component
│   ├── assets/icons/           # Heroicons, Remix, and Tabler SVG variants
│   ├── icon-variants.ts        # Maps widget/button types to variant SVG assets
│   ├── icons.ts                # Inline fallback renderers and close icon
│   ├── button-presets.ts       # Per-channel default colors/labels/icons
│   ├── types.ts                # WidgetConfig / ButtonConfig public types
│   ├── utils/link-resolver.ts  # Turns phone/LINE-ID/FB-URL shorthand into real links
│   ├── index.ts                # SDK entry point — mounts the element + processes the command queue
│   ├── styles.css               # Tailwind directives (compiled + inlined into the Shadow Root)
│   └── vite-env.d.ts
├── index.html                  # Dev-only test/demo page
├── vite.config.ts              # Builds a single dist/widget.v1.js IIFE
├── .github/workflows/
│   └── publish-package.yml      # Publishes tagged releases to npm
├── tailwind.config.js
├── postcss.config.js
└── tsconfig.json

Getting started

npm install
npm run dev       # http://localhost:5173 — live dev/test page
npm run build     # -> dist/widget.v1.js (single file, ready to deploy/CDN)
npm run typecheck # tsc --noEmit

Distribution via jsDelivr

The built widget is published as part of the @criclabs/universal-floating-contact-button npm package. jsDelivr mirrors public npm packages automatically, so no S3 bucket, CloudFront distribution, deploy credentials, or cache invalidation is needed. The package includes dist/widget.v1.js, with its default CSS and icons already bundled into that file.

Use a major-version URL in production so compatible v1 updates are picked up without unexpectedly upgrading to a future breaking release:

https://cdn.jsdelivr.net/npm/@criclabs/universal-floating-contact-button@1/dist/widget.v1.js

For a release that must never change, pin the exact package version instead, for example @criclabs/[email protected].

Release process

Releases move from develop to main through a dedicated release PR. npm Trusted Publishing must be configured for this repository and publish-package.yml; no long-lived npm token is required.

  1. Update develop, then create a release branch named for the version being prepared:

    git checkout develop
    git pull
    git checkout -b release/v1.0.1
  2. Choose the appropriate semantic version increment and update the package metadata without creating a Git tag:

    npm version patch --no-git-tag-version # or minor / major
    git add package.json package-lock.json
    git commit -m "Release v1.0.1"
    git push -u origin release/v1.0.1
  3. Open a PR from release/v1.0.1 to main titled Release v1.0.1. Complete the normal review and merge process.

  4. Merging a release/v* PR into main triggers publish-package.yml. The workflow verifies that the branch name matches the version in package.json, type-checks and builds the widget, tags the merge commit, publishes it to npm, and fast-forwards develop to the released commit. jsDelivr then mirrors the new package version automatically.

    The repository must provide a RELEASE_TOKEN Actions secret for an admin user. The token needs repository contents write access so the workflow can create the tag and bypass develop's pull-request requirement. npm itself continues to use Trusted Publishing and does not require an npm token.

  5. Verify the release on npm and at the version-pinned CDN URL:

    https://cdn.jsdelivr.net/npm/@criclabs/[email protected]/dist/widget.v1.js

The workflow still supports manually pushed v* tags as a recovery path. A manual tag must match the version in package.json; it publishes the package but does not update develop.

Published npm versions are immutable. If a release needs a correction, create a new patch version rather than attempting to replace the existing version.

Embedding on any website

Paste this snippet before </body> on any page:

<script>
  (function (w, d) {
    if (w.ContactWidget) return;
    var q = [];
    function ContactWidget() {
      q.push(arguments);
    }
    ContactWidget.q = q;
    w.ContactWidget = ContactWidget;
    var s = d.createElement('script');
    s.async = true;
    s.src = 'https://cdn.jsdelivr.net/npm/@criclabs/universal-floating-contact-button@1/dist/widget.v1.js';
    d.body.appendChild(s);
  })(window, document);

  ContactWidget('init', {
    color: '#EA1C24',
    iconVariant: 'heroicon',
    label: 'Contact us',
    buttons: [
      { type: 'phone', value: '0869828288' },
      { type: 'line', value: '@LCSGROUP' },
      { type: 'messenger', value: 'https://www.facebook.com/lcsgroupthailand' },
    ],
  });
</script>

Because calls are queued into ContactWidget.q until the real script finishes downloading, the init call can appear immediately after the loader (as above) — there's no race condition, and it works the same way whether the widget script has loaded yet or not. This is the same pattern used by Intercom, Google Analytics, Crisp, etc.

Real example — LCS Group Thailand

This is the exact config used in index.html to test the widget with real production contact info:

ContactWidget('init', {
  color: '#EA1C24',
  iconVariant: 'heroicon',
  label: 'ติดต่อเรา',
  buttons: [
    { type: 'phone', value: '0869828288', label: 'โทรเลย' },
    { type: 'line', value: '@LCSGROUP', label: 'แชททาง LINE' },
    { type: 'messenger', value: 'https://www.facebook.com/lcsgroupthailand', label: 'Messenger' },
  ],
});

Note that the Messenger button's value is the Facebook Page URL — the widget automatically rewrites facebook.com/<page> into the correct https://m.me/<page> deep link so the button opens a Messenger chat, not the Facebook page itself.

Configuration reference

WidgetConfig

| Property | Type | Default | Description | | ---------------- | ----------------------------- | ----------------- | -------------------------------------------------------------------- | | color | string (CSS color) | #EA1C24 | Main button color. | | iconVariant | 'heroicon' \| 'remix' \| 'tabler' | 'heroicon' | Built-in icon set for the toggle and channel buttons. | | buttonSize | number (px) | 48 | Diameter of the main toggle and inner channel buttons. | | position | 'bottom-right' \| 'bottom-left' | 'bottom-right' | Which corner the widget docks to. | | offsetX | number (px) | 32 | Horizontal distance from the viewport edge. | | offsetY | number (px) | 32 | Vertical distance from the viewport edge. | | pulse | boolean | false | Animated attention-grabbing ring around the closed button. | | pulseScale | number | 2.4 | Maximum pulse size relative to the circular button. | | pulseDuration | number (ms) | 1800 | Duration of each pulse cycle. | | label | string | — | Tooltip pill shown next to the closed button (e.g. "Contact us"). | | showLabels | boolean | false | Always show each button's label as a pill instead of only on hover. | | autoOpenDelay | number (ms) | — | Auto-expand the menu this many ms after the widget mounts. | | zIndex | number | 999999 | Stacking order override. | | buttons | ButtonConfig[] | required | The channel buttons to render, top to bottom. |

Icon variants

Set iconVariant at widget level to use a consistent icon family, then set buttons[].iconVariant when one button needs a different family. A per-button icon URL still takes priority over both variant settings. The close icon is shared across variants.

| Icon | Heroicons (heroicon) | Remix (remix) | Tabler (tabler) | | --- | --- | --- | --- | | Widget toggle | Heroicons widget | Remix widget | Tabler widget | | Phone | Heroicons phone | Remix phone | Tabler phone | | LINE | Heroicons LINE | Remix LINE | Tabler LINE | | Messenger | Heroicons Messenger | Remix Messenger | Tabler Messenger | | Facebook | Heroicons Facebook | Remix Facebook | Tabler Facebook | | WhatsApp | Heroicons WhatsApp | Remix WhatsApp | Tabler WhatsApp | | Telegram | Heroicons Telegram | Remix Telegram | Tabler Telegram | | Instagram | Heroicons Instagram | Remix Instagram | Tabler Instagram | | TikTok | Heroicons TikTok | Remix TikTok | Tabler TikTok | | LinkedIn | Heroicons LinkedIn | Remix LinkedIn | Tabler LinkedIn | | WeChat | Heroicons WeChat | Remix WeChat | Tabler WeChat | | Map | Heroicons map | Remix map | Tabler map | | Form / custom | Heroicons form | Remix form | Tabler form | | Email | Heroicons email | Remix email | Tabler email |

Messenger, Telegram, and TikTok use their recognizable brand marks across all three families because those logos do not have library-specific variants.

ButtonConfig

| Property | Type | Description | | --------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | | type | 'phone' \| 'line' \| 'messenger' \| 'facebook' \| 'whatsapp' \| 'telegram' \| 'instagram' \| 'tiktok' \| 'linkedin' \| 'wechat' \| 'map' \| 'form' \| 'email' \| 'custom' | Which channel. Controls the default icon/color/link format. | | value | string | A shorthand (phone number, LINE ID, username) or a full URL — see below. | | label | string | Accessible label + tooltip text. Falls back to a sensible per-type default. | | color | string | Override the built-in brand color/gradient for this button. | | icon | string (URL) | Override the built-in icon with your own image (PNG/SVG/CDN link). | | iconVariant | 'heroicon' \| 'remix' \| 'tabler' | Override the widget-level icon family for this button only. | | target | '_blank' \| '_self' | Defaults to '_blank'. | | enabled | boolean | Set false to temporarily hide a button without deleting its config. |

Shorthand → link resolution

You can always paste an already-complete URL/URI, but each channel also accepts a shorthand so non-technical teams (content/marketing) can paste what they already have:

| type | Shorthand example | Resolves to | | ----------- | -------------------------------------------- | ------------------------------------------------- | | phone | "0869828288" | tel:0869828288 | | email | "[email protected]" | mailto:[email protected] | | line | "@LCSGROUP" | https://line.me/R/ti/p/%40LCSGROUP | | messenger | "https://www.facebook.com/lcsgroupthailand" | https://m.me/lcsgroupthailand | | facebook | "lcsgroupthailand" | https://www.facebook.com/lcsgroupthailand | | whatsapp | "66869828288" | https://wa.me/66869828288 | | telegram | "lcsgroup" | https://t.me/lcsgroup | | instagram | "lcsgroup" | https://www.instagram.com/lcsgroup | | tiktok | "lcsgroup" | https://www.tiktok.com/@lcsgroup | | linkedin | "criclabs" | https://www.linkedin.com/in/criclabs | | wechat | "@criclabs" | weixin://dl/chat?criclabs | | map | "Bangkok, Thailand" | Google Maps search URL | | form | any full URL | used as-is | | custom | any full URL | used as-is (uses the selected form icon by default) |

Runtime commands

Once mounted, window.ContactWidget(command, payload) supports:

  • ContactWidget('init', config) — mount (or re-mount) the widget.
  • ContactWidget('update', partialConfig) — merge new config into the live widget (e.g. re-theme per page/section).
  • ContactWidget('open') / ContactWidget('close') — control the menu programmatically.
  • ContactWidget('destroy') — unmount the widget entirely.

The element also dispatches cw-open, cw-close, and cw-button-click DOM events (bubbling, composed) if you want to hook up analytics.

Design notes

  • Single source of truth for links: src/utils/link-resolver.ts centralizes every "shorthand → real URI" rule, so adding a new channel type only requires one resolver + one entry in button-presets.ts.
  • Icon variants: the widget bundles Heroicons, Remix, and Tabler SVG sets and selects them through iconVariant. Any button can still override its icon with a custom image URL.
  • Animation: menu items cascade in/out (animate-cw-pop-in / animate-cw-pop-out, staggered by proximity to the trigger button) and the trigger icon cross-fades between the chat bubble and the close X.
  • Accessibility: every link has an aria-label/title, the trigger button exposes aria-expanded, Escape closes the menu, and clicking outside the widget closes it too.