Skip to main content

Custom Features

Baits.com includes several custom-built features specifically designed for the fishing tackle retail market, enhancing product discovery and improving the user experience.

Slideout Variant Selector

The slideout variant selector is a custom React-based component that provides an enhanced interface for selecting product variants (primarily colors/patterns) on product detail pages.

Purpose

Fishing lures and baits often have dozens of color patterns. The standard BigCommerce swatch selector becomes unwieldy with many options. The slideout selector provides:

  • Grid view of all color options with images
  • Sorting capabilities (by name, length, etc.)
  • Sale price indicators per variant
  • Large, touch-friendly swatches
  • Modal interface to avoid page clutter

Architecture

Located in assets/slideout-variant-selector/ (React module) with initialization in assets/js/theme/baits/swatches.js.

Initialization

The Swatches class in swatches.js handles data preparation and event management:

export default class Swatches {
constructor() {
this.init_svs_data();

if (this.should_convert_swatch_button()) {
this.convert_swatch_button();
this.variant_change_listener();
}
}

init_svs_data() {
const svs_selector = jQuery('#slideout-variant-selector');

if (svs_selector.length && typeof window.productOptions === 'object') {
let svs_data = {
'orderByMethods': [
{
"label": "Name",
"value": "name",
"dir": "asc"
},
{
"label": "Length - Shortest",
"value": "length",
"dir": "asc"
},
{
"label": "Length - Longest",
"value": "length",
"dir": "desc"
}
],
'variants': []
};

// Process product options...
}
}
}

Data Structure

The selector receives variant data in a specific format:

{
orderByMethods: [
{
label: "Name",
value: "name",
dir: "asc"
},
{
label: "Length - Shortest",
value: "length",
dir: "asc"
},
{
label: "Length - Longest",
value: "length",
dir: "desc"
}
],
variants: [
{
id: 123, // Option value ID
name: "Black/Blue Flake", // Color name
image: {
src: "https://...", // Swatch image URL
alt: "Black/Blue Flake"
},
length: 4.5, // Parsed length (for sorting)
selected: false, // Currently selected
sale_variants: { // Sale price info per size
456: {
on_sale: true,
sale_price: 5.99,
attr: [
{
type: "Size",
label: "3.5 inch"
}
]
}
}
},
// ... more variants
]
}

Sale Price Mapping

The mapVariantSalePrices() method maps GraphQL variant data to color options:

mapVariantSalePrices(colorLabel) {
let sale_variants = {};

if (window.variantData && Array.isArray(window.variantData)) {
window.variantData.forEach(item => {
const variant = item.node;

// Find color option matching colorLabel
const colorOption = variant.options.edges.find(
optionEdge => optionEdge.node.displayName.toLowerCase() === 'color'
);

if (colorOption && colorOption.node.values.edges[0].node.label === colorLabel) {
// Collect non-color options (size, length, etc.)
const otherOptions = variant.options.edges.filter(
optionEdge => optionEdge.node.displayName.toLowerCase() !== 'color'
);

const saleInfo = {
on_sale: !!variant.prices?.salePrice,
sale_price: variant.prices?.salePrice?.value || null,
};

if (otherOptions.length > 0) {
const attrs = otherOptions.map(opt => ({
type: opt.node.displayName,
label: opt.node.values.edges[0].node.label
}));

sale_variants[variant.entityId] = {
...saleInfo,
attr: attrs,
};
}
}
});
}

return sale_variants;
}

Swatch Button Conversion

Replaces default BigCommerce swatches with a trigger button:

convert_swatch_button() {
const swatch_set = document.querySelectorAll('[data-product-attribute="swatch"]');
const button_event = new Event('product/variant-selector/open');

swatch_set.forEach(swatches => {
// Get currently selected or first option
const input_checked = swatches.querySelector('input.form-radio:checked');
const input_first = swatches.querySelector('input.form-radio:first-child');
const input = input_checked || input_first || null;

// Create trigger button
const button = document.createElement('button');
button.setAttribute('class', 'swatch-button');
button.setAttribute('type', 'button');
button.innerHTML = this.swatch_button_html(input);

// Hide original swatches
swatches.querySelectorAll('.form-option-wrapper').forEach(
wrapper => wrapper.style.display = 'none'
);

// Add button
swatches.insertAdjacentElement('beforeend', button);

// Attach click handler
button.addEventListener('click', () => {
document.dispatchEvent(button_event);
});
});
}

Event System

Custom events for communication:

// Initialization event
const init_event = new CustomEvent('product/variant-selector/init', {
detail: { variant_data }
});
document.dispatchEvent(init_event);

// Open modal event
const open_event = new Event('product/variant-selector/open');
document.dispatchEvent(open_event);

// Variant change event
const change_event = new CustomEvent('product/variant-selector/change', {
detail: { variantId }
});
document.dispatchEvent(change_event);

Variant Selection

When a variant is selected in the modal:

trigger_variant_click(variantId) {
const input = document.querySelector(`input[value="${variantId}"]`);
if (input) {
input.click(); // Trigger BigCommerce's native handler
}
}

This ensures BigCommerce's price updates, inventory checks, and add-to-cart validation work correctly.

Sorting Variants

The selector supports multiple sort methods:

  1. By Name (alphabetical): Default sort
  2. By Length - Shortest: For size-based products (e.g., "3 inch" < "4.5 inch")
  3. By Length - Longest: Reverse length sort

Length extraction:

const length = val.label.match(/(\d+(\.\d+)?)/)
? parseFloat(val.label.match(/(\d+(\.\d+)?)/)[0])
: 0;

Extracts numeric values from labels like "4.5 inch Black/Blue" → 4.5

Benefits

  • Improved UX: Easier to browse many color options
  • Visual Focus: Large, clear swatch images
  • Sale Visibility: Highlights discounted variants
  • Performance: Lazy-loaded modal (doesn't affect page load)
  • Mobile-Friendly: Touch-optimized interface

BazaarVoice Reviews Integration

Product reviews powered by BazaarVoice with custom styling via Shadow DOM manipulation.

Implementation

Located in assets/js/theme/baits/reviews.js:

const bv_reviews = () => {
const shadow_root_parent = document.querySelector('[data-bv-show="reviews"]');

if (shadow_root_parent) {
shadowRootListener(shadow_root_parent);
}
};

Shadow DOM Styling

BazaarVoice uses Shadow DOM to encapsulate styles. Custom styles are injected:

const reviews_styles = {
'': { // Base styles
'[class^=bv_rating_content] section': {
'margin': '0 0 30px',
'padding': '0'
},
'[class^=bv_rating_content] h3': {
'margin': '0',
'padding': '0'
},
'h2': {
'margin': '0 0 30px',
'text-align': 'center',
'width': '100%'
}
},
'1261': { // Desktop breakpoint
'h2:nth-child(n)': {
'font-size': '48px',
'margin': '0 0 45px',
}
}
};

Style Injection

MutationObserver watches for Shadow DOM creation:

const shadowRootListener = (shadow_root_parent) => {
const config = { attributes: true, childList: true, subtree: true };

const callback = (mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === 'attributes') {
observer.disconnect();
applyShadowRootStyles(shadow_root_parent.shadowRoot);
}
}
};

const observer = new MutationObserver(callback);
observer.observe(shadow_root_parent, config);
};

const applyShadowRootStyles = (shadowRoot) => {
let styles_html = '';

Object.keys(reviews_styles).forEach(breakpoint => {
if (breakpoint.length) {
styles_html += `@media all and (min-width: ${breakpoint}px) {
${style_selectors(reviews_styles[breakpoint])}
}`;
} else {
styles_html += style_selectors(reviews_styles[breakpoint]);
}
});

let stylesheet = document.createElement('style');
stylesheet.innerHTML = styles_html;

shadowRoot.append(stylesheet);
};

Template Integration

BazaarVoice container in product templates:

{{!-- templates/components/products/baits/product-reviews.html --}}
<div data-bv-show="reviews" data-bv-product-id="{{product.id}}"></div>

BazaarVoice script loads the widget based on data-bv-product-id.

Features

  • Star Ratings: Aggregate and individual review ratings
  • Review Sorting: Most helpful, newest, highest/lowest rated
  • Review Filtering: By star rating
  • Image Gallery: Customer-uploaded product photos
  • Q&A Section: Product questions and answers
  • Verified Purchaser Badges: For confirmed buyers
  • Helpful Voting: Upvote/downvote reviews

Product Carousels

Custom Swiper implementations for product showcases.

Homepage and landing page featured products:

const featuredProductsSwiper = () => {
productSwiper({
swiper_container: document.querySelector('.product-swiper-container--featured'),
swiper_class: '.product-swiper--featured',
});
};

Product detail page "You May Also Like" section:

const relatedSwiper = () => {
productSwiper({
swiper_container: document.querySelector('.product-swiper-container--related'),
swiper_class: '.product-swiper--related',
});
};

Shared Configuration

Both use the same responsive config:

const productSwiper = (args) => {
const product_swiper_container = args.swiper_container || null;
const product_swiper_class = args.swiper_class || null;
const product_swiper = product_swiper_class
? document.querySelector(product_swiper_class)
: null;

if (!product_swiper_container || !product_swiper) return;

const swiper = new Swiper(product_swiper, {
breakpoints: {
551: {
slidesPerView: 2.2,
},
801: {
spaceBetween: 10,
slidesPerView: 3.2,
},
1261: {
slidesOffsetAfter: 0,
slidesPerView: 4,
},
1441: {
slidesPerView: 5,
},
},
loop: false,
on: {
slideChange: () => {
// Update navigation state
if (swiper.isBeginning) {
product_swiper_container.classList.add('swiper-beginning');
} else {
product_swiper_container.classList.remove('swiper-beginning');
}

if (swiper.isEnd) {
product_swiper_container.classList.add('swiper-end');
} else {
product_swiper_container.classList.remove('swiper-end');
}
},
},
navigation: {
nextEl: product_swiper_container.querySelector('.product-swiper__button--next'),
prevEl: product_swiper_container.querySelector('.product-swiper__button--prev'),
},
pagination: {
clickable: true,
el: product_swiper_class + '__pagination',
enabled: true,
type: 'bullets',
},
slidesOffsetAfter: 20,
slidesPerView: 1.2,
spaceBetween: 25,
});
};

CSS classes added dynamically:

  • .swiper-beginning - At first slide (disable prev button)
  • .swiper-end - At last slide (disable next button)

Example CSS:

.product-swiper-container {
&.swiper-beginning .product-swiper__button--prev {
opacity: 0.3;
pointer-events: none;
}

&.swiper-end .product-swiper__button--next {
opacity: 0.3;
pointer-events: none;
}
}

Grid-based carousel for brand showcases:

const brandsSwiper = () => {
const swiper_container = document.querySelector('.promos-swiper-container');
const swiper_class = '.promos-swiper';
const swiper_instance = document.querySelector(swiper_class);

if (swiper_container && swiper_instance) {
const swiper = new Swiper(swiper_instance, {
breakpoints: {
551: {
grid: { fill: 'row', rows: 3 },
slidesPerView: 2.3,
},
801: {
grid: { fill: 'row', rows: 5 },
enabled: false, // Disable on desktop
slidesPerView: 3,
}
},
grid: {
fill: 'row',
rows: 5,
},
loop: false,
pagination: {
clickable: true,
el: swiper_class + '__pagination',
enabled: true,
type: 'bullets',
},
slidesPerView: 1.3,
});
}
};

Grid Mode: Shows multiple items per slide in a grid layout on smaller screens, then disables the carousel on desktop to show a static grid.

Enhanced image viewing with thumbnails and zoom.

Thumbnail Navigation

Slick Carousel for thumbnail strip (when > 7 images):

<ul class="productView-thumbnails"{{#gt product.images.length 7}} data-slick='{
"infinite": false,
"mobileFirst": true,
"dots": false,
"accessibility": false,
"responsive": [
{
"breakpoint": 550,
"settings": {
"slidesToShow": 6
}
},
{
"breakpoint": 1260,
"settings": {
"slidesToShow": 7
}
}
],
"slidesToShow": 4,
"slidesToScroll": 1
}'{{/gt}}>
{{#each product.images}}
<li class="productView-thumbnail">
<a href="..." data-image-gallery-item>
<img class="lazyload" ... />
</a>
</li>
{{/each}}
</ul>

Zoom Functionality

EasyZoom library for image zoom:

<figure 
data-image-gallery-main
data-zoom-image="{{getImageSrcset product.main_image 1x=theme_settings.zoom_size}}"
>
<a href="..." target="_blank">
<img src="..." data-main-image />
</a>
</figure>

Behavior:

  • Desktop: Hover to zoom
  • Mobile: Tap to view full-size in new tab
  • Gallery: Click thumbnail to update main image

Cross-Sell Features

Displayed below product details:

{{> components/products/baits/product-related}}

Uses the related products carousel with Swiper.

Cart Suggested Products

"You May Also Like" in cart:

{{> components/cart/suggested-products}}

Configured in theme settings:

  • Number of suggestions
  • Display position (sidebar or below cart)
  • Source (related, similar, best-sellers)

Quick View Modal

Enhanced quick view with key product info:

{{> components/products/quick-view}}

Features:

  • Product images (first 3)
  • Title and brand
  • Price (with sale display)
  • Star rating and review count
  • Key options (size, color)
  • Add to cart button
  • "View Full Details" link

Benefits:

  • Faster product evaluation
  • Reduced page loads
  • Improved mobile shopping experience

Wishlist Functionality

Save products for later:

$('[data-wishlist-add]').on('click', function(e) {
e.preventDefault();

const $this = $(this);
const productId = $this.data('product-id');

utils.api.wishlist.add({ productId }, (err, response) => {
if (err) {
// Show error
return;
}

// Update button state
$this.addClass('is-active');
$this.attr('aria-label', 'Remove from wishlist');

// Show success message
swal.fire({
text: 'Product added to wishlist',
icon: 'success'
});
});
});

Features:

  • Heart icon button on product cards
  • Add/remove from wishlist
  • Wishlist page with all saved items
  • Email wishlist to self or others
  • Share wishlist URL

Product Comparison

Compare up to 4 products side-by-side:

$('[data-compare-id]').on('change', function() {
const productId = $(this).val();
const isChecked = $(this).is(':checked');

if (isChecked) {
utils.api.productCompare.add(productId, callback);
} else {
utils.api.productCompare.remove(productId, callback);
}
});

Comparison Page:

  • Side-by-side product cards
  • Key attributes table:
    • Price
    • Brand
    • Rating
    • Specifications (weight, size, material, etc.)
  • Add to cart from comparison
  • Remove from comparison

Brand Pages

Custom brand showcase pages:

{{!-- templates/pages/brand.html --}}
<div class="brand-page">
<header class="brand-header">
<img src="{{brand.image}}" alt="{{brand.name}}" />
<h1>{{brand.name}}</h1>
</header>

{{#if brand.description}}
<div class="brand-description">
{{{brand.description}}}
</div>
{{/if}}

{{> components/faceted-search/faceted-search}}

<div class="product-grid">
{{> components/products/grid products=brand.products}}
</div>
</div>

Features:

  • Brand logo and description
  • Faceted search/filtering
  • Product grid
  • Pagination
  • Sort options

Search Functionality

Header search with autocomplete:

$('[data-search]').on('input', debounce(function() {
const query = $(this).val();

if (query.length < 3) return;

utils.api.search.search(query, { template: 'search/quick-results' }, (err, response) => {
if (err) return;

$('[data-search-results]').html(response);
});
}, 300));

Results Show:

  • Top 5 matching products with images
  • Product names and prices
  • "View all results" link

Full search page with filters:

{{!-- templates/pages/search.html --}}
<div class="search-page">
<form data-search-form>
<input type="search" name="search_query" />
<button type="submit">Search</button>
</form>

{{> components/faceted-search/faceted-search}}

<div class="product-grid">
{{> components/products/grid products=search.products}}
</div>
</div>

Advanced filtering on category and search pages:

Filter Types:

  • Price range slider
  • Brand checkboxes
  • Color swatches
  • Size/dimension selectors
  • Rating filter
  • Availability (in stock)
  • Custom fields (material, species, etc.)

Features:

  • AJAX updates (no page reload)
  • Active filter display with remove buttons
  • Filter counts (number of products per option)
  • Mobile: Collapsible filter panel
  • Desktop: Sidebar filters

Custom Product Flags

Visual badges on product cards:

{{#filter product.custom_fields 'flag_label' property='name'}}
<div class="flag-label {{toLowerCase this.value}}">
<span>{{value}}</span>
</div>
{{else}}
{{#or product.price.non_sale_price_with_tax product.price.non_sale_price_without_tax}}
<div class="sale-flag-side">
<span class="sale-text">Sale</span>
</div>
{{/or}}
{{/filter}}

Flag Types:

  • Sale
  • New
  • Best Seller
  • Limited Edition
  • Exclusive
  • Custom (via custom fields)

Styled per type with distinct colors and positions.

Performance Features

Lazy Loading

Lazysizes for images and iframes:

<img 
class="lazyload"
data-src="image.jpg"
data-srcset="image-500w.jpg 500w, image-1000w.jpg 1000w"
data-sizes="auto"
/>

Code Splitting

Dynamic imports per page type:

// Only loads product-specific JS on product pages
const product = () => import('./theme/product');

Resource Hints

Preconnect to CDN and external services:

<link rel="preconnect" href="https://cdn.bcapp.dev" />
<link rel="dns-prefetch" href="https://www.bazaarvoice.com" />

Maintenance & Updates

Adding New Custom Features

  1. Create module file: assets/js/theme/baits/new-feature.js
  2. Import in page class:
    import NewFeature from './baits/new-feature';
  3. Initialize:
    new NewFeature();
  4. Add styles: assets/scss/baits/_new-feature.scss
  5. Import in theme.scss:
    @import "baits/new-feature";

Testing Custom Features

  • Browser DevTools: Console for errors, Network for performance
  • Mobile Testing: Chrome DevTools device emulation, real devices
  • Cross-Browser: Chrome, Firefox, Safari, Edge
  • Accessibility: Lighthouse audit, screen reader testing

Documentation

Document custom features in:

  • Code comments
  • This documentation
  • README.md
  • CHANGELOG.md (when changes made)