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:
- By Name (alphabetical): Default sort
- By Length - Shortest: For size-based products (e.g., "3 inch" < "4.5 inch")
- 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:
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.
Featured Products Carousel
Homepage and landing page featured products:
const featuredProductsSwiper = () => {
productSwiper({
swiper_container: document.querySelector('.product-swiper-container--featured'),
swiper_class: '.product-swiper--featured',
});
};
Related Products Carousel
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,
});
};
Navigation State Classes
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;
}
}
Brands/Promotions Carousel
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.
Product Image Gallery
Enhanced image viewing with thumbnails and zoom.
Thumbnail Navigation
Slick Carousel for thumbnail strip (when > 7 images):
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
Related Products
Displayed below product details:
Uses the related products carousel with Swiper.
Cart Suggested Products
"You May Also Like" in cart:
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:
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:
Features:
- Brand logo and description
- Faceted search/filtering
- Product grid
- Pagination
- Sort options
Search Functionality
Quick Search
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
Advanced Search
Full search page with filters:
Faceted Search
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:
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
- Create module file:
assets/js/theme/baits/new-feature.js - Import in page class:
import NewFeature from './baits/new-feature'; - Initialize:
new NewFeature(); - Add styles:
assets/scss/baits/_new-feature.scss - 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)