Source Code

<div class="cmp-page">
  <div class="cmp-grid" id="cmpGrid">
    <div class="cmp-product" data-id="1" data-name="Studio Headphones" data-emoji="🎧" data-price="$199">
      <div class="cmp-img">🎧</div>
      <div class="cmp-pname">Studio Headphones</div>
      <div class="cmp-price">$199</div>
      <button class="cmp-toggle" onclick="toggleCompare(this)">Compare</button>
    </div>
    <div class="cmp-product" data-id="2" data-name="Field Watch" data-emoji="⌚" data-price="$249">
      <div class="cmp-img">⌚</div>
      <div class="cmp-pname">Field Watch</div>
      <div class="cmp-price">$249</div>
      <button class="cmp-toggle" onclick="toggleCompare(this)">Compare</button>
    </div>
    <div class="cmp-product" data-id="3" data-name="Mirrorless Camera" data-emoji="📷" data-price="$899">
      <div class="cmp-img">📷</div>
      <div class="cmp-pname">Mirrorless Camera</div>
      <div class="cmp-price">$899</div>
      <button class="cmp-toggle" onclick="toggleCompare(this)">Compare</button>
    </div>
    <div class="cmp-product" data-id="4" data-name="Smart Speaker" data-emoji="🔊" data-price="$129">
      <div class="cmp-img">🔊</div>
      <div class="cmp-pname">Smart Speaker</div>
      <div class="cmp-price">$129</div>
      <button class="cmp-toggle" onclick="toggleCompare(this)">Compare</button>
    </div>
    <div class="cmp-product" data-id="5" data-name="Mechanical Keyboard" data-emoji="⌨️" data-price="$159">
      <div class="cmp-img">⌨️</div>
      <div class="cmp-pname">Mechanical Keyboard</div>
      <div class="cmp-price">$159</div>
      <button class="cmp-toggle" onclick="toggleCompare(this)">Compare</button>
    </div>
    <div class="cmp-product" data-id="6" data-name="Wireless Mouse" data-emoji="🖱️" data-price="$79">
      <div class="cmp-img">🖱️</div>
      <div class="cmp-pname">Wireless Mouse</div>
      <div class="cmp-price">$79</div>
      <button class="cmp-toggle" onclick="toggleCompare(this)">Compare</button>
    </div>
  </div>

  <div class="cmp-bar" id="cmpBar">
    <div class="cmp-slots" id="cmpSlots"></div>
    <div class="cmp-actions">
      <span class="cmp-count" id="cmpCount">0 selected</span>
      <button class="cmp-clear" onclick="clearCompare()">Clear</button>
      <button class="cmp-go" id="cmpGo" onclick="doCompare()" disabled>Compare</button>
    </div>
  </div>
</div>

Product Compare Bar — HTML CSS JS Snippet

Product Compare Bar · Navigation · Plain HTML, CSS & JS · Live preview

What's included

Features

Single-array state
One selected id array drives the grid highlights, the tray, the count, the button, and bar visibility — nothing can fall out of sync.
Slot tray with placeholders
render always shows MAX slots, filling chosen products and rendering the rest as dashed empties so remaining capacity is visible.
Enforced max with feedback
Adding past the cap triggers flashLimit, briefly showing "Max 4 items" in red instead of an intrusive alert.
Two-way removal
Removing a tray slot un-highlights its grid card and vice-versa, because both reflect the same selected array.
Gated compare action
The compare button stays disabled until at least two products are selected, since comparing one is meaningless.
Slide-up only when needed
The bar springs up from below only when the selection is non-empty and slides away when cleared, never wasting space.
Pop-in slot animation
Newly filled slots animate with a scale pop keyframe, making each addition feel immediate and tactile.
Responsive layout
The grid drops to two columns and the count label hides on narrow screens so the bar stays usable on mobile.

About this UI Snippet

Product Compare Bar — Sticky Selection Tray, Max-Items Cap & Gated Compare Action

Screenshot of the Product Compare Bar snippet rendered live

"Compare" is a core shopping behaviour for considered purchases — electronics, appliances, plans, cars. Shoppers want to pick a few candidates from a grid and line them up side by side. The pattern that makes this smooth is a floating compare bar: a sticky tray that collects selected products as thumbnails, caps the selection at a sensible maximum, and offers a compare action once at least two are chosen. This snippet implements it in plain HTML, CSS, and vanilla JavaScript: a product grid with compare toggles, a slide-up tray with thumbnail slots, a max-items limit, per-item removal, clear-all, and a gated compare button.

A single selection array as the source of truth

All state lives in one selected array of product ids. toggleCompare adds or removes the card's id, toggles the card's .comparing highlight, and calls render. render rebuilds the tray, the count, the compare button's disabled state, and the bar's visibility entirely from that array — so the grid, the tray, and the button can never disagree about what is selected.

Thumbnail slots with a visible cap

The tray always shows MAX slots (four here). render fills the first slots with selected products' thumbnails and renders the remaining ones as dashed "empty" placeholders, so users can see both what they have chosen and how many more they may add. Each filled slot carries a small × button that calls removeFromBar(id) — removing an item from the tray also un-highlights its card in the grid, because both read from the same array.

Enforced maximum with feedback

Comparison tables become useless past a handful of columns, so the bar enforces MAX. When a user tries to add a fifth item, toggleCompare calls flashLimit, which briefly turns the count text red with "Max 4 items" and then restores the normal count — clear feedback without an intrusive alert. New slots animate in with a pop keyframe so additions feel responsive.

Gated compare and graceful empty

You cannot compare one product, so the compare button is disabled until at least two are selected. The whole bar slides up from below (transform: translate with a spring curve) only when the selection is non-empty, and slides away when cleared — it never occupies space when there is nothing to compare. doCompare is where you would open a comparison view; here it shows a brief "Comparing 3…" state.

Pair this compare bar with a product card grid, a comparison table as the destination view, or a faceted filter sidebar to narrow candidates first.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to reverse-engineer why the tray, grid, and button never disagree just by reading the render function once. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why a single selected array is treated as the only source of truth, and why render() rebuilds the slot HTML, the count, the disabled state, and the bar's visibility together rather than updating each independently. The same assistant can help optimize it — for instance asking whether rebuilding the entire slots innerHTML on every toggle is necessary versus patching just the changed slot. It is also a good way to extend the bar: ask it to persist the selection to sessionStorage so it survives navigation between category pages, add drag-to-reorder within the tray, or wire doCompare() to actually open a comparison table view instead of the placeholder timeout. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a floating "product compare" bar in plain HTML, CSS, and JavaScript — no state library, no frameworks.

Requirements:
- A grid of product cards, each with a "Compare" toggle button, where clicking it adds or removes that product's id from a single shared selection array (the only source of truth for the entire UI).
- A fixed-position tray that always renders exactly MAX slots (e.g. four), filling the first slots with the thumbnails of currently selected products in selection order and rendering any remaining slots as visually distinct empty/dashed placeholders, so users can see both what's selected and how much room is left.
- The tray must slide up into view from off-screen using a transform-based transition only when the selection is non-empty, and slide back out of view when the selection becomes empty — it must not occupy layout space when nothing is selected.
- Enforce the MAX cap: attempting to add a selection beyond the limit must not silently fail or add anyway — it must give brief, visible feedback (e.g. the count text flashing a warning color and message) and then return to normal after a short delay.
- Each filled tray slot needs its own small remove (×) control that deselects that specific product, updating both the tray and un-highlighting the corresponding product card in the grid simultaneously, since both read from the same array.
- A single render function must be the only place that writes the tray's HTML, the selected-count text, the compare button's disabled state, and the tray's visibility — driven entirely by reading the current selection array, so no other code path can leave any of those four things out of sync with each other.
- The compare action button must stay disabled until at least two products are selected, since comparing a single item is meaningless.
- A "Clear" action that empties the entire selection, un-highlights every card, and slides the tray away in one call.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA grid of six products appears, each with a "Compare" button; no bar is visible yet.
  2. 2
    Add a productClick "Compare" on a card — it highlights with a check, and a tray slides up from the bottom showing the product's thumbnail in the first slot.
  3. 3
    Add moreSelect another — its thumbnail fills the next slot, the count updates, and the "Compare" action unlocks once two are chosen.
  4. 4
    Hit the limitTry to add a fifth item — the count flashes "Max 4 items" in red and the selection is blocked.
  5. 5
    Remove from the trayClick the × on any slot — the item leaves the tray and its card un-highlights in the grid simultaneously.
  6. 6
    Compare or clearClick "Compare" to proceed (shows a brief comparing state), or "Clear" to empty the tray and slide the bar away.

Real-world uses

Common Use Cases

Electronics and appliance shops
Compare specs across a few candidates before buying. Feed the selection into a comparison table view.
Marketplace product grids
Let shoppers shortlist items from a large catalogue; pair with a faceted filter sidebar and product card grid.
SaaS plan comparison
Select plans or add-ons to compare features and pricing, leading into a pricing page or pricing table.
Real estate and auto listings
Shortlist properties or cars to compare side by side — the max-items cap keeps the comparison readable.
Travel and hotel booking
Compare a few hotels or flights on price and amenities before booking, building the shortlist from search results.
Course and content catalogues
Let learners compare courses by length, level, and price; the same tray pattern works for any selectable cards.
Related: Tab Switcher — CSS Only Radio Hack (No JavaScript)
See the Tab Switcher — CSS Only Radio Hack (No JavaScript) for a related navigation pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

In doCompare, navigate to a comparison page passing the selected ids (e.g. location.href = '/compare?ids=' + selected.join(',')) or open a modal that fetches those products and renders them as columns in a comparison table. Keep selected in sessionStorage so the shortlist survives navigation between the grid and the comparison view.

Edit the MAX constant. render draws exactly MAX slots and the cap logic uses it, so changing one number updates the tray, the placeholders, and the limit feedback together. Three to four is the sweet spot — comparison tables become hard to read with more columns, especially on mobile.

Write selected to localStorage or sessionStorage inside render, and read it back on load to restore the tray and re-highlight cards. This lets shoppers add items from different category pages into one comparison shortlist — a common requirement on large stores.

Make the toggles convey state with aria-pressed, announce tray changes via an aria-live="polite" region (e.g. "Studio Headphones added, 2 selected"), and ensure the × remove buttons have aria-labels (they do). The bar should be reachable in the tab order; keep the compare button's disabled state in sync so keyboard users are not sent to an empty comparison.

In React, hold selected in useState, derive the tray slots and button disabled state from it, and toggle ids on click — no manual DOM writes. In Vue, use a ref array with v-for over computed slots. In Angular, keep the array on the component and bind [class.comparing] and [disabled]. The slide-up bar and slot CSS port unchanged.