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>*.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 countRule: 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=1to 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=1in 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-expandwith no value expands every reference field.data-slater-expand="auteur,expertises"expands only those - smaller payloads.nameandslugstay exactly where they were, so existing bindings keep working.- On the raw API this is the
expandquery parameter:https://api.slater.app/<siteId>/<collectionSlug>?expand=*. It works on the collection list, the single-item read,/searchand/ai-search.
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:
| Attribute | On | Effect |
|---|---|---|
data-slater-bind-text="heading" | any | Sets text content. Never renders HTML. |
data-slater-bind-html="post-body" | any | Sets 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" | any | Sets any attribute from a field (see below). |
data-slater-hide-empty | any | Hides this node when its bound field is empty. |
data-slater-show-if="featured" | any | Hides 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 blankaria-labelis 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.
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".
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:
Load more
Page through a whole collection with a Load More button.
Keyword search
A search box over one CMS list, ranked across the whole collection.
Filtering, sorting & facets
Filters, sorting and facet counts driven by attributes.
Multi-collection lists
One list fed by several collections, with per-collection templates.
JS hooks
Reshape results and annotate cards with your own JavaScript.
Global site search
One search box across every collection and static page.
Typeahead
Ghost-text word completion, with no request per keystroke.
JS hooks
Reshape search results with your own JavaScript.
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:
| Attribute | Set by the embed | Use it for |
|---|---|---|
data-slater-loading | on the wrapper during each fetch | a loading state: [data-slater-loading] { opacity: .5 } |
data-slater-rendered | on every card the embed rendered | styling/animating injected cards |
Attribute index
Setup - on the wrapper / container
| Attribute | What it does | Details |
|---|---|---|
data-slater-list | Opts a CMS list in; the value is the key | List key |
data-slater-collection | Webflow collection slug (default: the key) | List key |
data-slater-group | Serve a dashboard-defined collection group instead of a single collection (data-slater-collection is ignored) | Multi-collection lists |
data-slater-results | The items container (default: .w-dyn-items inside the wrapper) | Quick start |
data-slater-template | The card to clone per item | Binding |
data-slater-site | Webflow site id (default: <html data-wf-site>) | Quick start |
data-slater-locale | Locale tag (default: <html lang>) | Config |
data-slater-detail-path | URL pattern for data-slater-item-href | Binding |
data-slater-expand | Pull full field data from linked collections | Linked fields |
data-slater-search-fields | Restrict the search box to certain fields | Keyword search |
data-slater-page-size | Items on first load / per page | Load more |
data-slater-load-more-amount | Items added per click | Load more |
data-slater-default-sort | Initial sort ([-]field) | Filtering |
data-slater-facets | Fields to facet / auto-fill | Filtering |
data-slater-search-list | Opts a global search block in | Global search |
data-slater-min-length | Min chars before a global search fires | Global 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
| Attribute | What it does | Details |
|---|---|---|
data-slater-search | Keyword search input | Keyword search / Global search |
data-slater-filter | Filter control on a field | Filtering |
data-slater-value | The value a checkbox/radio sends | Filtering |
data-slater-filter-match | any (default) / all within one field | Filtering |
data-slater-sort | Sort dropdown | Filtering |
data-slater-load-more | Appends the next page | Load more |
data-slater-clear | Resets search + every filter | Chrome |
data-slater-empty | Shown only when nothing matches | Chrome |
data-slater-count | Total matches (aria-live) | Chrome |
data-slater-initial | A search block's idle element | Global search |
data-slater-for | Binds a control to a key when not nested inside it | List key |
data-slater-typeahead | Inline ghost-text word completion on an input (needs no list/block) | Typeahead |
data-slater-search-page | Standalone input: the search page Enter navigates to | Typeahead |
Script-level / hooks
| Attribute | Purpose |
|---|---|
data-slater-debug (on the <script>) or ?slater-debug=1 | verbose console logging (field slugs) |
data-slater-loading / data-slater-rendered | set by the embed - style hooks (chrome) |
window.SlaterSearch / SlaterSearchConfig.hooks | JS lifecycle hooks - per-project custom functions (JS hooks) |
How each field type renders
| Stored value | bind-text | bind-src | filter value |
|---|---|---|---|
"News" | News | — | news |
42 / true | 42 / true | — | 42 / true |
{alt, url, fileId} (image) | the alt | the 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 removed | never 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>