How to Create CMS-Driven Tabs in Webflow Without Finsweet Attributes (Step-by-Step Guide)

How to Create CMS-Driven Tabs in Webflow Without Finsweet Attributes (Step-by-Step Guide)

Reading time:
4 min
Table of content
A step-by-step guide to building fully dynamic Webflow tabs with CMS content without plugins, just clean code.

Webflow’s native Tabs element doesn’t support CMS content — and that makes it kind of annoying when you're trying to build fully dynamic tabs. Whether it's for FAQs, service pages, product features, or treatment categories, you quickly hit a wall if you're using Collection Lists. You can't just drop CMS content inside a native tab pane — Webflow won’t allow it.

Sure, tools like Finsweet Attributes offer a plug-and-play workaround (and we’ll mention that later too), but in this tutorial, we’ll show you how to build your own dynamic tab system using a bit of custom code and a clean Webflow setup.

Why not just use Finsweet?

Finsweet’s Attributes are great — fast to implement, reliable, and beginner-friendly. But there are a few good reasons you might want to roll your own:

  • You want to understand how it works under the hood
  • You need more flexibility than pre-built attributes allow
  • You don’t want to add external dependencies to your site
  • You’re optimizing for page speed and want to keep things lightweight
  • Or maybe… you just enjoy building custom solutions and leveling up your Webflow skills 💪

This tutorial walks you through everything — from CMS setup to layout to JavaScript — so by the end, you’ll have a fully functional, scalable tab component that works dynamically with your CMS content.

No plugins. Just Webflow + a sprinkle of JavaScript magic.

🔗 Cloneable Webflow Project

Live demo:
👉 https://cms-tabs-without-finsweet.webflow.io/

Clone it:
👉 Cloneable Webflow Project

Step 1: Set Up Your CMS Collection in Webflow

We’ll build a Product Features Tabs Section powered by a CMS collection. Each tab will represent a product feature (like "Speed", "Security", "Integrations", etc.), and when clicked, it’ll show the full description from the CMS.

  1. Go to the CMS panel in Webflow
  2. Click + New Collection, name it something like Product Features
  3. Add the following fields:
    • Feature Name (Plain Text) — used as the visible tab label
    • Slug (generated automatically) — this will power the sync
    • Description (Rich Text) — displayed inside each tab
    • (Optional) Feature Icon (Image)
    • (Optional) Order (Number) — to sort tabs manually
  4. Add 4–5 sample items like:
    • Speed
    • Security
    • Integrations
    • Customization
    • Support
  5. Publish your site so the CMS data is live

Product Features collection fields
Product Features.
Products Features Collection Items

Step 2: Build a Fully CMS-Driven Tab Layout (No Native Tabs)

We won’t use Webflow’s native Tabs component at all. Instead, we’ll use Collection Lists to generate both the tab buttons and tab content — linked together using the CMS Slug field and a couple of custom attributes.

What You'll Build

  • A Collection List that generates all the tab buttons
  • A second Collection List for the content of each tab
  • A system where each tab and content block is matched using data-tab and data-tab-content attributes

Add the Layout Structure

  1. Add a Section called Tabs Section
  2. Inside, add a Div Block called Tabs Wrapper
  3. Inside that, add two nested divs:
    • Tab Buttons Wrapper
    • Tab Content Wrapper

Create a CMS Collection List for Tab Buttons

  1. Inside Tab Buttons Wrapper, add a Collection List
  2. Bind it to your Product Features collection
  3. Choose a horizontal or vertical layout based on your design
  4. Inside each Collection Item:
    • Add a Link Block with the class tabs-nav
    • Set a custom attribute:
      • Name: data-tab
      • Value: Bind it to the Slug field
    • Inside the Link Block:
      • Add a Text Block (bound to Feature Name)
      • (Optional) Add an Image for icons if using

Style the Active State

  • Create a combo class: tabs-nav is-active
  • Use it to style the active tab differently (e.g., bold text, underline, background)
  • Don’t apply it manually — our script will handle it

Create a CMS Collection List for Tab Content

  1. Inside Tab Content Wrapper, add another Collection List
  2. Bind it to the same Product Features collection
  3. Use a block layout (one item per row)
  4. Inside each Collection Item:
    • Add a Div Block, class tab-content-block
    • Set a custom attribute:
      • Name: data-tab-content
      • Value: Bind it to the Slug
    • Inside that block:
      • Add a Heading (bound to Feature Name)
      • Add a Rich Text Block (bound to Description)
  5. Style the content blocks to:
    • Have display: none by default
    • Use a class like .is-active for the visible block (display: block or use Webflow interactions)
Design setup

Why This Works So Well

  • No need to update tabs manually
  • Fully dynamic — just update the CMS and you're done
  • Layout and logic are separated — which keeps everything clean and flexible
  • Ready for JavaScript interaction and even deep linking

Step 3: Add JavaScript to Sync the Tabs

Now it’s time to make it all work.

This JavaScript connects the tab buttons to the matching content blocks. It also shows the first tab by default and supports deep linking via URL hash.

What the Script Does

  • Listens for clicks on .tabs-nav
  • Applies .is-active to the correct tab and content
  • Shows the first tab by default on page load
  • Enables deep linking via #slug in the URL

Pre-Check Before Adding Script

  • All buttons use data-tab="{{ Slug }}"
  • All content blocks use data-tab-content="{{ Slug }}"
  • You’ve styled .is-active for both tabs and content
 
  document.addEventListener("DOMContentLoaded", () => {
    const tabs = document.querySelectorAll('.tabs-nav');
    const contents = document.querySelectorAll('[data-tab-content]');

    const clear = () => {
      tabs.forEach(t => t.classList.remove('is-active'));
      contents.forEach(c => c.classList.remove('is-active'));
    };

    const activate = slug => {
      const btn = [...tabs].find(t => t.getAttribute('data-tab') === slug);
      const content = [...contents].find(c => c.getAttribute('data-tab-content') === slug);
      if (btn && content) {
        clear();
        btn.classList.add('is-active');
        content.classList.add('is-active');
      }
    };

    // Default: activate first tab (no hash update)
    if (tabs.length && contents.length) {
      activate(tabs[0].getAttribute('data-tab'));
    }

    // Tab click handler
    tabs.forEach(btn =>
      btn.addEventListener('click', e => {
        e.preventDefault();
        const slug = btn.getAttribute('data-tab');
        activate(slug);

        // 🔁 OPTIONAL: Update URL hash on tab click
        history.replaceState(null, null, '#' + slug);
      })
    );

    // ───────────────────────────────────────────────
    // ✅ OPTIONAL: Deep linking & auto-scroll
    // Enables opening a tab via #slug in the URL
    // and scrolls to tab section on load or hash change
    //
    // ⚠️ To use auto-scroll, give your tab section:
    // id="tab-section" 
    // ───────────────────────────────────────────────

    const scrollToTabSection = () => {
      const tabSection = document.getElementById("tab-section");
      if (tabSection) tabSection.scrollIntoView({ behavior: "smooth" });
    };

    // Deep link on load
    const hash = location.hash?.slice(1);
    if (hash) {
      activate(hash);
      scrollToTabSection();
    }

    // Hash change (e.g. clicking a # link in-page)
    window.addEventListener('hashchange', () => {
      const newHash = location.hash?.slice(1);
      if (newHash) {
        activate(newHash);
        scrollToTabSection();
      }
    });

    // ───────────────────────────────────────────────
  });

🔁 Bonus Tip: Slugs + Optional Features

I’m using the Slug field for both data-tab and data-tab-content. It just works better — cleaner URLs like #speed-optimization, no weird characters, and it's perfect for linking tabs.

I also added two optional extras to the script:

  1. Deep linking — so you can open a tab directly using a #slug in the URL
  2. Auto-scroll — the page scrolls to the tab section when a deep link is used

They’re both clearly marked in the code, so if you don’t need them, just delete or comment those parts out. The main tabs still work great without them.

💡 Want to See It in Action?

Live demo:
👉 https://cms-tabs-without-finsweet.webflow.io/

Clone it:
👉 Cloneable Webflow Project