Slater Data embed

A dependency-free script that adds load-more, filtering, keyword search, sorting, and facets to a Webflow CMS list — plus a no-code global site search across every collection and static page, and merged multi-collection lists. You wire it up entirely with data-slater-* attributes in the Webflow Designer. No JavaScript to write.

Quick start

Webflow stamps data-wf-site on <html> and <html lang> is the locale, so the minimum is:

<!-- On the collection list wrapper -->
<div class="w-dyn-list" data-slater-list="blog">
  <div class="w-dyn-items" data-slater-results="blog">
    <!-- One card, marked as the template -->
    <div class="w-dyn-item" data-slater-template>
      <h2 data-slater-bind-text="heading"></h2>
      <p data-slater-bind-text="description"></p>
    </div>
  </div>
</div>

<!-- Load the script once, before </body> -->
<script src="https://api.slater.app/embed/v1/slater-search.js" defer></script>

That renders the list from the API. Add controls (search, filters, load-more) from the feature guides.

Loading the script + config

<script src="https://api.slater.app/embed/v1/slater-search.js" defer></script>

Most settings can come from attributes, but a window.SlaterSearchConfig block is handy for per-list defaults. Put it before the script tag. Attributes win over config, which wins over the defaults.

<script>
  window.SlaterSearchConfig = {
    // siteId and locale are auto-read from the page if you omit them.
    lists: {
      blog: {
        collection: "blog-posts",   // Webflow collection slug (defaults to the list key)
        initialLimit: 12,           // items on first load / per page
        loadMoreAmount: 12,         // items added per Load More click
        sort: "-lastPublished",     // initial sort (optional)
        detailPath: "/{locale}/blog/{slug}",  // card link pattern
        facets: ["type", "category"],         // auto-fill these dropdowns
        expand: ["*"],                        // full data from linked collections
        searchFields: ["titre"]               // restrict the search box
      }
    }
  };
</script>
The snippets on this page already point at this environment's API. Your *.webflow.io staging domain automatically routes to the correct index - no extra setup.

The core idea: the list key

data-slater-list="X" gives the list a key - a label you pick. It is not the collection slug. Every control that belongs to that list references the same key:

data-slater-list="blog"          ← the list
data-slater-results="blog"       ← where cards render
data-slater-search="blog"        ← its search box
data-slater-load-more="blog"     ← its Load More button
data-slater-empty="blog"         ← its empty state
data-slater-count="blog"         ← its result count

Rule: data-slater-list and data-slater-results (and every control's key) must all be the same value.

The collection slug is separate. It comes from data-slater-collection (or collection in config), and falls back to the list key when you don't set it. So if you name the list blog-posts, the slug is inferred and you need nothing else. Use a distinct key only when a page has two lists:

<div data-slater-list="cars"   data-slater-collection="cars">…</div>
<div data-slater-list="brands" data-slater-collection="makes">…</div>

A control nested inside the wrapper belongs to it automatically; a control placed elsewhere on the page needs data-slater-for="X" (only matters when the page has more than one list/search block).

Finding your field slugs

Every binding and filter value refers to a field slug - the exact key in the API's item JSON. These are Webflow field slugs: lower-case, kebab-case (e.g. card-image, car-brand), and case-sensitive. Two ways to discover them:

  • Debug mode: add ?slater-debug=1 to the page URL. The console prints the detected list, the template node, the resolved endpoint, and the first item's field keys.
  • Directly: open https://api.slater.app/<siteId>/<collectionSlug>?limit=1 in a browser and read the keys.

Every item also carries these system fields you can bind or sort on: id, name, slug, createdOn, lastUpdated, lastPublished.

Fields from a linked collection (data-slater-expand)

A reference or multi-reference field arrives as {id, name, slug} - nothing else from the linked collection. That is a problem when name is an internal editorial identifier ([COM] - 10 étapes…) and the real display value lives in a custom field (titre), or when you want the linked item's image.

Add data-slater-expand to the wrapper to pull the linked item's full field data, then bind into it with a dot path:

<div data-slater-list="blog" data-slater-collection="blog" data-slater-expand>
  <div class="card" data-slater-template>
    <div data-slater-bind-text="auteur.titre"></div>
    <img data-slater-bind-src="auteur.photo" data-slater-bind-alt="auteur.titre">
  </div>
</div>
  • data-slater-expand with no value expands every reference field.
  • data-slater-expand="auteur,expertises" expands only those - smaller payloads.
  • name and slug stay exactly where they were, so existing bindings keep working.
  • On the raw API this is the expand query parameter: https://api.slater.app/<siteId>/<collectionSlug>?expand=*. It works on the collection list, the single-item read, /search and /ai-search.
Multi-reference fields still render names. Expansion puts the full data in the payload, but data-slater-bind-text="expertises" joins the name of each element. To show a custom field, bind a single element (expertises.0.titre) or reshape the value in a transformItems hook.
  • One level deep. A reference inside an expanded item stays {id, name, slug}.
  • Payloads grow. Expand only what the card actually renders, especially with a large data-slater-page-size.
  • A linked collection whose endpoint is disabled in the dashboard is not expanded; it keeps returning {id, name, slug}.
  • Naming a field that isn't a reference field logs a console warning and changes nothing.
  • Filtering and facets are unaffected - see Filtering limits. Expansion is display-only.

Binding data into the card

Put these on elements inside the data-slater-template:

AttributeOnEffect
data-slater-bind-text="heading"anySets text content. Never renders HTML.
data-slater-bind-html="post-body"anySets HTML, sanitized. Rich-text fields only.
data-slater-bind-src="card-image"<img>Sets src from an image field's URL.
data-slater-bind-alt="name"<img>Sets alt.
data-slater-bind-href="external-link"<a>Sets href from a field that already holds a full URL.
data-slater-href="/{locale}/blog/{slug}"<a>Builds href from a URL pattern (see below).
data-slater-item-href<a>Builds href from the list's detailPath config.
data-slater-bind-attr-aria-label="titre"anySets any attribute from a field (see below).
data-slater-hide-emptyanyHides this node when its bound field is empty.
data-slater-show-if="featured"anyHides this node unless the field is truthy/non-empty.

Values are coerced automatically per field type (see how each field type renders) - a reference renders as its name, an image as its URL, etc. You never get [object Object].

Any other attribute

data-slater-bind-attr-<name>="field" points any attribute at a CMS field - aria-label, title, data-*, poster, download. The part after data-slater-bind-attr- is the attribute to set, so several can sit on one node. This is the answer for an icon-only link, where the accessible name has to come from the item rather than a generic string repeated on every card:

<a data-slater-item-href
   class="card_read-more"
   data-slater-bind-attr-aria-label="titre"
   data-slater-bind-attr-title="chapo"
   data-slater-bind-attr-data-post-id="id">
  <svg><!-- icon-only link, no visible text --></svg>
</a>
  • Dot paths work: data-slater-bind-attr-aria-label="auteur.titre".
  • An empty field removes the attribute rather than writing "" - a blank aria-label is worse than none, and a leftover template value would belong to a different item.
  • URL-ish attributes (href, src, poster, action, …) take an image field's URL; every other attribute takes its display text.
Refused for safety: on* handlers and style are never written, and a URL attribute carrying a scheme other than http, https, mailto or tel is dropped. Each logs a console warning. CMS content is authored by anyone who can edit the collection, so it cannot be allowed to become an event handler or a javascript: URL.

Card links

CMS items have no URL field, only a slug. Build the link with a pattern instead of binding an href:

<a data-slater-href="/{locale}/blog/{slug}"></a>

{slug}, {name}, {id}, or any field name works inside {…}; {locale} is the page locale. A reference field resolves to its slug. Dot paths reach into nested objects: data-slater-bind-text="author.name", data-slater-bind-src="hero.url".

Always set data-slater-template. If you omit it the first .w-dyn-item is cloned, with a console warning. That is unsafe on any card using Webflow's conditional visibility: a node that only exists when a field is non-empty is missing from the template whenever the first item happens to lack that field - so no rendered card could ever show it. Author the template with every optional node present, and mark the optional ones data-slater-hide-empty:
<div class="card" data-slater-template>
  <div class="eyebrow" data-slater-hide-empty="expertise">
    <span data-slater-bind-text="expertise"></span>
  </div>
  <h2 data-slater-bind-text="name"></h2>
</div>

Feature guides

Each feature has its own focused guide, with the mandatory attributes first and the optional ones below:

Empty state, count, clear

Shared chrome that works with every feature, keyed like everything else:

<div data-slater-empty="blog">No results found.</div>   <!-- shown only when 0 match -->
<span data-slater-count="blog"></span>                   <!-- total matches; announced via aria-live -->
<button data-slater-clear="blog">Clear all</button>      <!-- resets search + every filter -->

The empty element never shows on a failed request - "no results" is only ever a true statement about the data. A broken request leaves the page as it was (see behaviour when things fail).

Author toggled elements visible. The embed shows and hides data-slater-empty, data-slater-load-more, and data-slater-initial via the inline style - "show" just clears style.display, so the element falls back to your stylesheet. Leave them visible in the Designer and let the embed hide them; if you hide one with a Webflow class (display: none), the embed can't override the class and the element will never appear.

Style hooks

The embed toggles no CSS classes - style against these attributes instead:

AttributeSet by the embedUse it for
data-slater-loadingon the wrapper during each fetcha loading state: [data-slater-loading] { opacity: .5 }
data-slater-renderedon every card the embed renderedstyling/animating injected cards

Attribute index

Setup - on the wrapper / container

AttributeWhat it doesDetails
data-slater-listOpts a CMS list in; the value is the keyList key
data-slater-collectionWebflow collection slug (default: the key)List key
data-slater-groupServe a dashboard-defined collection group instead of a single collection (data-slater-collection is ignored)Multi-collection lists
data-slater-resultsThe items container (default: .w-dyn-items inside the wrapper)Quick start
data-slater-templateThe card to clone per itemBinding
data-slater-siteWebflow site id (default: <html data-wf-site>)Quick start
data-slater-localeLocale tag (default: <html lang>)Config
data-slater-detail-pathURL pattern for data-slater-item-hrefBinding
data-slater-expandPull full field data from linked collectionsLinked fields
data-slater-search-fieldsRestrict the search box to certain fieldsKeyword search
data-slater-page-sizeItems on first load / per pageLoad more
data-slater-load-more-amountItems added per clickLoad more
data-slater-default-sortInitial sort ([-]field)Filtering
data-slater-facetsFields to facet / auto-fillFiltering
data-slater-search-listOpts a global search block inGlobal search
data-slater-min-lengthMin chars before a global search firesGlobal search

Binding - inside the template

data-slater-bind-text · data-slater-bind-html · data-slater-bind-src · data-slater-bind-alt · data-slater-bind-href · data-slater-bind-attr-* · data-slater-href · data-slater-item-href · data-slater-hide-empty · data-slater-show-if - all in Binding data into the card.

Controls - anywhere, keyed to a list / search block

AttributeWhat it doesDetails
data-slater-searchKeyword search inputKeyword search / Global search
data-slater-filterFilter control on a fieldFiltering
data-slater-valueThe value a checkbox/radio sendsFiltering
data-slater-filter-matchany (default) / all within one fieldFiltering
data-slater-sortSort dropdownFiltering
data-slater-load-moreAppends the next pageLoad more
data-slater-clearResets search + every filterChrome
data-slater-emptyShown only when nothing matchesChrome
data-slater-countTotal matches (aria-live)Chrome
data-slater-initialA search block's idle elementGlobal search
data-slater-forBinds a control to a key when not nested inside itList key
data-slater-typeaheadInline ghost-text word completion on an input (needs no list/block)Typeahead
data-slater-search-pageStandalone input: the search page Enter navigates toTypeahead

Script-level / hooks

AttributePurpose
data-slater-debug (on the <script>) or ?slater-debug=1verbose console logging (field slugs)
data-slater-loading / data-slater-renderedset by the embed - style hooks (chrome)
window.SlaterSearch / SlaterSearchConfig.hooksJS lifecycle hooks - per-project custom functions (JS hooks)

How each field type renders

Stored valuebind-textbind-srcfilter value
"News"News—news
42 / true42 / true—42 / true
{alt, url, fileId} (image)the altthe url—
{id, name, slug} (reference)the name—id, slug, or name
[{…}, {…}] (multi-reference)names joined ", "—any element
{id, name} (option)the name—id or name
rich text (HTML string)plain text——
null / missing"" (node stays)src removednever matches

With data-slater-expand: a reference also carries every field of the linked item, reachable by dot path (auteur.titre, auteur.photo). The bind-text column above is unchanged - binding the bare field still gives you the name.

Missing images: if an item has no image, the embed removes the src rather than leaving the template card's photo - otherwise every image-less item would show the same wrong picture. Add data-slater-hide-empty to the <img> if you'd rather the image node collapse entirely.

Worked example (car catalogue)

A vehicle list with search, a colour radio group, a make checkbox group, a year dropdown, a featured toggle, sorting, and load-more. Field slugs: name, car-brand (reference), color-theme (option), year, featured, image, price, location, state.

<script>
  window.SlaterSearchConfig = {
    lists: { cars: { collection: "cars", initialLimit: 12, loadMoreAmount: 12,
                     facets: ["car-brand", "year"] } }
  };
</script>
<script src="https://api.slater.app/embed/v1/slater-search.js" defer></script>

<!-- Controls -->
<input type="text" data-slater-search="cars" placeholder="Search…" />

<label><input type="radio" name="Color" data-slater-filter="color-theme" data-slater-value="black"/> Black</label>
<label><input type="radio" name="Color" data-slater-filter="color-theme" data-slater-value="white"/> White</label>

<label><input type="checkbox" data-slater-filter="car-brand" data-slater-value="fulgor"/> Fulgor</label>
<label><input type="checkbox" data-slater-filter="car-brand" data-slater-value="stream"/> Stream</label>

<select data-slater-filter="year">
  <option value="">Any year</option>
  <option value="2024">2024</option>
  <option value="2020">2020</option>
</select>

<label><input type="checkbox" data-slater-filter="featured" data-slater-value="true"/> Featured only</label>

<select data-slater-sort="cars">
  <option value="name">Name A–Z</option>
  <option value="-year">Newest year</option>
</select>

<button data-slater-clear="cars">Clear all</button>
<span data-slater-count="cars"></span> results

<!-- The list -->
<div class="w-dyn-list" data-slater-list="cars">
  <div class="w-dyn-items" data-slater-results="cars">
    <div class="w-dyn-item" data-slater-template>
      <img data-slater-bind-src="image" data-slater-bind-alt="name" />
      <h3 data-slater-bind-text="name"></h3>
      <span data-slater-bind-text="car-brand"></span>
      <span data-slater-bind-text="color-theme"></span>
      <span data-slater-bind-text="year"></span>
      <span data-slater-bind-text="location"></span>, <span data-slater-bind-text="state"></span>
    </div>
  </div>
</div>

<button data-slater-load-more="cars">Load more</button>
<div data-slater-empty="cars">No cars match your filters.</div>