section-042

Product Finder

Vault only: not sold separatelyGet the Vault

A product finder where every answer re-ranks the actual range: the best match rises into a hero tier on a tile of its own colour with the reason it fits, the rest follow in a ranked row and anything ruled out steps back.

Flip advanced
8 more details
flip-layoutfilterstate-transitionkeyboard-navigationprogressive-enhancementcontainer-queriesresponsive-sectionmatched-photo-set

This section reads better at your own screen size than in the frame below:

Released

About this section

Product Finder answers the visitor's question "which one of these is right for me?" without hiding the range behind a quiz. The questions are one sentence set large across the top of the section: I brew with a filter, espresso, a cafetière or a moka pot; I like a cup that is bright, balanced or chocolatey; I take it black or with milk; I drink caffeinated or decaf. Every answer is a word in that sentence.

Read the full section overview

Below it the range answers in two tiers. The best match rises into the top tier, its pack on a tile of its own colour beside its name, price, a sentence saying why it fits ("Because you brew with a filter and like a bright, fruity cup") and an Add to basket button. The rest follow in one ranked row, each saying what it is good for, and anything an answer rules out steps back to the right in grey, saying which answer set it aside.

The worked example is Halden Roasters, a fictional specialty roaster with seven coffees photographed as one matched set. Questions, answers, reasons and every product's scores are data attributes in ordinary HTML, so the same section re-points to skincare, bikes or mattresses by editing markup.

Product Finder - website section preview

What's included

12 items
  • Answers written as one editable sentence: every option is a native toggle button
  • Every answer re-ranks the real product photos with GSAP Flip; the new winner travels up into the hero tier and the old one drops into the row
  • Transparent cut-out packs with soft contact shadows, so they stand cleanly on the ground and on the winner's colour tile
  • Pouch labels are live, editable type that scales with each photo
  • Ruled-out products step back in grey and say which answer set them aside
  • The best match states its reasons; the rest say what they are good for, or best with
  • A running verdict with Undo and Start over beside the sentence
  • Answers change in any order, clear on a second press, undo step by step and start over
  • Rapid changes coalesce and settle on the final ranking; ties resolve by authored order
  • Scores, reasons and products live in data attributes, not a JavaScript array
  • Phones keep the sentence and the leader in one screen, so each answer's effect is visible
  • Independent instances, a polite live summary and exact teardown

Perfect for

4 use cases
  • Specialty coffee, tea or wine ranges with a find-your-match helper
  • Skincare or haircare product finders by skin type and concern
  • Bike, mattress or running-shoe selectors on a product category page
  • Plan or package pickers where a few answers narrow a small range

How it works

3 sections

Each question is a group of buttons inside a sentence. Each button carries an answer key and the phrase that explains it. Each product carries a data-pfs-fit list scoring those keys: 3 ideal, 2 good, 1 workable, nothing neutral, and no to set the product aside.

On every change the script totals each product's score for the chosen answers, sets aside any product an answer rules out, and orders the rest by score, breaking ties by the order the products are written in. It records where every product photo is with Flip.getState, moves the real list items into the new order (the first becomes the hero tier, the rest form the ranked row), then plays Flip.from with scaling so each photo travels from where it was: the new winner rises into the hero tile, the old one drops into the row. Captions fade in at their new places, the tile takes the winner's colour, set-aside photos shrink and turn grey, and the winner's reason, composed from the answers it scores 2 or more on, arrives once the move has landed. Set-aside products that share a reason are summarised in one line under the group.

Clicks are coalesced to one update per frame, and each update captures positions mid-flight before finishing the previous move, so fast changes never jump and always settle on the last answer. Reduced motion re-ranks instantly. With no JavaScript the full range is listed with prices and buy links.

Plugins Flip
Difficulty Advanced
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this website section's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Vault only

Get the Vault to use this section.

The Vault includes Product Finder, the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate. Sections are available only in the Vault for now while the collection is small.

Documentation

Quick Start

Copy these into your project:

  • the <section class="pfs"> element from index.html, between the EXTRACT START and EXTRACT END comments
  • assets/style.css
  • assets/script.js
  • assets/img/ (or your own photographs)

Load the font and styles, then GSAP, Flip and the section script after the markup:

Code snippet omitted: it ships with the download.

Everything outside the <section> in index.html (the page background and body margin) is demo furniture. No build step is needed.

Make it yours

Questions and answers

Each question is a <p class="pfs__line" role="group" data-pfs-question> with a lead-in (.pfs__lead, its id named by aria-labelledby) and one <button class="pfs__opt"> per answer. Words between buttons (or, commas, full stops) are ordinary text, so the line reads as a sentence. Each button needs two attributes:

  • data-pfs-answer="filter": a key, unique across all questions, that products score against.
  • data-pfs-reason="you brew with a filter": the phrase that completes "Because ..." when this answer helps a product win, and "Set aside because ..." when it rules one out.
  • data-pfs-label="a filter" (optional): a short noun for the ranked products' captions ("Good for a filter and milk", "Best with espresso and milk"). Defaults to the button text; set it empty to keep an answer out of captions.

Add or remove a question by adding or removing a .pfs__line. Add an answer by adding a button. One answer per question can be chosen; pressing the chosen answer again clears it.

A skincare version might read:

Code snippet omitted: it ships with the download.

Products and scores

Each product is an <li class="pfs-item"> in <ol data-pfs-range>: a photo, name link, price, origin line, notes and an Add to basket link. Its data-pfs-fit attribute scores answer keys:

Code snippet omitted: it ships with the download.

  • 3 ideal, 2 good (both count towards the reason sentence and the "Good for ..." captions), 1 workable. A ranked product that scores 2+ on none of the chosen answers says what it is best with instead, from its own 3s.
  • A key that is missing scores 0: neutral.
  • no sets the product aside whenever that answer is chosen.

The order you write the products in is the ranking before any answer, and it breaks ties, so put your house recommendation first. Add, remove or reorder products by moving <li> elements; counts, ranks and the row's column count are derived from the list. Five to eight products suit the single row on a wide layout (the row holds every product but the winner); beyond that the photos get small, so split a larger range into two finders. A single product works but leaves nothing to rank.

If every product is set aside, the closest one still leads and says which answer set it aside.

Each item also carries:

  • data-pfs-id="kiamabara": your product id, sent with the basket events below.
  • style="--pfs-tone: #4a2b3f": the product's own colour (its pack colour here). The winner's photo sits on a tile tinted with it, and its tag carries a swatch of it. No text is set on the tile.

Replace each href="#..." on names with your product URLs. The Add to basket links are the no-JavaScript fallback (point them at your add-to-cart URL); with the script running they become in-place toggles and fire events instead (see Basket events). Only the leader's button shows while the finder runs; all of them show if JavaScript is off. data-pfs-basket-url on the section sets the "View basket" link that appears once something is added.

When several set-aside products share a reason, the section says it once in a line under that group ("All 6 set aside because you drink decaf."); a product with a different reason keeps its own line. Every product keeps its full reason for screen readers.

Photos

Each product has a .pfs-item__photo holding one <img> with two local WebP sources (assets/img/coffee-*-400.webp and -800.webp, 2:3), width/height and alt text. To replace one, drop in your own photo, ideally 800x1200 or larger, update src, srcset, width, height and alt. No animation changes are needed.

Photos are shown in a 2 / 3 frame (--pfs-frame) with object-fit: cover around --pfs-focus (default 50% 50%). Set --pfs-focus on one <img> via style="--pfs-focus: 50% 70%" to move a single crop.

One shelf. The demo photos are cut-outs: transparent WebP with the pouch, beans and a soft contact shadow, so the packs stand on the row over any ground colour and the winner's tile shows behind its pack with no fringe. Use cut-outs of your own (any background-removal tool, saved as PNG or WebP with transparency) for the same effect. Ordinary photos with their own backdrop work too; they read as tiles and cover the winner's colour tile.

Printed label. .pfs-item__label is live type (brand, name, origin line) laid over the blank label in each demo photo, so names and rebrands are text edits, never image edits. It is decorative (aria-hidden); the real name is the h3 below. Its box is set in percent of the photo frame by --pfs-label-x, --pfs-label-y, --pfs-label-w and --pfs-label-h (on .pfs for the whole set, or on one .pfs-item__photo), and its type scales with the photo. If your photos already show printed packaging, or have no blank label, delete the <span class="pfs-item__label"> from each item.

Brand variables

All on .pfs:

Variable Default Controls
--pfs-ground #cfd1d2 Section background
--pfs-ink #161715 Headings, chosen copy, buttons
--pfs-muted #4b4d48 Unchosen answers, secondary copy
--pfs-accent #1f2fd0 Chosen answer, best-match tag, focus rings
--pfs-on-accent #ffffff Text on the accent
--pfs-line translucent ink The line under the ranked row, and tool borders
--pfs-font Bricolage Grotesque All type
--pfs-width 1440px Content width
--pfs-pad fluid Side padding
--pfs-gap fluid Space between products
--pfs-radius 999px Button and tag corners
--pfs-frame 2 / 3 Photo frame aspect ratio
--pfs-focus 50% 50% Photo focal point
--pfs-label-x/y/w/h label box Printed label position on the photo, in % of the frame
--pfs-label-ink #26221f Printed label type colour
--pfs-tone (per item) pack colour The winner's tile and tag swatch

Check contrast if you change --pfs-muted or --pfs-accent against --pfs-ground: both carry text.

Behaviour and options

  • data-duration on the section sets the re-rank move in seconds (default 0.75).
  • The script mounts every [data-product-finder] on load. For content added later, call ProductFinder.mount(el) or ProductFinder.mountAll(container).
  • mount() returns an instance: setAnswer(questionIndex, key) (pass null to clear), show(productId), reset() and destroy(). ProductFinder.destroyAll(container) tears down every instance inside a container.
  • destroy() kills its own tweens, removes its listeners and generated elements (rank numbers, tags, reasons, the live region), restores the authored product order and disables the answer buttons again. It never touches other animation on the page.

Looking at another product

Every product in the row is a button (over its photo): pressing it brings that product into the top tier with the same move, while the ranking of the rest stays as it was. The tag then reads "You're viewing", the reason line says how it fits the visitor's answers (its rank and what it is good for, or "Set aside because you drink decaf, but here it is if you are curious."), and a quiet "Back to best match" sits beside the tag. Set-aside products can be viewed too; they stay grey in the row but show in full colour on top. Add to basket works for whatever is shown. Changing any answer returns the top tier to the new best match; Undo restores the previous answers and the product that was being viewed. On a phone, if the top tier is off-screen after a pick, it scrolls into view.

Programmatically: instance.show('wahana') (a data-pfs-id) or instance.show(null) for the best match.

Basket events

The basket belongs to your site, so the section only tells it what happened. Clicking Add to basket on the product in the top tier settles the button into an Added state with a tick (its width animates; instant under reduced motion), shows a View basket link, announces "Kiamabara added to basket." politely, and fires a bubbling event on the section root. Clicking Added again undoes it and fires the matching remove event. The button returns to Add to basket when a different product takes the top tier (nothing is removed from your basket).

Code snippet omitted: it ships with the download.

Accessibility and integration

  • Answers are native buttons with aria-pressed, grouped by question with role="group" and labelled by the sentence's lead-in. Undo and Start over are native buttons, disabled when there is nothing to undo or clear.
  • The product list is reordered in the DOM, so reading and tab order always match the ranking. A polite live region announces the best match and how many products were set aside. The running verdict beside the sentence ([data-pfs-tally]) shows the same summary visually and is hidden from assistive technology to avoid a double announcement.
  • With the script running, Add to basket links take role="button" (Enter and Space both work), and the add/remove result is announced in the same polite live region.
  • Reduced motion re-ranks instantly, including when the preference changes while the page is open. Without GSAP the ranking still works, without motion.
  • With JavaScript off the question sentence reads as plain text (its buttons ship disabled and the script enables them), Undo and Start over stay hidden, and the full range is listed in authored order, every product with its price and buy link.
  • The heading is an h2 and product names are h3; change the tags to suit your page, the styles use classes.
  • The section sizes from its own width (container queries). From 900px of section width: the winner's tier, then one ranked row. Below that: the winner beside a compact answer, then the rest three to a row, so the sentence, verdict and winner fit one phone screen. Between tiers the photos travel (scaled, one aspect ratio) and captions fade in at their new places. It needs no full-bleed or sticky positioning and leaves page scrolling alone.
  • This is a front-end recommender. Basket links are placeholders for your shop.

Dependencies and credits

  • GSAP 3 core and the Flip plugin, loaded from jsDelivr.
  • Bricolage Grotesque, SIL Open Font License, via Google Fonts.
  • Product photographs are generated concept images made for this demo. Halden Roasters, its coffees, tasting notes and prices are fictional sample content.

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.