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

@finsweet/webflow-astro

v0.2.1

Published

Astro components reproducing Webflow's built-in elements — navbar, dropdown, tabs, slider, lightbox, forms, collection lists

Readme

@finsweet/webflow-astro

Astro components reproducing Webflow's built-in elements — navbar, dropdown, tabs, slider, lightbox, forms, collection lists.

A Webflow page is mostly ordinary HTML plus a set of built-in widgets whose behaviour comes from Webflow's own runtime (webflow.js + jQuery). Migrating to Astro drops that runtime and the widgets stop working. This package supplies them again: the same markup, the same class names, the same ARIA — behaviour reimplemented in TypeScript, no jQuery, nothing bundled from Webflow.

npm install @finsweet/webflow-astro

Astro 7.3+ as a peer dependency.

Converting a Webflow site? Jump to Migrating a Webflow project — it maps every Webflow data-w-type onto a component, and lists the traps that fail silently.

⚠️ This package ships no CSS

Webflow's stylesheet stays the consuming project's responsibility. Include your Webflow export's stylesheets globally, in this order, or none of these components will look like anything:

---
// src/layouts/BaseLayout.astro
import "../styles/normalize.css";
import "../styles/webflow.css";        // widget base rules + the icon webfont
import "../styles/your-site.webflow.css";
---

That split is deliberate: these components emit exactly the class names Webflow emits, so an existing Webflow stylesheet styles them unchanged. It also means a wrong class name would be a silently unstyled element — which is why every component's markup was copied from real published Webflow output rather than written from memory, and why the surprises are documented in the prop docs instead of smoothed over.

Usage

---
import {
  Navbar,
  NavbarContainer,
  NavbarBrand,
  NavbarMenu,
  NavbarLink,
  NavbarButton,
} from "@finsweet/webflow-astro";
---

<Navbar collapse="medium" duration={400}>
  <NavbarContainer class="w-container">
    <NavbarBrand href="/"><img src="/logo.svg" alt="" /></NavbarBrand>
    <NavbarMenu>
      <NavbarLink href="/" current>Home</NavbarLink>
      <NavbarLink href="/about">About</NavbarLink>
    </NavbarMenu>
    <NavbarButton />
  </NavbarContainer>
</Navbar>

Every component:

  • takes class, merged after the Webflow classes, which are never replaced;
  • spreads any other attribute (id, data-*, style, aria-*) onto its root;
  • takes content through slots, never text props;
  • documents every prop — the prop docs are the API reference.

Every component is a named export of the package root. A single-component subpath also works, and is identical in behaviour and in typing — use it to keep a barrel import out of a file that needs only one component:

import Navbar from "@finsweet/webflow-astro/Navbar.astro";

Collection-list helpers are their own subpath, because they are plain TypeScript rather than components:

import { paginate, readPageFromRequest } from "@finsweet/webflow-astro/collection-list";

Types a consumer needs for their own data come from the root:

import type { LightboxItem } from "@finsweet/webflow-astro";

Components

| Family | Components | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | Layout | Container Grid QuickStack Cell Columns Column VFlex HFlex | | Text | Heading List ListItem Link RichText CodeBlock HtmlEmbed WebflowImage WebflowIcon | | Navbar | Navbar NavbarContainer NavbarBrand NavbarMenu NavbarLink NavbarButton | | Dropdown | Dropdown DropdownToggle DropdownList DropdownLink | | Tabs | Tabs TabsMenu TabsLink TabsContent TabsPane | | Slider | Slider SliderMask Slide SliderNav SliderArrow | | Lightbox | Lightbox | | Forms | Form FormSuccess FormError FormLabel TextInput Textarea Select Checkbox Radio FileUpload ReCaptcha FormButton | | CMS | CollectionList CollectionItems CollectionItem CollectionEmpty Pagination PaginationPrevious PaginationNext PaginationCount | | Locales | LocaleList LocaleItems LocaleItem LocaleLink LocaleEmpty | | Media | Lottie Rive Spline BackgroundVideo Video YouTube Facebook Twitter WebflowMap |

Collection lists

CollectionList renders structure only; pagination is a helper, so one component works in a static build and inside a server:defer island.

---
// static — page number is a route segment
import { paginate } from "@finsweet/webflow-astro/collection-list";
const list = paginate(await getCollection("blog"), { limit: 10, page: 1 });
---
<CollectionList>
  {!list.isEmpty && (
    <CollectionItems>
      {list.items.map((post) => <CollectionItem>{post.data.name}</CollectionItem>)}
    </CollectionItems>
  )}
  {list.isEmpty && <CollectionEmpty />}
</CollectionList>

⚠️ A static build cannot vary a prerendered file by query string, so Webflow's ?page=2 link shape does not survive it — pass href to PaginationPrevious / PaginationNext and paginate by route. Query params work inside an island, where readPageFromRequest(Astro.request) reads them.

Cloudflare Turnstile

Set PUBLIC_CF_TURNSTILE_SITE_KEY and Form renders a Turnstile widget inside the form, holding the submit button disabled until Cloudflare returns a token. Unset, no widget is rendered and forms submit normally. There is no prop and no attribute — the same variable is read server-side (to decide whether to render the container) and in the client script (which passes the key to turnstile.render()).

⚠️ The PUBLIC_ prefix is required. Astro inlines only prefixed variables into the client bundle; without it the script would read undefined and the widget would silently never appear. A Turnstile site key is public by design — the secret key stays on your server, and verifying cf-turnstile-response is your endpoint's job.

reCAPTCHA

ReCaptcha takes a siteKey prop and falls back to PUBLIC_RECAPTCHA_SITE_KEY, so a site with one key configures it once. With neither, it renders in Webflow's own invalid-key state (g-recaptcha-error g-recaptcha-disabled g-recaptcha-invalid-key) rather than looking configured but doing nothing.

⚠️ Unlike Turnstile, the key is emitted as data-sitekey — Google's api.js auto-renders by reading it off the element. You must also add the script yourself; neither this package nor Webflow's own forms module loads it:

<script src="https://www.google.com/recaptcha/api.js" async defer></script>

Third-party runtimes

Lottie, Rive, Spline and WebflowMap need a runtime this package deliberately does not bundle — they are loaded on demand, so a page without them pays nothing. Each component's prop docs name what it loads and how to supply a key or pin a version.

Not implemented

Site search (Search::SearchForm, SearchInput, SearchButton). Webflow's search is served by its own index; an Astro site has none, so a migration has to pick a backend first. Nothing is stubbed.

Form submissions have no endpoint. Form takes an action and drives the success/error flow against it, but the endpoint is yours to write — see the TODO in src/scripts/form.ts. There is deliberately no default: a wrong one would silently POST user data at a 404.

Migrating a Webflow project

Everything below is the conversion cheatsheet: look up a Webflow element's type, emit what the right-hand column says. It is written for an agent converting a Webflow export, and it lives in this README so it travels with the installed package.

Identifying an element

Two keys, and they are not the same string:

| Source | Field | Example | | --- | --- | --- | | whtml.html (Designer markup) | data-w-type | Dropdown::DropdownWrapper | | dom.json (internal schema) | type | DropdownWrapper |

WHTML namespaces the type; dom.json does not. Match on the bare type and you match both. The table below is sorted by it.

Settings live in data-props-* JSON in WHTML and in dom.json's data object.

⚠️ Four types exist only in dom.json — DynamoList, DynamoItem, LocalesList, LocalesItem. WHTML flattens them away, so a converter reading only WHTML cannot see a collection list's item template. Read dom.json for anything repeating.

⚠️ Webflow's published HTML is a post-JavaScript snapshot. Runtime ARIA (aria-controls, aria-expanded), generated ids (w-dropdown-list-0), the navbar overlay and inline style widths are added by webflow.js after load — they are not in the static export. These components server-render the static half and assign the runtime half themselves, so do not copy those attributes into your output.

The map

| Webflow type | Astro | Notes | | --- | --- | --- | | Layout | | | | Block | <div> | No component. The most common element by far. | | Section | <section> | No component. | | Paragraph | <p> | No component. | | Blockquote | <blockquote> | No component. Webflow gives it no w- class and no settings. | | BlockContainer | Container | w-layout-blockcontainer w-container | | Grid | Grid | | | Layout | QuickStack | Grid definition is CSS, not props — see #w-node- below | | Cell | Cell | No settings at all | | Row | Columns | Row carries no span info; the columns do | | Column | Column | span/medium/small/tiny, 1–12 | | VFlex / HFlex | VFlex / HFlex | | | DropTarget | a layout's <slot /> | Designer-only template slot, not an element | | Text | | | | Heading | Heading | level 1–6 | | List / ListItem | List / ListItem | tag="ul"\|"ol", unstyled → w-list-unstyled | | Link | Link | button → w-button, block → w-inline-block | | RichText | RichText | Pass Webflow's HTML to html; rendered verbatim | | Strong/Emphasized/Superscript/Subscript | <strong>/<em>/<sup>/<sub> | Inside rich text only | | CodeBlock | CodeBlock | Highlighting is baked in at publish time | | HtmlEmbed | HtmlEmbed | Prefer the html prop; children get compiled by Astro | | DOM | the tag itself | Webflow's arbitrary-HTML escape hatch. Tag + data-props-attributes ({name,value}[]) | | Image | WebflowImage | ImageMetadata → Astro <Image>; string → plain <img>. Named to avoid colliding with astro:assets' own Image, which it wraps. | | Icon | WebflowIcon | Empty div; glyph comes from the icon webfont | | Navigation | | | | NavbarWrapper | Navbar | animation, collapse, duration, easing, easing2, docHeight, noScroll | | NavbarContainer NavbarBrand NavbarMenu NavbarLink NavbarButton | same names | NavbarLink takes current → w--current | | DropdownWrapper | Dropdown | hover, delay | | DropdownToggle DropdownList DropdownLink | same names | Toggle content is a slot — caret included | | TabsWrapper | Tabs | easing, fadeIn, fadeOut | | TabsMenu TabsLink TabsContent TabsPane | same names | tab pairs a link to its pane | | SliderWrapper | Slider | delay, animation, autoplay, infinite, disableSwipe, … | | SliderMask SliderNav SliderArrow | same names | | | SliderSlide | Slide | ⚠️ the one slider name that differs | | LightboxWrapper | Lightbox | Media group serialises into a script.w-json payload | | Forms | | | | FormWrapper + FormForm | Form | One component renders both. action is required for JS submit | | FormSuccessMessage / FormErrorMessage | FormSuccess / FormError | Also the success/error slot defaults | | FormBlockLabel | FormLabel | Webflow emits no w-* class here | | FormInlineLabel | — | Rendered by Checkbox/Radio; a <span>, not a <label> | | FormTextInput | TextInput | | | FormTextarea | Textarea | | | FormSelect | Select | | | FormCheckboxWrapper + Input | Checkbox | custom → the styled-div variant | | FormRadioWrapper + Input | Radio | custom → the styled-div variant | | FormFileUpload* (15 types) | FileUpload | One component renders the whole widget | | FormReCaptcha | ReCaptcha | | | FormButton | FormButton | wait → data-wait label swap | | SearchForm/SearchInput/SearchButton | TODO — not implemented | Webflow's search is server-side; pick a backend first | | CMS | | | | DynamoWrapper | CollectionList | | | DynamoList (dom.json only) | CollectionItems | hide → w-dyn-hide when empty | | DynamoItem (dom.json only) | CollectionItem | repeater → w-dyn-repeater-item | | DynamoEmpty | CollectionEmpty | | | Pagination | Pagination | | | PaginationPrevious PaginationNext PaginationCount | same names | Prev/next render nothing when out of range | | LocalesWrapper | LocaleList | | | LocalesList/LocalesItem (dom.json only) | LocaleItems/LocaleItem | | | LocalesEmpty | LocaleEmpty | | | — | LocaleLink | hreflang is the locale tag (es-ES), not its subdirectory (es) | | Media & widgets | | | | Animation | Lottie | | | Rive | Rive | | | Spline | Spline | | | BackgroundVideoWrapper (+ 3 button types) | BackgroundVideo | One component renders the whole widget | | Video | Video | Keep the embedly URL; don't download | | YouTubeVideo | YouTube | | | Facebook | Facebook | Pure iframe — no JS | | Twitter | Twitter | Pure iframe — no JS | | MapWidget | WebflowMap | Needs a Google Maps API key. Named to avoid shadowing the global Map constructor. |

Sub-types absorbed by a parent component

These data-w-types have no component of their own — one parent renders the whole widget. Listed so a search for the exact type string lands somewhere.

| Webflow type | Rendered by | | --- | --- | | FormCheckboxInput | Checkbox | | FormRadioInput | Radio | | FormFileUploadWrapper, FormFileUploadDefault, FormFileUploadInput, FormFileUploadLabel, FormFileUploadText, FormFileUploadInfo, FormFileUploadUploading, FormFileUploadUploadingBtn, FormFileUploadUploadingIcon, FormFileUploadSuccess, FormFileUploadFile, FormFileUploadFileName, FormFileUploadRemoveLink, FormFileUploadError, FormFileUploadErrorMsg | FileUpload | | BackgroundVideoPlayPauseButton, BackgroundVideoPlayPauseButtonPlaying, BackgroundVideoPlayPauseButtonPaused | BackgroundVideo | | PaginationPreviousIcon | PaginationPrevious | | PaginationNextIcon | PaginationNext |

Traps

#w-node-… ids are load-bearing CSS. Webflow writes grid placement into the stylesheet, keyed on element id:

#w-node-_1e54fc0e-…-2869e666 { grid-template-columns: 1fr 1fr; }

Copy the id verbatim onto the element or its layout silently does not apply. It passes through {...rest}. This is also why QuickStack has no columns prop — an inline style would outrank those rules, including inside media queries.

Images: don't reach for Astro's <Image />. WebflowImage deliberately uses getImage() and writes its own <img>, because <Image> injects width, height and decoding that Webflow never publishes — enough to change layout on a site whose CSS sizes images by class. Pass an imported ImageMetadata and you get Webflow's exact tag, with its own 500/800/1080/1600/2000/2600/3200 srcset ladder; pass a string and you get a bare <img src>, as Webflow publishes for SVGs and CDN-hosted images.

Custom checkbox/radio knobs go in the after slot. Webflow puts a styled toggle after span.w-form-label, inside the <label> — a sibling, not a child:

<Checkbox name="analytics" id="analytics" labelClass="prefs_label">
  Analytics<Fragment slot="after"><div class="prefs_toggle" /></Fragment>
</Checkbox>

⚠️ Nesting it in the default slot instead looks identical in a structural diff and breaks silently: a site whose label CSS is display: none hides the knob with it.

BackgroundVideo needs showPlayPause passed explicitly. It defaults off, because Webflow publishes the control only when the author added the button. The signal in the source data is a BackgroundVideoPlayPauseButton node.

A <div class="w-form"> can carry its own id. {...rest} on Form goes to the inner <form>; the wrapper is reached with wrapperAttrs={{ id: "form" }}.

Tabs: current on the wrapper does nothing. webflow.js finds the starting tab with .w--current on the links, never from the wrapper attribute. Set current on the TabsLink (and matching TabsPane), not on Tabs.

CMS field bindings. Most fields bind into an existing element rather than adding one. Three that surprise:

  • Number → an attribute on the item (data-number="8"), never text.
  • Switch → no markup at all; it is a build-time filter. In Astro, a getCollection filter callback.
  • Color → inline style on the item (border-color, background-color).
  • Multi-Image and Multi-Reference → a genuinely nested collection list.
  • Reference → one field feeds several elements via dotted paths (reference.fullSlug, reference.icon, reference.name).

Pagination differs by build mode. Webflow uses ?page=2. A static Astro build cannot vary a prerendered file by query string, so pass href to PaginationPrevious/PaginationNext and paginate by route segment. Query params work in a server:defer island — use readPageFromRequest(Astro.request).

Page scaffolding a converter must carry over: stylesheet order (normalize.css, webflow.css, <site>.webflow.css), <html data-wf-page data-wf-site>, and the inline script adding w-mod-js/w-mod-touch.

Component naming

Components take human names, not Webflow's internal ones — Lottie not Animation, Form not FormWrapper, CollectionList not DynamoWrapper. The src/components/ui/*.astro paths in Webflow export notes describe a consuming project's layout, not this package's.

Contributing

CONVENTIONS.md is the contract every component follows. The short version: class names come from Webflow's published output and the file wins over your expectation; behaviour uses enhance() because an Astro <script> is hoisted once per page and does not run per instance.

pnpm check   # astro check + tsc
pnpm build   # compiles src/lib → dist