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

webflow-vue

v0.3.0

Published

Vue 3 islands on Webflow-rendered DOM — mount, shared state, and CMS parsing, with no build step required.

Readme

Webflow Vue

Vue 3 islands on Webflow-rendered DOM. Webflow owns the markup and the styling; Vue owns the behaviour. No build step required, and no page-wide takeover — the rest of the page stays untouched Webflow, with its own runtime intact.

Status: 0.3.0, unstable. The API still moves. Pin an exact version; expect signatures to change before 1.0.0.

Quick start — two script tags

Paste into your Webflow page's custom code (head):

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/webflow-vue.global.js"></script>

Give any element an id — that is your island. Then add an embed placed after it (a code embed's scripts run at parse time, so an embed above the island mounts nothing):

<script>
  const { ref, computed } = Vue
  const { mountIsland } = WebflowVue

  mountIsland('#counter', 'counter', () => {
    const cups = ref(1)
    const grams = computed(() => cups.value * 18)
    return { cups, grams }
  })
</script>
<!-- the island itself, built in the Designer -->
<div id="counter">
  <button v-on:click="cups++">+</button>
  <span>{{ cups }} cups = {{ grams }} g</span>
</div>

vue.global.js is required rather than the runtime-only build: an island's template is the live Webflow DOM, so the template compiler has to be present.

The bridge (route 2)

Building a project rather than pasting islands into a page? One tag in the page's footer custom code loads everything:

<script
  src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bridge.global.js"
  data-bundle="https://cdn.prod.website-files.com/<site-id>/<asset-id>_main.txt"
></script>

It picks, in the browser, where your island code comes from:

| URL | loads | | --- | --- | | normal | Vue → webflow-vue → your built bundle, in that order | | ?debug | your local Vite dev server, with HMR, and no CDN copies |

The version in that src is the only version pin: the bridge loads the webflow-vue build that shipped beside it, derived from its own URL, so the page and the bundle cannot drift apart.

Optional: data-staging-bundle (a different bundle for *.webflow.io), data-dev, data-vite-client, data-vue, data-library, data-debug-param.

Omit data-bundle and the page tells you so in the console rather than fetching a 404 in silence.

CLI

npx webflow-vue detect <url>    # read a published page and report what it is running
npx webflow-vue init [dir]      # scaffold a project, when a page is not enough

detect fetches the published page and tells you which of the two delivery routes it is on — CDN script tags in page custom code, or a bridge backed by a project bundle — along with the pinned versions, the mountIsland() calls it can read, and the problems that are visible without running anything:

$ npx webflow-vue detect https://my-site.webflow.io/brew

  route 1 — CDN tags in the page's own custom code. No project needed.

    vue          3 — https://unpkg.com/vue@3/dist/vue.global.js
    webflow-vue  0.3.0 — https://cdn.jsdelivr.net/npm/[email protected]
    mounts       brew → [data-brew]
    markup       3 mustache(s), 1 directive(s)

    1 warning(s)
      ! foreign-directive
        v-expand is not a Vue directive — Vue fails with "Failed to resolve
        directive" and the whole island renders nothing

It catches, among others: both routes installed at once (two Vue instances that do not share reactivity), a bridge still carrying its SITE_ID placeholders, {{ mustaches }} on a page that loads no Vue — the usual symptom of island markup living inside a Webflow component that ships site-wide — {{ x.value }}, v-* attributes belonging to some other library, and a <script src> that got line-wrapped and 404s in silence.

Run it before deciding anything, and again after publishing. It takes a local file path as well as a URL.

Agent skill

npx skills add brmbh/webflow-vue

Installs webflow-vue-scaffold, which drives the Webflow MCP to build islands for you. It runs detect first and branches on the answer: a page already on route 1 gets its custom code edited in place; only route 2 gets a bridge, a bundle and a dev server.

API

mountIsland(target, label, setup)      // mounts EVERY match; returns an array of apps
unmountIsland(target)                  // tears them down; returns how many
useSharedStore(name, initial, opts)    // named reactive singleton, optional persistence
useWebflowCMS(options)                 // parse rendered Collection Lists into reactive data
fetchCollection(url, options)          // fetch an item's template page, cached and deduped
loadAllPages(collections, options)     // walk Webflow's pagination past the 100-item limit
useFinsweetList(options)               // read items from Finsweet Attributes instead
cleanDOMForVue(root, label)            // detach/restore Webflow runtime nodes around a mount

setup is Vue's setup(). Whatever object it returns becomes the vocabulary your Designer markup can reference; anything not returned stays private. It is called once per element and receives (el, index).

One element or many — same call

An island is a component definition; it may appear in several places. mountIsland mounts every element that matches, so #counter matching once and [data-brew] matching three times are the same operation. Nothing can silently under-mount.

mountIsland('#counter', 'counter', setup)      // → [app]
mountIsland('[data-brew]', 'brew', setup)      // → [app, app, app]

Prefer an attribute over IDs when a component repeats: adding another mount point is then a Designer action with no code change. target also accepts an element or a list of elements.

Where to declare things

The callback runs once per island, and that single fact decides everything:

| declared | outside the callback | inside the callback | |---|---|---| | plain const | once | once per island — harmless | | computed over shared state | 1 evaluation | N evaluations, same values | | ref | shared by every island | independent per island | | anything using el / index | not available | required |

outside = properties of the thing · inside = properties of this instance

const cups = ref(1)                                  // shared by every island
const grams = computed(() => cups.value * 18)        // evaluated once

mountIsland('[data-brew]', 'brew', () => ({ cups, grams }))
mountIsland('[data-bean]', 'bean', (el) => {
  const qty = ref(1)                                 // this card's own
  const price = Number(el.dataset.price) || 0        // this card's own
  return { qty, price, total: computed(() => qty.value * price) }
})

You probably do not need useSharedStore

Within one script block, a ref declared outside the callback is already shared by every island — they close over the same object. The store earns its place in three cases only:

  1. Separate <script> blocks or embeds, which share no lexical scope.
  2. Separate files you would rather not couple with imports.
  3. Persistence — { persist: true } survives a page load or a client-side navigation. A plain ref resets.

Page transitions (barba, swup, Turbo)

The container is replaced, so islands inside it are destroyed and the fresh markup is never mounted. Re-run the mounts after each navigation:

function mountAll() { /* all your mount calls */ }

mountAll()
if (window.barba) barba.hooks.afterEnter(mountAll)

mountIsland returns the existing app for any root it has already mounted, so re-running is safe. State resets across a transition because the elements are new — use a persisted store for anything that must survive.

Why islands

Wrapping a whole Webflow page in one Vue app breaks Webflow's runtime: Vue's compiler rebuilds the mount target, destroying lightbox JSON configs (script.w-json), slider DOM, and native interactions. Webflow Vue mounts one createApp per interactive component on its own root element — everything outside the islands stays untouched Webflow DOM.

Setup

npm install
npm run dev    # https://localhost:3000 — local dev harness with HMR
npm run build      # dist/main.js — demo app bundle, for Webflow asset upload
npm run build:lib  # dist/webflow-vue.global.js + .esm.js — the library
npm test           # vitest + jsdom

mkcert generates a trusted local cert on first run so the dev server can be loaded into HTTPS Webflow pages without mixed-content errors.

Architecture

  • Vue runtime loaded from CDN (vue.global.js, ships the template compiler). In dev, Vite aliases vue → vue/dist/vue.esm-bundler.js (see vite.config.js) — islands compile the live Webflow DOM as their template, so the runtime compiler must always be present.
  • App code in src/, bundled with Vite as a UMD main.js. Vue is externalized to the CDN global in builds.
  • Mount strategy: mountIsland(target, label, setup) (src/mountIsland.js) — NO template option; Vue uses the live Webflow-rendered DOM as its template. Islands skip themselves when their mount point isn't on the current page, so one bundle serves the whole site.
  • Bridge script (webflow-bridge.html) goes in Webflow page custom code. Routes the bundle source by env: ?debug → local Vite with HMR, *.webflow.io → staging asset, prod host → prod asset. Apply at page level only — site + page double-mounts Vue.

Core modules

| Module | Job | | --- | --- | | src/mountIsland.js | One createApp per island, pre-mount sweep, error handler, timing logs | | src/composables/useSharedStore.js | Cross-island state: named module-scoped reactive() singletons, optional sessionStorage persistence for cross-page state | | src/composables/useWebflowCMS.js | Parses Webflow-rendered Collection List data into a reactive collections object | | src/utils/cleanDOMForVue.js | Rescues script.w-json configs and strips <style> blocks inside a mount target before Vue compiles it |

Shared state between islands

const store = useSharedStore('cart', { items: [] }, { persist: true });

Every island calling useSharedStore('cart') gets the same reactive object — module scope is the singleton boundary, no Pinia needed. With persist: true the state survives reloads and page navigation (badge in the navbar keeps its count on every page).

Conventions (verified live)

  • Custom Element is the default scaffolding primitive. Create elements via element_builder type: "DOM" + set_dom_config: { dom_tag: "..." } — including plain divs. Pick the semantically correct tag (real <button>, not <a href="#">).
  • Long-form Vue directives only on Webflow-rendered DOM: v-on:click ✅ (@click ❌), v-bind:class ✅ (:class ❌). v-model works. Inside .vue SFCs shorthand is fine.
  • CMS data — two conventions, freely mixable per item (both parsed by useWebflowCMS(); grouping key is always the attribute data-field-collection="beans"):
    1. Attribute fields — data-field-name="…" on the Collection Item element. Designer-authored; bind values by hand in the Designer.
    2. Field elements — <span data-field="name">…</span> children whose text is CMS-bound. This is the API-scaffoldable variant: the Webflow MCP can bind CMS fields to element text (set_settings key "text" + cms binding) but not to attribute values.
  • Collection List via MCP: the list's source setting takes static_json {"collectionId":"…"} on the DynamoWrapper element.
  • Styles via MCP: set_style's style_names array resolves as a combo-class chain, not stacked globals — register shared modifiers (e.g. gutter-medium) as combos under each base class.
  • Bundle hosting: Webflow assets reject .js — upload dist/main.js renamed to bundle.txt; the asset CDN serves it without X-Content-Type-Options, so a classic <script src> executes it. Update the bridge by re-registering the same displayName with a bumped version (update_registered_script 404s) and re-applying via set_page_scripts.
  • Known MCP limits: native Lightbox media items aren't writable (Designer only); set_text HTML-escapes (no JSON payloads — use an HtmlEmbed's code setting); body elements reject classes (scope body styles with body:has(#your-island) CSS in head code).

Case studies (live on the sandbox)

Both wireframe-styled, built 100% through the Webflow MCP from one chat session, console.log-instrumented ([webflow-vue:*] prefixes) and verified by driving a real browser.

  1. Webflow Vue Islands — /vue/vueflow-islands?debug The mechanics: counter island + status island sharing one store across page regions, CMS filter island, sessionStorage hydration, cleanDOMForVue rescuing a lightbox config.
  2. Brew Lab — /brew-lab?debug + /brew-lab-about?debug The product-shaped demo: bean grid from a real, MCP-bound CMS Collection List with reactive facets; subscription configurator (grind/size/frequency → live price + discount); navbar cart badge that persists across pages; origin island fetching live Open-Meteo weather for the selected bean's growing region. DOM classed per the Semantic Framework (composition owns spacing, BEM components, is-* combos) so a design pass needs zero DOM/code changes.

Demo flow

  1. Open the Webflow page with ?debug → bridge loads Vue from CDN + bundle from localhost:3000 with HMR
  2. Webflow Designer open on the same page → enables Designer-side MCP calls (headless data_* tools work without it)
  3. Ask Claude: scaffold a variable + matching DOM. Claude edits src/ AND inserts Custom Elements via MCP, then publishes
  4. Vite HMR auto-reloads the live page (the scaffold skill touches src/main.js after publish_site) → code + DOM changes appear together

Stakeholder demo runbook: demo/filter-showoff.md

Companion Skills

Install the upstream skill pack:

claude plugin marketplace add webflow/webflow-skills
claude plugin install webflow-skills@webflow-skills
  • Required: webflow-skills:custom-code-management — bridge registration + page apply
  • Recommended: safe-publish, site-audit, link-checker
  • Local: .claude/skills/webflow-vue-scaffold — orchestrates bridge install, code + DOM co-evolution, publish + auto-reload

Planned / Wanted

  • webflow-vue-data-bridge — scaffold a CMS-bound reactive list end-to-end (the Brew Lab beans pipeline, generalized)
  • webflow-vue-verify — diff the Vue contract (mount selectors, v-* directives, refs) against live Webflow DOM, report drift