Webflow doesn't provide a native way to display the total count of items in your CMS collections, which can be frustrating when you're trying to show statistics like "324 products available" or "1,247 blog posts published". The good news? There's a simple workaround that automatically counts and displays your collection items using a lightweight JavaScript solution that can even handle collections larger than 100 items.
This guide will show you how to implement a CMS item counter that works with any Webflow collection, giving you the ability to display formatted item counts anywhere on your site, and the best part is that it's 100% free forever with no ongoing costs or third-party dependencies.
Showing collection counts provides valuable context and social proof for your visitors, while also enabling dynamic statistics that update automatically as your content grows. Here's why implementing CMS counting makes sense for different scenarios:
The brix-cms counting system we'll implement offers a reliable, performant solution with automatic summing:
Let's implement the counting functionality step by step.
First, we need to add the script that powers the automatic counting and formatting functionality:
1 - Access your Webflow project settings
2 - Navigate to the Custom Code section
3 - Locate the "Before </body> tag" field
Inside that code box, please insert the following code:
<script>
/**
* BRIX Templates CMS Counter for Webflow
* Counts CMS items, displays formatted totals, and automatically sums multiple lists
* to overcome Webflow's 100-item limit.
*
* Features:
* - Automatic filtered item counting for visible lists.
* - Global totals for hidden 'phantom' lists.
* - Supports custom prefixes and suffixes (e.g., "Showing X results").
* - Flexible number formatting ('1,234', '1.2K', '3.5M', '1.2B').
* - Robust change detection for filters, search, and dynamic content.
*
* @version 2.2.0
*/
(function() {
'use strict';
// --- Configuration ---
const COUNT_ATTR = 'brix-cms-count';
const DISPLAY_ATTR = 'brix-cms-display';
const FORMAT_ATTR = 'brix-cms-format';
const PREFIX_ATTR = 'brix-cms-prefix';
const SUFFIX_ATTR = 'brix-cms-suffix';
const ENABLE_LOGGING = true; // Set to false to disable console logging for production
// Debounce and interval timings
const RECOUNT_DEBOUNCE_MS = 80; // Coalesces rapid DOM changes
const WATCHDOG_INTERVAL_MS = 1000; // Safety-net interval
// --- State ---
let recountTimer = null;
let lastKnownCountsSignature = '';
// --- Logging Helpers ---
function log(message, ...args) {
if (ENABLE_LOGGING) {
console.log(`🖥️ BRIX Templates CMS Counter for Webflow: ${message}`, ...args);
}
}
function warn(message, ...args) {
if (ENABLE_LOGGING) {
console.warn(`🖥️ BRIX Templates CMS Counter for Webflow: ${message}`, ...args);
}
}
// --- Initialization ---
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
function init() {
log('Initializing v2.2.0...');
countAndDisplayItems();
observeDomChanges();
// Start the safety-net watchdog
setInterval(watchdogCheck, WATCHDOG_INTERVAL_MS);
}
// --- Core Logic ---
/**
* Main function to count all items and update all display elements.
* Automatically determines whether to perform a filtered or global count.
*/
function countAndDisplayItems() {
const visibleSums = {};
const phantomSums = {};
const hasVisibleSource = {};
const countElements = document.querySelectorAll(`[${COUNT_ATTR}]`);
countElements.forEach((element) => {
const countName = element.getAttribute(COUNT_ATTR);
if (!countName) return;
const isListVisible = isElementActuallyVisible(element);
const items = getDirectDynItems(element);
if (isListVisible) {
// This is a visible list, so we count only its visible items.
const visibleItemCount = items.filter(isElementActuallyVisible).length;
visibleSums[countName] = (visibleSums[countName] || 0) + visibleItemCount;
hasVisibleSource[countName] = true;
} else {
// This is a hidden "phantom" list, so we count all its items for a global total.
phantomSums[countName] = (phantomSums[countName] || 0) + items.length;
}
});
// Resolve final counts: if a visible source exists for a name, use it. Otherwise, fall back to phantom totals.
const finalCounts = {};
const allNames = new Set([...Object.keys(visibleSums), ...Object.keys(phantomSums)]);
allNames.forEach(name => {
if (hasVisibleSource[name]) {
finalCounts[name] = visibleSums[name] || 0;
} else {
finalCounts[name] = phantomSums[name] || 0;
}
});
// Check if counts have actually changed before updating the DOM
const newSignature = JSON.stringify(finalCounts);
if (newSignature === lastKnownCountsSignature) {
log('Counts unchanged, skipping DOM update.');
return;
}
lastKnownCountsSignature = newSignature;
log('Counts changed, updating display.', finalCounts);
updateDisplayElements(finalCounts);
}
/**
* Updates all display elements on the page with the final calculated counts.
* @param {object} finalCounts - An object mapping count names to their final numbers.
*/
function updateDisplayElements(finalCounts) {
const displayElements = document.querySelectorAll(`[${DISPLAY_ATTR}]`);
if (displayElements.length === 0) {
log('No display elements found on this page.');
return;
}
displayElements.forEach((display) => {
try {
const displayName = display.getAttribute(DISPLAY_ATTR);
const count = finalCounts[displayName] ?? 0;
const format = display.getAttribute(FORMAT_ATTR) || 'none';
const prefix = display.getAttribute(PREFIX_ATTR) || '';
const suffix = display.getAttribute(SUFFIX_ATTR) || '';
const formattedCount = formatNumber(count, format);
const finalDisplay = prefix + formattedCount + suffix;
display.textContent = finalDisplay;
// Add title attribute for accessibility (shows full number on hover)
if ((format === 'short' || format === 'comma') && count >= 1000) {
display.setAttribute('title', count.toLocaleString('en-US'));
}
} catch (err) {
warn('Error updating a display element:', display, err);
}
});
}
// --- Helper Functions ---
/**
* Gets only the direct .w-dyn-item children to avoid overcounting nested lists.
* @param {Element} listElement - The .w-dyn-list element.
* @returns {Element[]} An array of direct item elements.
*/
function getDirectDynItems(listElement) {
const itemsContainer = listElement.querySelector('.w-dyn-items');
const container = itemsContainer || listElement;
return Array.from(container.querySelectorAll(':scope > .w-dyn-item'));
}
/**
* Robustly checks if an element is visible to the user.
* Considers display, visibility, hidden attributes, and ancestor properties.
* @param {Element} el - The element to check.
* @returns {boolean} True if the element is visible.
*/
function isElementActuallyVisible(el) {
if (!(el instanceof Element)) return false;
// An element is not visible if it has no rendered dimensions
if (el.getClientRects().length === 0) return false;
// Check computed styles and attributes up the DOM tree
let current = el;
while (current) {
const style = window.getComputedStyle(current);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return false;
}
if (current.hasAttribute('hidden') || current.getAttribute('aria-hidden') === 'true') {
return false;
}
current = current.parentElement;
}
return true;
}
/**
* Formats a number based on the specified format.
* 'comma' is guaranteed to use commas.
* 'short' supports K, M, and B for thousands, millions, and billions.
* @param {number} num - The number to format.
* @param {string} format - The format type ('comma', 'short', 'none').
* @returns {string} The formatted number string.
*/
function formatNumber(num, format) {
switch (format) {
case 'comma':
// Guarantees US-style comma separators, regardless of browser locale.
return num.toLocaleString('en-US');
case 'short':
if (num >= 1_000_000_000) {
return (num / 1_000_000_000).toFixed(1).replace(/\.0$/, '') + 'B';
}
if (num >= 1_000_000) {
return (num / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M';
}
if (num >= 1_000) {
return (num / 1_000).toFixed(1).replace(/\.0$/, '') + 'K';
}
return num.toString();
case 'none':
default:
return num.toString();
}
}
// --- Change Detection ---
/**
* Schedules a debounced recount to avoid excessive calls during rapid DOM changes.
*/
function scheduleRecount() {
clearTimeout(recountTimer);
recountTimer = setTimeout(() => {
log('Debounced recount triggered.');
countAndDisplayItems();
}, RECOUNT_DEBOUNCE_MS);
}
/**
* Sets up a MutationObserver to watch for any relevant DOM changes.
*/
function observeDomChanges() {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
const target = mutation.target;
// Check if the mutation target is relevant to our counting logic
if (target.nodeType === Node.ELEMENT_NODE &&
(target.closest('.w-dyn-list, [brix-cms-count]') || target.hasAttribute(COUNT_ATTR))
) {
scheduleRecount();
return; // No need to check other mutations
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class', 'hidden', 'aria-hidden'],
});
}
/**
* A safety-net function that runs periodically to catch any changes the observer might miss.
*/
function watchdogCheck() {
// This check is lightweight. It recalculates counts in memory but only updates
// the DOM if the final numbers have actually changed.
countAndDisplayItems();
}
})();
</script>
When to use site-wide vs. page-specific implementation:
Follow these steps to create an optimized counting setup that handles any collection size.
This process involves creating a "phantom" or "dummy" collection list that exists purely for counting purposes. It is not your main, visible collection list. This lightweight, hidden list ensures accurate counting without affecting your site's design or performance.
Inside your collection item, include only the bare minimum:
This ensures fast loading without impacting performance.
Select your entire Collection List wrapper and:
Where you want to show the count:
For example, to show "We have [count] products available":
Webflow collection lists can only display a maximum of 100 items at a time. To count more than 100 items, simply add more than one hidden Collection Lists.
Give them all the same brix-cms-count attribute (e.g., "products"). Then, use Webflow's collection settings to paginate them: the first list shows items 1-100, the second shows 101-200 (by setting "Skip" to 100), the third shows 201-300 (by setting "Skip" to 200), and so on.
The script will automatically add them all up to display the correct total.
Make your counts more readable with built-in formatting options. To use them, add the brix-cms-format attribute to your display element with one of the following values:
For example, to display "Total: 50 products", you would add the attribute brix-cms-prefix with a value of 'Total: ' and the attribute brix-cms-suffix with a value of ' products'.
For collections that frequently exceed 1000+ items, adding multiple collection lists can become tedious. In these cases, API-based solutions from services like Jetboost or a custom integration with Webflow's API can provide fully automated counting. These advanced implementations typically require paid subscriptions and/or custom development work. If you need a robust, automated solution for very large collections, our Webflow dev agency can help you with a custom API integration.
Counter shows "0" or doesn't update:
Counter shows wrong number with 100+ items:
Formatting not working:
Performance issues:
Add our counter script to your Webflow project, create collection list(s) with the brix-cms-count attribute, and add text elements with the brix-cms-display attribute where you want counts shown. The script automatically counts items and displays formatted totals, even for collections larger than 100 items.
Simply add multiple collection lists with different skip values (0, 100, 200, etc.) but the same brix-cms-count value. Our script automatically sums all lists with matching names. For example, two lists both marked as brix-cms-count="products" will have their items added together.
Add the brix-cms-format attribute to your display element with values like "comma" for 1,234 formatting or "short" for 1.2K abbreviations. You can also add prefixes and suffixes using brix-cms-prefix and brix-cms-suffix attributes.
The script automatically detects when collection items are filtered (hidden/shown) and updates the count accordingly. This works with Webflow's native filters and third-party solutions like Finsweet CMS Filter.
If configured properly with images disabled and minimal fields displayed, the performance impact is negligible. Always hide counting collections with display: none and avoid loading unnecessary data like images or rich text content.
Use unique values for each collection type (like "products", "posts", "team") in both your brix-cms-count and brix-cms-display attributes. Each type will be counted independently.
Implementing CMS item counting in Webflow fills a crucial gap in the platform's native functionality, allowing you to display dynamic statistics that automatically update as your content grows. Our solution handles collections of any size through automatic summing, making it perfect for everything from small portfolios to large e-commerce stores.
The combination of hidden collections, automatic summing, and flexible formatting ensures you get accurate, beautifully formatted counts without impacting user experience. Whether you're showing "47 products" or "3,847 businesses listed", our free Webflow attribute has you covered.
Need help implementing advanced counting features or complex CMS requirements? Our top-notch Webflow development agency specializes in custom CMS solutions including API integrations, complex filtering systems, and enterprise-level functionality that goes beyond Webflow's native capabilities. Reach out to discuss how we can help optimize your Webflow project with professional, scalable solutions.
Add a functional back button to your Webflow 404 or Thank you page with our lightweight script. Works for internal and external navigation.
Detect and block IE users on Webflow with our free script. Prevents broken layouts and poor user experience. Works with all IE versions!
Add automatic page data tracking to Webflow forms to capture URLs and titles. Works with static pages and CMS collections.