BX Webflow Framework
A barebones, component-first Webflow starter. Custom (Client-First style) class naming, a self-contained per-section script system, an optional smart vendor loader, and full Cursor + Claude Code tooling. Everything beyond the cloneable is optional.
Why it's built this way
Webflow ships per-page CSS, so utility-class frameworks lose most of their payoff while adding composition overhead. This template leans on semantic, component-scoped classes instead — clear to read, easy to map to Figma, and simpler for AI to set properties on without recalling token soup.
blog_, hero_) — self-explanatory, no practical limits.Classes, containers & structure
Client-First with a few tweaks: the component name comes first, element parts follow, separated by underscores (hyphens inside multi-word names).
.[component]_section instead of a generic .section — e.g. .blog-slider_section..[component]_[element] — e.g. .product-card_image_wrap..element_contain.container-medium, not a bare .container-medium.class attribute, so you can drop it when nesting a section (inside a dialog, split page, etc.).Typical section structure
.element_section.padding-global
├── .section-css # per-section CSS embed
├── {{Spacer Component}}
├── .element_contain.container-medium
│ └── .element_layout
├── {{Spacer Component}}
└── .section-js # per-section JS embed (paste minified script here)
Three wiring patterns
Markup hooks always use the data- prefix and the Client-First word pattern:
data-{middle}-{facet}="{value}". Pick the pattern that matches what you're marking.
el; value names the block.data-split-page-el="content"data-dialog-open="hero" ↔ data-dialog-id="hero"data-anim-el="fade-up"Modular section & component scripts
Each section or component owns one script, pasted into its .section-js / .component-js embed.
Scripts are idempotent, multi-instance safe, and declare their own namespace window.bx_[component].
Shared libraries are requested through the dispatcher — never loaded with a raw <script> tag.
/*! 13/03/26 v1 hero-slider: Swiper nav + pagination, A11y */ (() => { const ns = 'bx_hero-slider'; if (!window[ns]) window[ns] = { instances: new Map() }; // Declare what this section needs; the loader unions all // requests on the page and loads each library once. window.bx_vendors.swiper.request( ['Navigation', 'Pagination', 'A11y'], initHeroSlider, { priority: true } // above-the-fold → load immediately ); function initHeroSlider() { // query .hero-slider_section, init each .swiper once } })();
Every code/sections/*.js and code/components/*.js file opens with a fixed three-part preamble:
- Banner — one
/*! … */line: date, version, name, one-line summary (survives minification). - Description — what it does, DOM/classes, vendors, a11y, and any script-only
data-*state. - Data attributes — the author-facing
data-*hooks you wire up in Webflow.
docs/COMPONENT_SCRIPT_PATTERN.md is the canonical
spec for preambles, the dispatcher, and dependency-loading decisions — phrased so an agent can read it and
scaffold a brand-new, on-pattern section script for you. This repo is a ready-made sample project: open it in
Cursor or Claude Code and the rules + pattern doc are already wired in, so “build me a blog slider section”
produces a script that matches everything else here.
Minification & comments
Full source files are encouraged to be heavily commented — the three-part preamble plus inline notes are
what let an agent understand a script's behaviour and safely extend it. You ship the minified version: a Cursor
minification plugin generates a -min companion for each file (e.g. project_section_faq-min.js), and
that’s what goes into the Webflow embed. Never edit the -min files by hand — they’re regenerated
from source.
Smart shared vendor dispatcher
The default head snippet only creates an empty window.bx_vendors object, so the framework stays
optional. Enable the presets a project needs in code/global/src/project_global_vendors.js, minify, and
paste over the placeholder. Sections then request() what they need; the dispatcher collects every
declaration and loads each library once per page with the union of requested modules.
request(modules, cb, { priority }) — structured modules, unioned per page. priority: true bypasses the batch for above-the-fold heroes (Swiper only).request(flags, cb) — Finsweet Attributes v2; fs-* flags applied before load. No priority mode.request(['accordion', 'a11y']) — v1 standalone packages, one load per page.external(url, { async }) — any one-off script; dedupes by URL, returns a Promise.
If no section requests a library on a page, nothing loads for it — zero cost. GSAP is supplied by Webflow when
Interactions need it; don't add a GSAP loader, feature-detect window.gsap instead.
Prefer not to use it? You can always drop a library’s <script> straight into a page’s custom code and
init it yourself — the loader is optional. But the dispatcher exists for a reason: a single deduplicated load per
page, only the modules each page needs, and modular sections that stay self-contained. For optimal performance and
portability, the vendor script is the better default.
docs/COMPONENT_SCRIPT_PATTERN.md. Working in the
sample project with Cursor or Claude Code, you can just ask the agent to enable a preset (Swiper, Finsweet, …)
or add a new one — it edits project_global_vendors.js on-pattern and reminds you to re-minify into the head snippet.
Shipped components & their data hooks
Drop-in scripts that live in code/components/ and code/sections/. Expand each for what it does and the
markup hooks it reads — click any attribute to copy it.
One init per .video_wrap. Lazy by default: inline <video>/<iframe> preload on hover and
play on click; background videos load near the viewport via IntersectionObserver. Handles native files,
YouTube/Vimeo (including Webflow's Embedly wrappers, unwrapped to direct embeds), eager loading, full-file
blob preload, and external play triggers.
data-video-el="…"Slot roles inside the wrap: poster_wrap, bg-video_wrap, main-video_wrap, controls_wrap, controls_play-btn.data-video-loadingOn .video_wrap: lazy (default) or eager.data-srcDeferred URL on <source>/<video>/<iframe>; promoted to src when loaded.data-video-full-preloadOn .video_wrap: full-file blob preload for native inline video (needs CORS).data-video-play ↔ data-video-idExternal trigger anywhere on the page; value matches .video_wrap[data-video-id].
Click-delegated modal. Triggers match a surface by shared data-dialog-id. Supports native
<dialog> (backdrop click) and plain roots (hidden / .is-open). Escape closes; page wrappers go
inert for focus-trap semantics; surfaces under .page-wrapper are reparented to body while open.
Optional per-dialog scroll lock.
data-dialog-openOpen trigger; value = dialog id.data-dialog-video-openOpen trigger that also starts the inline video inside the surface; value = dialog id.data-dialog-idOn [data-dialog-el="wrap"]; paired with open-trigger values.data-dialog-el="wrap"Dialog surface root that opens/closes.data-dialog-el="close"Close control inside the wrap.data-dialog-lock-scroll="true"On the wrap; opt-in page-scroll lock while open.
Loads Finsweet Attributes v1 accordion + a11y once per page through bx_vendors.finsweetV1.
Finsweet owns all behaviour via its markup attributes — no custom init. Pairs with Schema.org FAQ
microdata in the markup. Requires the vendor loader to be enabled.
fs-accordion-element="accordion"Accordion wrapper (the repeating item root).fs-accordion-element="trigger"Clickable toggle / header.fs-accordion-element="content"Collapsible content panel.fs-a11y-*Optional a11y overrides per Finsweet docs.
Builds an in-page table of contents from the content column: slugifies each section's h2 into an id,
clones a link template per section, and sets href="#id". Anchor scrolling (offset, history) is handled by
the global body helper.
data-split-page-el="content"Main column root scanned for sections.data-split-page-el="link"TOC link template (cloned once per section).data-split-page-el="link-title"Title node inside the link; gets the section heading text.data-split-page-el="section"Optional explicit TOC section (e.g. CMS items) instead of direct <section> children.Folder structure
Scripts stay close to what they belong to. Readable source lives in the repo; you minify and paste the output into Webflow.
ProjectName/ ├── .cursor/ # Cursor rules, agents, skills │ ├── rules/ agents/ skills/ │ └── mcp.json # project MCP config (Webflow) ├── .claude/ # Claude Code agents + skills ├── CLAUDE.md # Claude project instructions ├── assets/ # cms/ · meta/ · pages/ ├── code/ │ ├── global/ # head, body, styles (+ src/ for vendor source) │ ├── sections/ # per-section scripts │ └── components/ # dialog, video, … ├── docs/ # this doc + COMPONENT_SCRIPT_PATTERN.md ├── fonts/ └── tools/
Project-level AI skills
Shipped for both Cursor (.cursor/skills/) and Claude Code (.claude/skills/). The AI reads them when
relevant or when you @-mention them.
project_* files to <slug>_*, find-replaces the placeholder in docs/rules/Claude setup, then deletes itself.text-size utility classes and heading-style classes from Figma local text styles, with matching Style Guide samples. Use variables or classes — your call.Editors & rules
Cursor and Claude Code share the same rules — Cursor via .cursor/, Claude Code via CLAUDE.md +
.claude/. Rules cover the section-script pattern, accessibility, asset naming, never editing minified
output, and Webflow/Figma setup.
CLAUDE.md; rules and skills are mirrored. Strong for agentic runs..cursor/mcp.json. Authorize only the current site; launch the MCP Bridge App in Designer for Designer-level tools.Using the cloneable
/project-start, then the section-script system, vendor loader, AI skills and asset pipeline are at your disposal..bx-docs. Delete that one embed when you start a
real project — nothing else depends on it.