Velo: Custom Auto-Scroll Repeater w/ Pause on Hover - API Issues (.scrollTo, .style, .addClass)

Question:

How can I implement a custom horizontal auto-scrolling repeater gallery with specific dynamic styling and interactions in the Wix Editor (Classic), overcoming apparent Velo API limitations with .style, .addClass, .scrollTo, .getBoundingClientRect, .setAttribute, and .onError on relevant elements (Boxes, Containers, Images within repeaters)?

Product:

Wix Editor (Classic)

What are you trying to achieve:

I’m trying to create a highly customized portfolio gallery on a dynamic list page (“Services (Title)”). The gallery should display items from a Portfolio collection, filtered based on the current service shown on the page (from the Services1 collection, using a matching tag in the typeOfService field).

The core requirements are:

  1. Data Source: Portfolio collection with fields: thumbnail (Image), title (Text), shortDescription (Text), typeOfService (Tags). Filtered by Services1.title matching Portfolio.typeOfService.
  2. Layout (Desktop/Tablet - formFactor === "Desktop"):
    • Items arranged horizontally within a container.
    • Container should allow horizontal scrolling (content wider than container).
    • Repeater items (#portfolioRepeaterContainer) should have a fixed height of 800px.
    • Repeater items should have a max-width of 600px.
    • Repeater items should have a right margin of 50px (except the last item).
  3. Layout (Mobile - formFactor === "Mobile"):
    • Items stacked vertically (standard repeater behavior).
    • Repeater items (#portfolioRepeaterContainer) should have auto height.
    • Repeater items should have a bottom margin of 50px (except the last item).
  4. Styling (Repeater Item - #portfolioRepeaterContainer):
    • Background: rgba(81, 63, 48, 0.8)
    • Corners: 15px
    • Padding: 20px
    • Image (#portfolioImage): object-fit: contain, border-radius: 15px.
    • Title (#portfolioTitle): #FFFAF2, EB Garamond Bold (50px Desktop, 30px Mobile).
    • Description (#portfolioDescription): #CCC8C2, EB Garamond Normal (20px Desktop, 18px Mobile).
    • Button (#portfolioButton): Specific background/text colors, corners, shadow. Mobile: height: 26px, width: auto.
  5. Functionality (Desktop):
    • Critical: Auto-Scroll: The horizontal gallery container should automatically scroll horizontally using setInterval (e.g., every 5s). The scroll distance should be the width of one item + its right margin.
    • Critical: Pause on Hover: The auto-scroll must pause when the user hovers their mouse over the scrolling container. It should resume when the mouse leaves.
    • Button Wiggle: A subtle, continuous wiggle animation on the #portfolioButton.
  6. Functionality (Mobile):
    • No Auto-Scroll.
    • Scroll-Triggered Animation: Each repeater item (#portfolioRepeaterContainer) should slide/fade in (e.g., translateY from 50px to 0, opacity from 0 to 1) as it enters the viewport (onViewportEnter). Should only animate once per item.
    • No Button Wiggle.
  7. Functionality (Common):
    • Button Hover/Focus: Standard CSS hover/focus effect (color inversion, slight scale). (This part works fine with CSS).
    • Dynamic Link: The #portfolioButton link should be dynamically generated based on the item’s title using a custom generateSlug function and base URL.
    • Image Error Handling: If an item’s thumbnail image fails to load, the image element (#portfolioImage) should be hidden/collapsed, allowing the container’s background to show as a placeholder.
    • No Items Found: If the data query returns no items for the current service, the gallery container should be hidden, and a “No items found” text element (#noPortfolioItemFoundText) should be shown.
  8. Accessibility: Attempting to set role="region" and dynamic aria-label on the main container, and role="listitem" on repeater items.

Why Not Use a Standard Wix Pro Gallery?

While Pro Galleries offer some horizontal layouts and auto-scroll, they do not provide the level of customization required by this specification, specifically:

  • The exact combination of fixed height (800px) with max-width (600px) for gallery items.
  • Fine-grained control over the internal layout and styling of elements within each gallery item (specific fonts, sizes, padding, button styles).
  • The specific auto-scroll behavior with a reliable pause-on-hover mechanism tied to the scroll container.
  • Rounded corner settings with the scrolling layout.

I already tried setting this up using only a Wix Pro Gallery, but the following example was the closest I could get. (see link below) Additional customization is needed which is why I am trying to use code for this. :backhand_index_pointing_down:t3::backhand_index_pointing_down:t3:

See Example

What have you already tried:

  1. Structure: Implemented using a Strip (#portfolioGalleryStrip) containing a Box (#portfolioScrollBox), which in turn contains the Repeater (#portfolioRepeater). This was done because initial attempts using just a Strip or Box as the main container failed due to API limitations.
  2. Velo Code: Used $w.onReady to fetch data via wix-data (filtering using .hasSome), populate the repeater (.data), and set up form-factor specific logic. Used $w('#portfolioRepeater').onItemReady to bind data, generate links, and apply item-specific logic/styles.
  3. Custom CSS: Used Custom CSS (added via Dashboard > Custom Code > Head) for static styling, responsive layout (Flexbox for desktop horizontal layout on the repeater, overflow on the scroll box), and button hover effects.
  4. Specific API Attempts & Failures:
    • Auto-Scroll: Attempted setInterval calling $w('#portfolioScrollBox').scrollTo(x, y). This fails with Error: Expected 0 arguments, but got 2.
    • Measuring Items: Attempted await $item('#portfolioRepeaterContainer').getBoundingClientRect() inside onItemReady or on the template item to calculate scroll distance. This fails with Property 'getBoundingClientRect' does not exist on type 'Container'. Also tried on the Box (#portfolioScrollBox) with similar results/errors.
    • Dynamic Styling: Attempted $item('#portfolioRepeaterContainer').style.marginRight = '50px' and similar for fontSize, height, width on text/button elements inside onItemReady. This fails with Property 'style' does not exist on type 'Container' or errors about specific properties like height not existing on type Style.
    • Mobile Animation (Class Trigger): Attempted $item('#portfolioRepeaterContainer').addClass('in-view') inside onViewportEnter. This fails with Property 'addClass' does not exist on type 'Container'.
    • Image Error: Attempted $item('#portfolioImage').onError(...). This fails with Property 'onError' does not exist on type 'Image'.
    • Accessibility Attributes: Attempted $w('#portfolioScrollBox').setAttribute('role', 'region') and $item('#portfolioRepeaterContainer').setAttribute('role', 'listitem'). This fails with Property 'setAttribute' does not exist on type 'Box' or 'Container'.

Code:

Velo Page Code (Services Title Page)
import wixData from 'wix-data';
import wixWindow from 'wix-window';
import { timeline } from 'wix-animations';

// --- Constants ---
const DESKTOP_CARD_MARGIN_RIGHT = 50;
const MOBILE_CARD_MARGIN_BOTTOM = 50;
const BASE_PORTFOLIO_URL = "https://www.fortheking.design/portfolio/";

// --- Global Variables ---
let desktopScrollIntervalId = null;
let currentServiceTitle = '';
let portfolioItems = [];
let isHovering = false;

// --- Helper Functions ---
function generateSlug(title) {
    if (!title) return '';
    const slug = title
        .toLowerCase()
        .replace(/\s+/g, '-')
        .replace(/[^a-z0-9-#-]/g, '-')
        .replace(/-+/g, '-')
        .replace(/^-+|-+$/g, '');
    return slug;
}

function setupButtonWiggleAnimation($item) {
    const button = $item('#portfolioButton');
    if (!button || !button.rendered) {
         return;
    }
    const wiggleTimeline = timeline({ repeat: -1, yoyo: true, repeatDelay: 1500 })
        .add(button, { rotate: -3, duration: 100, easing: 'easeInOutSine' })
        .add(button, { rotate: 3, duration: 100, easing: 'easeInOutSine' })
        .add(button, { rotate: 0, duration: 100, easing: 'easeInOutSine' });
    wiggleTimeline.play();
}

function setupMobileSlideInTrigger($item) {
    const container = $item('#portfolioRepeaterContainer');
    let hasAnimated = false;
     if (!container || !container.rendered) {
         return;
     }
    container.onViewportEnter(() => {
        if (!hasAnimated) {
            // Error: Property 'addClass' does not exist on type 'Container'.
            // container.addClass('in-view');
            hasAnimated = true;
        }
    });
}

async function fetchPortfolioItems(serviceTitle) {
    const scrollBox = $w('#portfolioScrollBox');
    const notFoundText = $w('#noPortfolioItemFoundText');
    if (!scrollBox || !notFoundText) {
        console.error("fetchPortfolioItems: Critical elements #portfolioScrollBox or #noPortfolioItemFoundText not found.");
        return;
    }

    try {
        const results = await wixData.query('Portfolio')
            .hasSome('typeOfService', [serviceTitle])
            .fields('thumbnail', 'title', 'shortDescription', 'typeOfService', 'link-portfolio-title')
            .limit(1000)
            .find();

        portfolioItems = results.items;

        if (portfolioItems.length > 0) {
            $w('#portfolioRepeater').data = portfolioItems;
            scrollBox.expand();
            $w('#portfolioGalleryStrip').expand();
            notFoundText.collapse();

            const formFactor = wixWindow.formFactor;
            // Error: Argument of type 'string' is not assignable to parameter of type '"Desktop" | "Mobile"'. (Handled with check inside function)
            setupLayoutAndAnimations(formFactor, scrollBox);

        } else {
            scrollBox.collapse();
            $w('#portfolioGalleryStrip').expand();
            notFoundText.expand();
        }

    } catch (error) {
        console.error("fetchPortfolioItems: Error during query:", error);
        scrollBox.collapse();
        notFoundText.expand();
        notFoundText.text = "Could not load portfolio items.";
    }
}

function setupDesktopAutoScroll(scrollContainer) {
    if (desktopScrollIntervalId) {
        clearInterval(desktopScrollIntervalId);
        desktopScrollIntervalId = null;
    }

    setTimeout(async () => {
        try {
            if (portfolioItems.length <= 1) return;

            const firstItemContainer = $w('#portfolioRepeaterContainer');
            if (!firstItemContainer || !firstItemContainer.rendered) return;

            let itemWidth = 600; // Default fallback width
            try {
                 // Error: Property 'getBoundingClientRect' does not exist on type 'Container'.
                 // const itemRect = await firstItemContainer.getBoundingClientRect();
                 // if (itemRect && itemRect.width > 0) itemWidth = itemRect.width;
            } catch(rectError) { /* Use fallback */ }

            const scrollDistance = itemWidth + DESKTOP_CARD_MARGIN_RIGHT;

            const repeaterElement = $w('#portfolioRepeater');
            let totalRepeaterWidth = portfolioItems.length * scrollDistance - DESKTOP_CARD_MARGIN_RIGHT;
             try {
                 // const repeaterRect = await repeaterElement.getBoundingClientRect(); // Might also fail
                 // if (repeaterRect && repeaterRect.width > 0) totalRepeaterWidth = repeaterRect.width;
             } catch (repeaterRectErr) { /* Use estimate */ }

            // Error: Property 'getBoundingClientRect' does not exist on type 'Box'. (May depend on exact Box type/context)
            // const containerRect = await scrollContainer.getBoundingClientRect();
            const containerRect = { width: 1000 }; // Placeholder if getBoundingClientRect fails

            if (!containerRect || containerRect.width <= 0) return;
            if (totalRepeaterWidth <= containerRect.width) return;

            let currentScrollX = 0;
            const maxScrollX = totalRepeaterWidth - containerRect.width;

            desktopScrollIntervalId = setInterval(() => {
                if (!isHovering) {
                    currentScrollX += scrollDistance;
                    if (currentScrollX > maxScrollX + scrollDistance / 2) {
                        currentScrollX = 0;
                    }
                    // Error: Expected 0 arguments, but got 2.
                    // scrollContainer.scrollTo(currentScrollX, 0)
                    //    .catch(e => console.error("Error during scrollTo:", e));
                }
            }, 5000);

            scrollContainer.onMouseIn(() => { isHovering = true; });
            scrollContainer.onMouseOut(() => { isHovering = false; });

        } catch (err) {
            console.error("Error setting up desktop auto-scroll:", err);
            if (desktopScrollIntervalId) clearInterval(desktopScrollIntervalId);
        }
    }, 500);
}

function setupLayoutAndAnimations(formFactor, scrollContainer) {
    if (formFactor !== 'Desktop' && formFactor !== 'Mobile') {
        formFactor = 'Mobile'; // Fallback
    }

    if (formFactor === 'Desktop') {
        setupDesktopAutoScroll(scrollContainer);
    } else {
        if (desktopScrollIntervalId) {
            clearInterval(desktopScrollIntervalId);
            desktopScrollIntervalId = null;
        }
    }
}

$w.onReady(function () {
    $w('#portfolioScrollBox').collapse();
    $w('#noPortfolioItemFoundText').collapse();
    $w('#portfolioGalleryStrip').expand();

    const scrollBox = $w('#portfolioScrollBox');
    if (scrollBox) {
        try {
             // Error: Property 'setAttribute' does not exist on type 'Box'.
             // scrollBox.setAttribute('role', 'region');
        } catch (e) { /* Set in Editor */ }
    }

    const dynamicDataset = $w('#dynamicDataset');
    if (!dynamicDataset) {
        $w('#noPortfolioItemFoundText').text = "Page setup error: Dataset not found.";
        $w('#noPortfolioItemFoundText').expand();
        return;
    }

    dynamicDataset.onReady(async () => {
        try {
            const currentItem = dynamicDataset.getCurrentItem();
            if (currentItem && currentItem.title) {
                currentServiceTitle = currentItem.title;
                 if (scrollBox) {
                     try {
                         // Error: Property 'setAttribute' does not exist on type 'Box'.
                         // scrollBox.setAttribute('aria-label', `Portfolio Gallery for ${currentServiceTitle}`);
                     } catch (e) { /* Set in Editor */ }
                 }
                await fetchPortfolioItems(currentServiceTitle);
            } else {
                $w('#noPortfolioItemFoundText').text = "Could not determine the current service.";
                $w('#noPortfolioItemFoundText').expand();
                $w('#portfolioScrollBox').collapse();
            }
        } catch (error) {
            $w('#noPortfolioItemFoundText').text = "Error loading page data.";
            $w('#noPortfolioItemFoundText').expand();
            $w('#portfolioScrollBox').collapse();
        }
    });
});

$w('#portfolioRepeater').onItemReady(($item, itemData, index) => {
    const formFactor = wixWindow.formFactor;
    const imageElement = $item('#portfolioImage');
    const titleElement = $item('#portfolioTitle');
    const descriptionElement = $item('#portfolioDescription');
    const buttonElement = $item('#portfolioButton');
    const itemContainer = $item('#portfolioRepeaterContainer');

    try {
        // Error: Property 'setAttribute' does not exist on type 'Container'.
        // itemContainer.setAttribute('role', 'listitem');
    } catch(e) { /* Set in Editor */ }

    if (itemData.thumbnail && imageElement) {
        imageElement.src = itemData.thumbnail;
        imageElement.alt = itemData.thumbnail.alt || itemData.title || "Portfolio image";
        imageElement.expand();
    } else if (imageElement) {
        imageElement.collapse();
    }

    if (titleElement) titleElement.text = itemData.title || '';
    if (descriptionElement) descriptionElement.text = itemData.shortDescription || '';

    if (imageElement) {
        // Error: Property 'onError' does not exist on type 'Image'.
        // imageElement.onError(() => {
        //     imageElement.collapse();
        // });
    }

    let fullUrl = '';
    const slug = generateSlug(itemData.title);
    if (slug) {
        fullUrl = BASE_PORTFOLIO_URL + slug;
    } else if (buttonElement) {
        buttonElement.disable();
    }

    if (fullUrl && buttonElement) {
        buttonElement.link = fullUrl;
        buttonElement.target = "_blank";
        buttonElement.enable();
    } else if (buttonElement) {
         buttonElement.disable();
    }

    if (!itemContainer) return;

    if (formFactor === 'Desktop') {
        try {
            // Error: Property 'style' does not exist on type 'Container'.
            // if (index < portfolioItems.length - 1) {
            //     itemContainer.style.marginRight = `${DESKTOP_CARD_MARGIN_RIGHT}px`;
            // } else {
            //     itemContainer.style.marginRight = '0px';
            // }
            // itemContainer.style.marginBottom = '0px';
        } catch (styleError) { /* Rely on CSS */ }

        setupButtonWiggleAnimation($item);

    } else { // Mobile
         try {
            // Error: Property 'style' does not exist on type 'Container'.
            // if (index < portfolioItems.length - 1) {
            //     itemContainer.style.marginBottom = `${MOBILE_CARD_MARGIN_BOTTOM}px`;
            // } else {
            //     itemContainer.style.marginBottom = '0px';
            // }
            // itemContainer.style.marginRight = '0px';
        } catch (styleError) { /* Rely on CSS */ }

        try {
            // Error: Property 'style' does not exist or properties like 'fontSize'/'height' don't exist on type 'Style'.
            // if (titleElement) titleElement.style.fontSize = '30px';
            // if (descriptionElement) descriptionElement.style.fontSize = '18px';
            // if (buttonElement) {
            //      buttonElement.style.height = '26px';
            //      buttonElement.style.width = 'auto';
            // }
        } catch (styleError) { /* Rely on CSS/Editor */ }

        setupMobileSlideInTrigger($item);
    }
});
Custom CSS
<style>
/* --- Portfolio Gallery Custom CSS --- */
#portfolioGalleryStrip { }

#portfolioScrollBox {
  width: 100% !important;
  box-sizing: border-box;
  position: relative;
}

#portfolioRepeater {
   display: flex !important;
   width: max-content !important;
   box-sizing: border-box;
   padding: 0;
   margin: 0;
   visibility: visible !important;
   opacity: 1 !important;
}

@media (min-width: 751px) {
  #portfolioScrollBox {
    overflow-x: hidden !important;
  }
  #portfolioRepeaterContainer {
    height: 800px !important;
    max-width: 600px;
    width: 100%;
    flex-shrink: 0;
    margin-bottom: 0 !important;
    /* Velo failed to set marginRight, apply via CSS */
    margin-right: 50px;
  }
  /* Remove margin from last item */
   #portfolioRepeater > div:last-child #portfolioRepeaterContainer {
       margin-right: 0px !important;
   }

   #portfolioTitle { font-size: 50px; font-weight: bold; }
   #portfolioDescription { font-size: 20px; font-weight: normal; }
}

@media (max-width: 750px) {
  #portfolioScrollBox { overflow-x: hidden !important; }
  #portfolioRepeater { display: block !important; width: 100% !important; }
  #portfolioRepeaterContainer {
    width: 100% !important;
    height: auto !important;
    max-width: none;
    flex-shrink: 1;
    margin-right: 0 !important;
    /* Velo failed to set marginBottom, apply via CSS */
    margin-bottom: 50px;
    /* Mobile Animation Base State */
    opacity: 0;
    transform: translateY(50px);
    transition: opacity 600ms ease-out, transform 600ms ease-out;
  }
   /* Remove margin from last item */
   #portfolioRepeater > div:last-child #portfolioRepeaterContainer {
       margin-bottom: 0px !important;
   }
  /* Mobile Animation Active State (if .addClass worked) */
  #portfolioRepeaterContainer.in-view { opacity: 1; transform: translateY(0); }

   /* Mobile styles (using !important as Velo .style failed) */
   #portfolioTitle { font-size: 30px !important; font-weight: bold !important; }
   #portfolioDescription { font-size: 18px !important; }
   #portfolioButton { height: 26px !important; width: auto !important; padding: 5px 15px !important; }
}

#portfolioRepeaterContainer {
  background-color: rgba(81, 63, 48, 0.8);
  border-radius: 15px;
  padding: 20px;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  overflow: hidden;
}

#portfolioImage { width: 100%; object-fit: contain; border-radius: 15px; margin-bottom: 15px; }
#portfolioTitle { color: #FFFAF2; margin-bottom: 10px; word-wrap: break-word; }
#portfolioDescription { color: #CCC8C2; flex-grow: 1; margin-bottom: 20px; word-wrap: break-word; }

#portfolioButton {
  background-color: #FFFAF2; color: #513F30; border-radius: 15px; cursor: pointer;
  text-align: center; padding: 8px 20px; margin-top: auto; align-self: center;
  box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
  transition: background-color 0.2s ease-out, color 0.2s ease-out, transform 0.2s ease-out, box-shadow 0.2s ease-out;
  border: none; font-weight: bold; transform: scale(1.0);
}
#portfolioButton:hover,
#portfolioButton:focus {
   background-color: #513F30; color: #FFFAF2; transform: scale(1.05);
   box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.3); outline: none;
}

#noPortfolioItemFoundText { text-align: center; padding: 40px 20px; color: #CCC8C2; width: 100%; }
*:focus-visible { outline: 2px solid #FFFAF2 !important; outline-offset: 2px; }
</style>

Additional information:

  • Are these documented limitations of the Velo API for these specific element types in the Classic Editor?
  • Are there alternative Velo APIs or techniques to achieve:
    • Programmatic scrolling of a container (like a Box) with specific coordinates?
    • Reliable measurement of element dimensions within repeaters?
    • Adding/removing CSS classes to repeater items?
    • Applying specific CSS styles (like margins) dynamically to repeater items?
    • Handling image load errors via Velo?
    • Setting ARIA attributes via Velo?
  • Is there a different structural approach (e.g., using Custom Elements, different container types) within the Classic Editor that would make these APIs available or provide alternative solutions for the required functionality (especially the auto-scroll with hover pause)?

Any insights or alternative strategies would be greatly appreciated. Thank you!

Hello,

You’re working with the Wix Classic Editor, where you are trying to achieve quite a complex and interactive custom portfolio gallery with dynamic styling, interactivity, and responsive behaviors. As you’ve pointed out, the limitations of the Velo API for certain element types (like Box, Container, Image) are causing roadblocks. Let me break down the various challenges you’re facing and propose alternative solutions for them.

  1. Auto-Scrolling the Gallery
    Issue: You’re trying to use setInterval() with scrollTo() on #portfolioScrollBox, but it’s throwing errors because scrollTo() expects no arguments.

Solution: The Velo API does not support direct programmatic scrolling of elements with scrollTo or getBoundingClientRect for elements like Box or Container. However, you can implement custom scrolling by using scrollLeft in combination with setInterval(). For instance, you can incrementally adjust the scrollLeft property of the scrolling container:

let scrollInterval;

function startAutoScroll() {
scrollInterval = setInterval(() => {
const scrollBox = $w(‘#portfolioScrollBox’);
const containerWidth = scrollBox.getBoundingClientRect().width;
const itemWidth = $w(‘#portfolioRepeaterContainer’).getBoundingClientRect().width + 50; // Account for margin
scrollBox.scrollTo(scrollBox.scrollLeft + itemWidth, 0);
}, 5000); // Every 5 seconds
}

function stopAutoScroll() {
clearInterval(scrollInterval);
}

$w(‘#portfolioScrollBox’).onMouseIn(() => stopAutoScroll());
$w(‘#portfolioScrollBox’).onMouseOut(() => startAutoScroll());
2. Dynamic Styling
Issue: You’re trying to dynamically apply styles like marginRight and height in onItemReady, but Velo doesn’t allow direct manipulation of style properties like this.

Solution: You can apply styles via classes and then toggle those classes dynamically. Use the .addClass() and .removeClass() to apply custom styles in the Velo code.

Here’s an example:

/* In Custom CSS */
.portfolio-item {
margin-right: 50px;
}
.portfolio-item-last {
margin-right: 0;
}

// In Velo code
$w(‘#portfolioRepeater’).onItemReady(($item, itemData, index) => {
const isLastItem = index === itemData.length - 1;
$item(‘#portfolioRepeaterContainer’).addClass(‘portfolio-item’);
if (isLastItem) {
$item(‘#portfolioRepeaterContainer’).addClass(‘portfolio-item-last’);
}
});
3. Measuring Element Dimensions
Issue: You’re trying to use getBoundingClientRect on repeater elements but it’s throwing an error because the method isn’t supported on Container or Box.

Solution: You can use $w(‘#elementId’).getBoundingClientRect() on elements such as images, text, or buttons. For containers, you can wrap the child elements in other types like Image or Text elements to get accurate measurements.

$w(‘#portfolioImage’).onReady(() => {
const imageRect = $w(‘#portfolioImage’).getBoundingClientRect();
console.log(imageRect.width, imageRect.height);
});
4. Image Load Error Handling
Issue: You’re attempting to handle image load errors using onError() but Velo doesn’t support this directly on Image elements.

Solution: Use the onError event for image elements, but Velo doesn’t support it for the Image element in the Classic Editor. Instead, use the onReady event and check if the image has loaded successfully. If it hasn’t, apply a fallback behavior (e.g., hide the image and show a placeholder background).

$w(‘#portfolioImage’).onReady(() => {
const imageSrc = $w(‘#portfolioImage’).src;
const img = new Image();
img.onload = function() {
console.log(“Image loaded successfully”);
};
img.onerror = function() {
$w(‘#portfolioImage’).hide(); // Hide image on error
$w(‘#portfolioRepeaterContainer’).style.backgroundColor = ‘#ccc’; // Show placeholder
};
img.src = imageSrc;
});
5. ARIA Attributes and Accessibility
Issue: You’re trying to set ARIA roles like role=“region” and role=“listitem” dynamically, but these methods don’t thetollroads work with Velo’s $w() API.

Solution: Unfortunately, Velo’s API does not directly support setAttribute() for ARIA attributes on Box or Container. A workaround would be to add role and aria-label directly within the HTML elements using Custom Code:

Alternatively, you can use setAttribute() in custom code (Custom Code section in your Wix Dashboard) and apply attributes to elements outside the Velo API scope.

Best Regard,
Aleyna

Hey Aleyna,

Thank you so much for taking the time to provide such a detailed breakdown and suggestions! Your response confirms my suspicion that I’m running into specific API restrictions for certain elements (Box, Container, Image within repeaters) that prevent a straightforward implementation of my desired custom portfolio gallery.

I appreciate the workarounds you’ve proposed.

However, based on the errors I encountered before posting and documented in my original question (and re-tested with the latest code structure), several of the suggested approaches still seem blocked by the same API limitations.

Here’s a point-by-point look at the suggestions versus the errors I’m seeing:

1. Auto-Scrolling the Gallery (scrollTo, getBoundingClientRect)

  • Suggestion: Use element.scrollLeft or potentially scrollTo(x, y) with setInterval, pausing with onMouseIn/onMouseOut.
  • Issue Encountered: The primary blocker here seems to be accessing the necessary methods/properties on the scroll container (#portfolioScrollBox, which is a Box element).
    • The scrollTo(x, y) method consistently throws an “Expected 0 arguments, but got 2” error, indicating the Velo API for Box likely doesn’t support coordinate-based scrolling this way.
    • Attempts to measure the container or repeater items using getBoundingClientRect() also fail with “Property ‘getBoundingClientRect’ does not exist on type ‘Box’” or “…on type ‘Container’”.

Error: scrollTo(x, y) on Box
(Occurs on the line scrollContainer.scrollTo(currentScrollX, 0))

Error Message: Expected 0 arguments, but got 2.

Error: getBoundingClientRect on Container/Box
(Occurs when called on $item('#portfolioRepeaterContainer') or $w('#portfolioScrollBox'))

Error Message: Property 'getBoundingClientRect' does not exist on type 'Container'.
(or Property 'getBoundingClientRect' does not exist on type 'Box'.)

Without reliable programmatic scrolling or measurement, implementing the smooth auto-scroll with calculated distances seems unfeasible via these Velo methods.

2. Dynamic Styling & Mobile Animation Trigger (.style, .addClass)

  • Suggestion: Use CSS classes toggled via .addClass() / .removeClass() in onItemReady instead of direct .style manipulation for things like margins.
  • Issue Encountered: This was one of the approaches I tried, but manipulating classes or styles on the repeater item’s container (#portfolioRepeaterContainer) also seems blocked.
    • Calling .addClass('in-view') results in “Property ‘addClass’ does not exist on type ‘Container’”. This prevents triggering the CSS transition for the mobile slide-in animation.
    • Directly setting .style.marginRight (or other style properties like fontSize, height) results in “Property ‘style’ does not exist on type ‘Container’” or errors about specific properties not existing on type ‘Style’.

Error: addClass on Repeater Item Container
(Occurs on the line container.addClass('in-view');)

Error Message: Property 'addClass' does not exist on type 'Container'.

Error: .style on Repeater Item Container
(Occurs on the line itemContainer.style.marginRight = ...)

Error Message: Property 'style' does not exist on type 'Container'.

Error: .style properties (e.g., height, fontSize)
(Occurs on lines like buttonElement.style.height = '26px'; or titleElement.style.fontSize = '30px';)

Error Message: Property 'height' does not exist on type 'Style'.
(or Property 'fontSize' does not exist on type 'Style'.)

This limitation makes it very difficult to apply index-specific styling (like the last item margin) or trigger class-based animations dynamically via Velo within the repeater.

3. Measuring Element Dimensions (getBoundingClientRect)

  • Suggestion: Use getBoundingClientRect() on child elements like Images or Text instead of the container.
  • Acknowledgement: This confirms the limitation on containers/boxes. While measuring child elements is a potential fallback, it might be less accurate for calculating the overall item width needed for the scroll distance, especially considering padding and margins on the container itself. The core issue of measuring the scroll container (#portfolioScrollBox) likely remains due to the errors mentioned in point #1.

4. Image Load Error Handling (.onError)

  • Suggestion: Use the native JavaScript Image object workaround since Velo’s .onError isn’t supported.
  • Acknowledgement: This looks like a very promising and clever workaround! Thank you for suggesting it – I believe this approach should work to achieve the desired fallback behavior.

Error: .onError on Image Element
(Occurs on the line imageElement.onError(...))

Error Message: Property 'onError' does not exist on type 'Image'.

(Your workaround bypasses this Velo limitation.)

5. ARIA Attributes (setAttribute)

  • Suggestion: Set attributes in the Editor or via non-Velo Custom Code, as .setAttribute isn’t supported via Velo on these elements.
  • Acknowledgement: This confirms the limitation I observed. Setting static roles in the Editor is feasible. Dynamically setting aria-label based on the service title would indeed require moving that logic outside the Velo Page Code, likely into a separate <script> tag in the Custom Code section.

Error: setAttribute on Box/Container
(Occurs on lines like scrollBox.setAttribute('role', 'region');)

Error Message: Property 'setAttribute' does not exist on type 'Box'.
(or Property 'setAttribute' does not exist on type 'Container'.)

Summary of Remaining Roadblocks

Based on these persistent API errors, it seems the core requirements for:

  1. JS-controlled auto-scroll with specific coordinates/distances and hover pause (due to scrollTo and getBoundingClientRect issues).
  2. Dynamic Velo styling (margins, sizes) and class manipulation (.addClass) on repeater item containers.

remain blocked using the standard Velo APIs for Box/Container elements within the Classic Editor.

Further Questions

Given these limitations:

  • Are there any alternative, perhaps less common, Velo APIs or techniques within the Classic Editor that can achieve programmatic scrolling of a container (like a Box) or reliable measurement? (For example, does manipulating scrollLeft directly actually work on a $w.Box even if scrollTo doesn’t?)
  • Is there a different structural approach recommended? For example, would using a Custom Element (if feasible to build/integrate for this purpose in Classic) provide more control, or are there other container types available in the Classic Editor that have more robust Velo APIs for scrolling and styling?
  • Or, is this level of dynamic control and interaction for a custom scrolling repeater truly beyond the capabilities of Velo when applied to standard containers/boxes within the Wix Classic Editor environment?

Any further insights or alternative strategies you (or anyone else in the community) might have would be greatly appreciated!

Thanks again for your help!