Hey there, I’m working with claude code to setup a pretty robust events system in Webflow where I can add recurring events into CMS collection lists (based on some CMS fields, recurring events are duplicated and shown in a variety of different layouts - cards, calendar view etc.).
As a part of this I ran into 2 issues with the Attributes API, I was able to find some workarounds but according to claude they were genuine bugs within the api (I had it confirm after looking at the sourcecode of the rep). so I thought I’d run this by your team so you could take a look.
Apologies if this is totally off-base, I won’t say I understand the issues 100% but thought it would be worth sharing if your team is able to validate and fix them on your end.
Here’s the full bug report I had claude write:
Bug Report: List API — DOM rendering is asynchronous and decoupled from every promise/hook we could find that’s meant to signal “done”
Environment
@finsweet/attributesv2, List module:<script async type="module" src="https://cdn.jsdelivr.net/npm/@finsweet/attributes@2/attributes.js" fs-list></script>- Webflow Collection List with
fs-list-element="list", some instances also usingfs-list-load="all" - Programmatic API via
window.FinsweetAttributes.push(['list', callback]), not the declarative filter UI - Source references below are against the public
finsweet/attributesGitHub repo,packages/list/src/
Bug 1: createItem() items hidden on the first pipeline run only
What we’re doing
We need to render more cards than there are CMS items — e.g., one CMS entry (“Weekly Class”) should produce 4 separate cards for a given month, one per weekly occurrence. Declarative filtering can’t do this (it filters existing items, it doesn’t multiply them), so we use the JS API. Inside an addHook('filter', ...) callback, for each extra occurrence we clone an existing item’s element and register it:
const clone = originalElement.cloneNode(true);
originalElement.insertAdjacentElement('afterend', clone);
const newItem = listInstance.createItem(clone);
// newItem is included in the array returned from the filter hook
This matches the pattern shown in your own docs for addHook('filter', ...) and createItem().
The bug
On page load, the very first time our filter hook runs, the cloned elements are correctly inserted into the DOM and correctly included in the array returned from the hook — but they end up with an inline style="display: none" applied to them and never become visible.
If the exact same hook runs again immediately after — identical data, identical logic, identical result — the clones render correctly. Every run after the first works correctly, indefinitely. Only the very first pipeline run for a given list instance is affected.
What we ruled out
- Not a data/logic bug on our end — we logged the input/output of our hook on both the broken first run and a working later run; identical in every respect (same computed count, same array length returned).
- Not a load-order issue — deferring our first
triggerHook('filter')call made no difference. - Not interceptable via the documented hook lifecycle — we added a cleanup pass on
afterRender(the last phase in the documented lifecycle:start → filter → sort → static → pagination → beforeRender → render → afterRender) that explicitly reset the clones’displaystyle. This did not fix it, meaning whatever applies the hidden style runs after or outside that phase. - What did work: attaching a
MutationObserverto each cloned element to watch forstylechanges and immediately revertdisplay: none. This confirms something in the List module actively hides elements it doesn’t recognize as part of its own tracked item set, through a code path not reachable via any documented hook.
Related finding
Your docs show the fully “correct” way to add an item is not just createItem() + returning it from a hook, but also:
const newItem = listInstance.createItem(element);
listInstance.items.value = [...listInstance.items.value, newItem];
We tried this. Doing it from inside the filter hook caused the page to hang/become unresponsive — consistent with an infinite loop, since mutating items.value seems to re-trigger the same pipeline the filter hook is itself part of. We abandoned this approach as soon as we saw the hang, so we can’t confirm the exact mechanism, but it’s reproducible and severe enough to flag on its own.
Bug 2: loadingPaginatedItems resolves before paginated items are actually in the DOM
What we’re doing
For a different component, we don’t use addHook/createItem at all — we only need to know when a fs-list-load="all" Collection List has finished loading every Webflow-paginated page, so we can safely scan wrap.querySelectorAll(...) ourselves afterward. Per your docs, we await the documented API for this:
window.FinsweetAttributes.push(['list', (listInstances) => {
const listInstance = listInstances.find((l) => l.listElement === list);
Promise.resolve(listInstance.loadingPaginatedItems).then(() => {
// scan the DOM for items here
});
}]);
The bug
loadingPaginatedItems resolves once every page has been fetched over HTTP and merged into list.items.value — confirmed by reading packages/list/src/load/load.ts: the promise (list.loadingPaginatedItems = (async () => { ... })(), lines 37–49) only awaits parallelItemsLoad/chainedPagesLoad, both of which fetch pages and push the parsed items into list.items.value (parseLoadedPage, lines 158–186) — neither function touches the DOM at all.
Actual DOM insertion happens in a completely separate, reactively-triggered pipeline. List watches items via @vue/reactivity’s watch() (components/List.ts line 576: watch(this.items, () => this.triggerHook(key), { immediate: true })), which cascades through the same 8-phase lifecycle as Bug 1 (start → filter → sort → static → pagination → beforeRender → render → afterRender). Two things make this pipeline’s completion decoupled in time from loadingPaginatedItems resolving:
watch()from@vue/reactivityschedules its callback asynchronously (not synchronously in the same tick as theitems.valuewrite), so there’s an inherent gap between “data updated” and “render pipeline even starts.”- The built-in
renderhook (components/List.tslines 494–564) doesawait new Promise(requestAnimationFrame)per item, plus awaits any CSS animations on that item finishing, before considering that item rendered. For a batch of several newly-loaded items, that’s several animation frames (and potentially CSS transition durations) of real wall-clock time between the data being “loaded” and the DOM actually holding those elements.
Concretely reproduced: a 4-page paginated Collection List (fs-list-load="all", page-count element reading “1 / 4”). We awaited loadingPaginatedItems, then did a one-time synchronous scan+hide of every item. Result was non-deterministic across reloads — a captured DOM snapshot showed exactly the last page’s 3 items with no inline style applied at all (not display:none — genuinely untouched by our code), while every earlier page’s items were correctly hidden. They weren’t un-hidden after the fact; they simply weren’t in the DOM yet when our scan ran, despite loadingPaginatedItems having already resolved.
What we ruled out
- Not a data bug — same entries/counts logged before and after, only the timing of DOM presence varied.
- Your README documents
loadingPaginatedItemsas “An awaitable Promise that resolves once all the Webflow CMS paginated items have been loaded.” That reads as “the items are now present,” but per source it only means “fetched via HTTP and merged intoitems.value.” Nothing next to that property, or anywhere else in the README we found, flags that DOM rendering is a distinct, later, asynchronous step driven by the item lifecycle described elsewhere in the same doc. - We looked for a more precise “fully rendered” signal to await instead of guessing with a timer. Internal
currentHook/queuedHookstate on theListinstance (components/List.tslines 857–871, cleared at lines 587–588) does track exactly this — it’sundefined/undefinedonly once the entire cascade, includingafterRender, has fully settled with nothing queued behind it. But neither property appears in the README’s “Reactive properties” or “Standard properties” tables, so they’re undocumented internals with no stability guarantee — depending on them felt riskier than the alternative below.
What worked (our workaround): stopped trusting any single “ready” signal from the API and instead watch the DOM directly — debounce on the list container’s childList mutations (treat it as “settled” once no new item nodes have been appended for ~150ms) before scanning, plus a standing MutationObserver on the container that hides any genuine template item that shows up later than expected, regardless of when it arrives. Implementation-agnostic, but clearly a workaround for a gap that shouldn’t need one.
Questions for your team
-
Is there a supported way to add items via
createItem()from within afilterhook that doesn’t also require mutatingitems.value? -
If
items.valuemutation is required, what’s the safe way to do it from inside a hook that belongs to the same pipeline, without triggering re-entrant/infinite execution? -
Is “elements not in
items.valueget hidden, but only on the first render pass” (Bug 1) expected behavior? If so, is there a hook or signal that would let us handle it reliably, instead of aMutationObserverworkaround? -
Is there a supported, documented way to know when the render pipeline triggered by a paginated-items load has fully completed (all
afterRenderpasses done, nothing queued) — as opposed to just when the fetch/merge step (loadingPaginatedItems) has completed? -
If not, would you consider clarifying
loadingPaginatedItems’s doc entry to explicitly state that it guarantees pages have been fetched and merged intoitems.value, but not that they’ve been rendered into the DOM yet? -
Are
currentHook/queuedHookintended to be a stable, public signal for “pipeline fully idle”? If so, could they be documented and officially exposed as a supported way to detect full settle, rather than something we only found by reading source?Bug Report: List API — DOM rendering is asynchronous and decoupled from every promise/hook we could find that’s meant to signal “done”
Environment
@finsweet/attributesv2, List module:<script async type="module" src="https://cdn.jsdelivr.net/npm/@finsweet/attributes@2/attributes.js" fs-list></script>- Webflow Collection List with
fs-list-element="list", some instances also usingfs-list-load="all" - Programmatic API via
window.FinsweetAttributes.push(['list', callback]), not the declarative filter UI - Source references below are against the public
finsweet/attributesGitHub repo,packages/list/src/
Bug 1: createItem() items hidden on the first pipeline run only
What we’re doing
We need to render more cards than there are CMS items — e.g., one CMS entry (“Weekly Class”) should produce 4 separate cards for a given month, one per weekly occurrence. Declarative filtering can’t do this (it filters existing items, it doesn’t multiply them), so we use the JS API. Inside an addHook('filter', ...) callback, for each extra occurrence we clone an existing item’s element and register it:
const clone = originalElement.cloneNode(true);
originalElement.insertAdjacentElement('afterend', clone);
const newItem = listInstance.createItem(clone);
// newItem is included in the array returned from the filter hook
This matches the pattern shown in your own docs for addHook('filter', ...) and createItem().
The bug
On page load, the very first time our filter hook runs, the cloned elements are correctly inserted into the DOM and correctly included in the array returned from the hook — but they end up with an inline style="display: none" applied to them and never become visible.
If the exact same hook runs again immediately after — identical data, identical logic, identical result — the clones render correctly. Every run after the first works correctly, indefinitely. Only the very first pipeline run for a given list instance is affected.
What we ruled out
- Not a data/logic bug on our end — we logged the input/output of our hook on both the broken first run and a working later run; identical in every respect (same computed count, same array length returned).
- Not a load-order issue — deferring our first
triggerHook('filter')call made no difference. - Not interceptable via the documented hook lifecycle — we added a cleanup pass on
afterRender(the last phase in the documented lifecycle:start → filter → sort → static → pagination → beforeRender → render → afterRender) that explicitly reset the clones’displaystyle. This did not fix it, meaning whatever applies the hidden style runs after or outside that phase. - What did work: attaching a
MutationObserverto each cloned element to watch forstylechanges and immediately revertdisplay: none. This confirms something in the List module actively hides elements it doesn’t recognize as part of its own tracked item set, through a code path not reachable via any documented hook.
Related finding
Your docs show the fully “correct” way to add an item is not just createItem() + returning it from a hook, but also:
const newItem = listInstance.createItem(element);
listInstance.items.value = [...listInstance.items.value, newItem];
We tried this. Doing it from inside the filter hook caused the page to hang/become unresponsive — consistent with an infinite loop, since mutating items.value seems to re-trigger the same pipeline the filter hook is itself part of. We abandoned this approach as soon as we saw the hang, so we can’t confirm the exact mechanism, but it’s reproducible and severe enough to flag on its own.
Bug 2: loadingPaginatedItems resolves before paginated items are actually in the DOM
What we’re doing
For a different component, we don’t use addHook/createItem at all — we only need to know when a fs-list-load="all" Collection List has finished loading every Webflow-paginated page, so we can safely scan wrap.querySelectorAll(...) ourselves afterward. Per your docs, we await the documented API for this:
window.FinsweetAttributes.push(['list', (listInstances) => {
const listInstance = listInstances.find((l) => l.listElement === list);
Promise.resolve(listInstance.loadingPaginatedItems).then(() => {
// scan the DOM for items here
});
}]);
The bug
loadingPaginatedItems resolves once every page has been fetched over HTTP and merged into list.items.value — confirmed by reading packages/list/src/load/load.ts: the promise (list.loadingPaginatedItems = (async () => { ... })(), lines 37–49) only awaits parallelItemsLoad/chainedPagesLoad, both of which fetch pages and push the parsed items into list.items.value (parseLoadedPage, lines 158–186) — neither function touches the DOM at all.
Actual DOM insertion happens in a completely separate, reactively-triggered pipeline. List watches items via @vue/reactivity’s watch() (components/List.ts line 576: watch(this.items, () => this.triggerHook(key), { immediate: true })), which cascades through the same 8-phase lifecycle as Bug 1 (start → filter → sort → static → pagination → beforeRender → render → afterRender). Two things make this pipeline’s completion decoupled in time from loadingPaginatedItems resolving:
watch()from@vue/reactivityschedules its callback asynchronously (not synchronously in the same tick as theitems.valuewrite), so there’s an inherent gap between “data updated” and “render pipeline even starts.”- The built-in
renderhook (components/List.tslines 494–564) doesawait new Promise(requestAnimationFrame)per item, plus awaits any CSS animations on that item finishing, before considering that item rendered. For a batch of several newly-loaded items, that’s several animation frames (and potentially CSS transition durations) of real wall-clock time between the data being “loaded” and the DOM actually holding those elements.
Concretely reproduced: a 4-page paginated Collection List (fs-list-load="all", page-count element reading “1 / 4”). We awaited loadingPaginatedItems, then did a one-time synchronous scan+hide of every item. Result was non-deterministic across reloads — a captured DOM snapshot showed exactly the last page’s 3 items with no inline style applied at all (not display:none — genuinely untouched by our code), while every earlier page’s items were correctly hidden. They weren’t un-hidden after the fact; they simply weren’t in the DOM yet when our scan ran, despite loadingPaginatedItems having already resolved.
What we ruled out
- Not a data bug — same entries/counts logged before and after, only the timing of DOM presence varied.
- Your README documents
loadingPaginatedItemsas “An awaitable Promise that resolves once all the Webflow CMS paginated items have been loaded.” That reads as “the items are now present,” but per source it only means “fetched via HTTP and merged intoitems.value.” Nothing next to that property, or anywhere else in the README we found, flags that DOM rendering is a distinct, later, asynchronous step driven by the item lifecycle described elsewhere in the same doc. - We looked for a more precise “fully rendered” signal to await instead of guessing with a timer. Internal
currentHook/queuedHookstate on theListinstance (components/List.tslines 857–871, cleared at lines 587–588) does track exactly this — it’sundefined/undefinedonly once the entire cascade, includingafterRender, has fully settled with nothing queued behind it. But neither property appears in the README’s “Reactive properties” or “Standard properties” tables, so they’re undocumented internals with no stability guarantee — depending on them felt riskier than the alternative below.
What worked (our workaround): stopped trusting any single “ready” signal from the API and instead watch the DOM directly — debounce on the list container’s childList mutations (treat it as “settled” once no new item nodes have been appended for ~150ms) before scanning, plus a standing MutationObserver on the container that hides any genuine template item that shows up later than expected, regardless of when it arrives. Implementation-agnostic, but clearly a workaround for a gap that shouldn’t need one.
Questions for your team
- Is there a supported way to add items via
createItem()from within afilterhook that doesn’t also require mutatingitems.value? - If
items.valuemutation is required, what’s the safe way to do it from inside a hook that belongs to the same pipeline, without triggering re-entrant/infinite execution? - Is “elements not in
items.valueget hidden, but only on the first render pass” (Bug 1) expected behavior? If so, is there a hook or signal that would let us handle it reliably, instead of aMutationObserverworkaround? - Is there a supported, documented way to know when the render pipeline triggered by a paginated-items load has fully completed (all
afterRenderpasses done, nothing queued) — as opposed to just when the fetch/merge step (loadingPaginatedItems) has completed? - If not, would you consider clarifying
loadingPaginatedItems’s doc entry to explicitly state that it guarantees pages have been fetched and merged intoitems.value, but not that they’ve been rendered into the DOM yet? - Are
currentHook/queuedHookintended to be a stable, public signal for “pipeline fully idle”? If so, could they be documented and officially exposed as a supported way to detect full settle, rather than something we only found by reading source?