How to Customize Recently Viewed Products in Hyvä? 

Hyvä, by default, provides a recently viewed products section, which can be enabled with just a few simple steps.

Navigate to Stores > Configuration > Catalog > Catalog > Recently Viewed/Compared Products and set the “Enable Recently Viewed Products” option to “Yes.

Enable recently viewed products

Once enabled, you will see the Recently Viewed Products block on the frontend.

Recently viewed products block on the frontend

However, the default design may not always align with the overall aesthetics of your website. To maintain design consistency and enhance user experience, customizing the recently viewed products section using Tailwind CSS is highly recommended.

Why Customize?

  • Ensures consistency with your site’s visual design using Tailwind CSS
  • Improves performance and responsiveness, especially on mobile devices
  • Increases product visibility and boosts conversions by encouraging users to revisit previously viewed items

To achieve this, you will need to override both the frontend templates and potentially the logic behind them.

Steps to Customize Recently Viewed Products in Hyvä:

Update product-slider-container.phtml at:

app/design/frontend/Meetanshi/hyva_werra/Magento_Catalog/templates/product/slider/product-slider-container.phtml

<?php
/**
 * Hyvä Themes - https://hyva.io
 * Copyright © Hyvä Themes 2020-present. All rights reserved.
 * This product is licensed per Magento install
 * See https://hyva.io/license
 */

declare(strict_types=1);

use Hyva\Theme\Model\ViewModelRegistry;
use Hyva\Theme\ViewModel\Cart\Items as CartItems;
use Hyva\Theme\ViewModel\ProductCompare;
use Hyva\Theme\ViewModel\ProductList;
use Hyva\Theme\ViewModel\Slider;
use Hyva\Theme\ViewModel\Store;
use Hyva\Theme\ViewModel\Wishlist;
use Magento\Catalog\Model\Product\Visibility as ProductVisibility;
use Magento\Framework\Escaper;
use Magento\Framework\View\Element\Template;

/** @var Template $block */
/** @var Escaper $escaper */
/** @var ViewModelRegistry $viewModels */
/** @var Slider $sliderViewModel */
/** @var ProductList $productListViewModel */
/** @var Wishlist $wishlistViewModel */
/** @var ProductCompare $compareViewModel */
/** @var Store $storeViewModel */

$sliderViewModel      = $viewModels->require(Slider::class);
$productListViewModel = $viewModels->require(ProductList::class);
$wishlistViewModel    = $viewModels->require(Wishlist::class);
$compareViewModel     = $viewModels->require(ProductCompare::class);
$storeViewModel       = $viewModels->require(Store::class);

$sliderName        = str_replace('.', '_', $block->getNameInLayout());
$categoryIds       = $block->getData('category_ids') ?: '';
$isAnchorCategory  = $block->getData('include_child_category_products');
$pageSize          = $block->getData('page_size') ?: 8;
$priceFrom         = $block->getData('price_from');
$priceTo           = $block->getData('price_to');
$sortAttribute     = $block->getData('sort_attribute') ?: '';
$sortDirection     = $block->getData('sort_direction') ?: 'ASC';
$title             = $block->getData('title') ?: '';
$headingTag        = $block->getData('heading_tag') ?: 'h3';
$hideDetails       = $block->getData('hide_details') ?? false;
$hideRatingSummary = $block->getData('hide_rating_summary') ?? false;
$type              = $block->getData('type');
$skusFilter        = $block->getData('product_skus') ? explode(',', $block->getData('product_skus')) : [];
$additionalFilters = (array) $block->getData('additional_filters');
$itemTemplate      = $block->getData('item_template') ?? 'Magento_Catalog::product/list/item.phtml';
$containerTemplate = $block->getData('container_template')
    ?? 'Magento_Catalog::product/slider/product-slider-container.phtml';


// The number of slides visible on the xl breakpoint
$maxVisibleSlides = $block->getData('max_visible'); // default to null
// Passed to the product slider container
$slideSectionClasses = $block->getData('maybe_purged_tailwind_section_classes'); // default to null
$slideItemClasses = $block->getData('maybe_purged_tailwind_slide_item_classes'); // default to null


// In case $type is not set on the block, it will default the block class name, which is not what we need in this template.
$type = is_string($type) && strpos($type, '\\') === false  ? $type : 'generic';

if ($categoryIds) {
    $productListViewModel->addFilter('category_id', $categoryIds, 'in');
}

if ($isAnchorCategory) {
    // Only has an effect if a single category ID filter is set, and that category is an anchor category
    $productListViewModel->includeChildCategoryProducts();
}

if ($priceFrom) {
    $productListViewModel->addFilter('price', $priceFrom, 'gteq');
}
if ($priceTo) {
    $productListViewModel->addFilter('price', $priceTo, 'lteq');
}

if ($hideRatingSummary) {
    $productListViewModel->excludeReviewSummary();
}

if ($skusFilter) {
    $productListViewModel->addFilter('sku', array_map('trim', $skusFilter), 'in');
}

foreach ($additionalFilters as $filter) {
    $productListViewModel->addFilter($filter['field'], $filter['value'], $filter['conditionType'] ?? 'eq');
}

$productListViewModel->setPageSize($pageSize);
$productListViewModel->addFilter('website_id', $storeViewModel->getWebsiteId());
$productListViewModel->addFilter('visibility', [
    ProductVisibility::VISIBILITY_IN_CATALOG,
    ProductVisibility::VISIBILITY_BOTH,
], 'in');
if ($sortAttribute) {
    $sortDirection === 'ASC'
        ? $productListViewModel->addAscendingSortOrder($sortAttribute)
        : $productListViewModel->addDescendingSortOrder($sortAttribute);
}

if (in_array($type, ['related', 'upsell', 'crosssell'], true)) {

    $items = $type === 'crosssell'
        ? $productListViewModel->getCrosssellItems(...$viewModels->require(CartItems::class)->getCartItems())
        : $productListViewModel->getLinkedItems($type, $block->getProduct());

} else {
    $items = $productListViewModel->getItems();
}

$sliderHtml = $sliderViewModel->getSliderForItems($itemTemplate, $items, $containerTemplate)
    ->setData('hide_details', $hideDetails)
    ->setData('hide_rating_summary', $hideRatingSummary)
    ->setData('name', $sliderName)
    ->setData('title', $title)
    ->setData('item_relation_type', $type)
    ->setData('heading_tag', $headingTag)
    ->setData('max_visible', $maxVisibleSlides)
    ->setData('maybe_purged_tailwind_section_classes', $slideSectionClasses)
    ->setData('maybe_purged_tailwind_slide_item_classes', $slideItemClasses)
    ->toHtml();

if (empty($sliderHtml)) {
    return '';
}
$actionName = $this->getRequest()->getFullActionName();
$container = 'container';
if($actionName == 'catalog_product_view'){
    $container = '';
}
?>
<div class="product-slider <?= $container ?> <?= $escaper->escapeHtmlAttr($type) ?>-product-slider">
    <div>
        <?= /* @noEscape */ $sliderHtml ?>
    </div>
    <script>
        'use strict';
        window.addEventListener('DOMContentLoaded', function() {
            if (! window.productSliderEventHandlerInitialized) {
                window.productSliderEventHandlerInitialized = true;

                <?php if ($wishlistViewModel->isEnabled()): ?>
                window.addEventListener('product-add-to-wishlist', (event) => {
                    const formKey = hyva.getFormKey();
                    const postUrl = BASE_URL + 'wishlist/index/add/';
                    const productId = event.detail.productId;

                    fetch(postUrl, {
                        "headers": {
                            "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
                        },
                        "body": "form_key=" + formKey + "&product=" + productId + "&uenc=" + hyva.getUenc(),
                        "method": "POST",
                        "mode": "cors",
                        "credentials": "include"
                    }).then(function (response) {
                        if (response.redirected) {
                            window.location.href = response.url;
                        } else if (response.ok) {
                            return response.json();
                        } else {
                            typeof window.dispatchMessages !== "undefined" && window.dispatchMessages(
                                [{
                                    type: "warning",
                                    text: "<?= $escaper->escapeHtml(__('Could not add item to wishlist.')) ?>"
                                }], 5000
                            );
                        }
                    }).then(function (result) {
                        if (!result) {
                            return
                        }
                        typeof window.dispatchMessages !== "undefined" && window.dispatchMessages(
                            [{
                                type: (result.success) ? "success" : "error",
                                text: (result.success)
                                    ? "<?=
                                        $escaper->escapeHtml(
                                            __("%1 has been added to your Wish List.", __("Product"))
                                        )
                                        ?>" : result.error_message
                            }], 5000
                        );
                        window.dispatchEvent(new CustomEvent("reload-customer-section-data"));
                    }).catch(function (error) {
                        typeof window.dispatchMessages !== "undefined" && window.dispatchMessages(
                            [{
                                type: "error",
                                text: error
                            }], 5000
                        );
                    });
                })
                <?php endif; ?>

                <?php if ($compareViewModel->showInProductList()): ?>
                window.addEventListener('product-add-to-compare', (event) => {
                    const productId = event.detail.productId;
                    hyva.postForm({
                        action: BASE_URL + 'catalog/product_compare/add/',
                        data: {product: productId}
                    })
                })
                <?php endif; ?>
            }
        });
    </script>
</div>

Update product-slider.phtml at:

app/design/frontend/Meetanshi/hyva_werra/Magento_Catalog/templates/product/slider/product-slider.phtml

<?php
/**
 * Hyvä Themes - https://hyva.io
 * Copyright © Hyvä Themes 2020-present. All rights reserved.
 * This product is licensed per Magento install
 * See https://hyva.io/license
 */

declare(strict_types=1);

use Hyva\Theme\Model\ViewModelRegistry;
use Hyva\Theme\ViewModel\HeroiconsOutline;
use Hyva\Theme\ViewModel\ProductListItem;
use Hyva\Theme\ViewModel\ProductPage;
use Hyva\Theme\ViewModel\Store;
use Magento\Catalog\Block\Product\ReviewRendererInterface as ProductReviewRenderer;
use Magento\Framework\Escaper;
use Magento\Framework\View\Element\Template;

// phpcs:disable Generic.Files.LineLength.TooLong

/** @var Template $block */
/** @var Escaper $escaper */
/** @var ViewModelRegistry $viewModels */

/** @var Store $viewModelStore */
$viewModelStore = $viewModels->require(Store::class);

/** @var ProductPage $productViewModel */
$productViewModel = $viewModels->require(ProductPage::class);

/** @var HeroiconsOutline $heroicons */
$heroicons = $viewModels->require(HeroiconsOutline::class);

/** @var ProductListItem $productListItemViewModel */
$productListItemViewModel = $viewModels->require(ProductListItem::class);

$viewMode = 'grid';
$imageDisplayArea = 'category_page_grid';
$showDescription = false;

$name = (string) $block->getName();
$title = (string) $block->getTitle();
$headingTag = $block->getData('heading_tag') ?: 'h3';
$items = $block->getItems() ?? [];
if (is_object($items) && $items instanceof Iterator) {
    $items = iterator_to_array($items);
}
if (!$itemCount = count($items)) {
    return '';
}

$sliderIndex = 1;
$sliderItemRenderer = $block->getLayout()->getBlock('product_list_item')
    ?: $block->getChildBlock('slider.item.template')
    ?: $block->getLayout()->createBlock(Template::class);

$hideRatingSummary = true;
$hideDetails       = true;

$sliderItemRenderer->setData('hide_details', $hideDetails);
$sliderItemRenderer->setData('hide_rating_summary', $hideRatingSummary);

// The slider item renderer block is often a shared instance.
// If a specific item template is set for this slider, the previously set template must be reset later
// so the item template is only replaced for the one slider it is specified on.
$sharedItemRendererTemplate = null;
$isSharedItemRenderer       = $sliderItemRenderer !== $block->getChildBlock('slider.item.template');
if ($isSharedItemRenderer && $block->getChildBlock('slider.item.template')) {
    $sharedItemRendererTemplate = $sliderItemRenderer->getTemplate();
    $sliderSpecificItemTemplate = $block->getChildBlock('slider.item.template')->getTemplate();
    $sliderItemRenderer->setTemplate($sliderSpecificItemTemplate);
}

// The number of slides visible on the xl breakpoint
$maxVisibleSlides = $block->getData('max_visible') ?? 4;

// Breakpoints for 1 visible slider items on mobile, 2 visible on md, 3 on lg and 4 on xl (see $sliderPageSize).
$defaultSliderItemClasses = 'md:w-1/2 lg:w-1/3 xl:w-1/4';

$sliderSectionClasses = $block->getData('maybe_purged_tailwind_section_classes') ?? 'my-4 md:my-6 lg:my-8 xl:my-12 text-gray-700 body-font';
$slideItemClasses = $block->getData('maybe_purged_tailwind_slide_item_classes') ?? $defaultSliderItemClasses;

?>
<script>
    'use strict';

    function initSliderComponent() {
        return {
            active: 0,
            itemCount: 0,
            getSlider() {
                return this.$root.querySelector('.js_slides');
            },
            pageSize: 4,
            pageFillers: 0,
            calcPageSize() {
                const slider = this.getSlider();
                if (slider) {
                    this.itemCount = slider.querySelectorAll('.js_slide').length;
                    this.pageSize = Math.round(slider.clientWidth / slider.querySelector('.js_slide').clientWidth);
                    this.pageFillers = (
                        this.pageSize * Math.ceil(this.itemCount / this.pageSize)
                    ) - this.itemCount;
                }
            },
            calcActive() {
                const slider = this.getSlider();
                if (slider) {
                    const sliderItems = this.itemCount + this.pageFillers;
                    const calculatedActiveSlide = slider.scrollLeft / (slider.scrollWidth / sliderItems);
                    this.active = Math.round(calculatedActiveSlide / this.pageSize) * this.pageSize;
                }
            },
            scrollPrevious() {
                this.scrollTo(this.active - this.pageSize);
            },
            scrollNext() {
                this.scrollTo(this.active + this.pageSize);
            },
            scrollTo(idx) {
                const slider = this.getSlider();
                if (slider) {
                    const slideWidth = slider.scrollWidth / (this.itemCount + this.pageFillers);
                    slider.scrollLeft = Math.floor(slideWidth) * idx;
                    this.active = idx;
                }
            },
            skipCarouselToNavigation(navSelector) {
                const element = document.getElementById(navSelector)
                if (element) {
                    element.scrollIntoView({behavior: 'smooth', block: 'end'});
                    const button = element.querySelector('button:not([disabled])');
                    this.$nextTick(() => button && button.focus({preventScroll: true}))
                }
            }
        }
    }
</script>
<section
    class="<?= $escaper->escapeHtmlAttr($sliderSectionClasses) ?>"
    x-data="initSliderComponent()"
    x-init="calcPageSize();"
    x-id="['slider-nav', 'slider-desc', 'slider-id']"
    @resize.window.debounce="calcPageSize(); $nextTick( function() { calcActive() })"
    role="group"
    aria-roledescription="<?= $escaper->escapeHtmlAttr(__('Carousel')) ?>"
    aria-label="<?= $escaper->escapeHtmlAttr(__('Carousel %1', $title)) ?>"
    :aria-describedby="$id('slider-desc')"
    x-defer="intersect"
>
    <?php if ($items): ?>
        <div class="relative">
            <?php if ($title): ?>

                <div class="justify-between flex items-center xl:pt-6 xl:pb-3 mx-auto md:flex-row">
                    <<?= /* @noEscape */ $headingTag ?> class="text-[20px] md:text-[40px] text-th-primary-default title-font">
                        <?= /* @noEscape */ $title ?>
                    </<?= /* @noEscape */ $headingTag ?>>
                    <div>
                        <template x-if="itemCount > pageSize">
                            <div
                                class="gap-3 flex items-center justify-center py-4"
                                :id="$id('slider-nav')"
                            >
                                <button
                                    type="button"
                                    aria-label="<?= $escaper->escapeHtmlAttr(__('Previous slide')) ?>"
                                    :disabled="active === 0"
                                    class="bg-th-primary-lighter text-white flex-none p-1 md:p-3"
                                    :class="{ 'opacity-25 pointer-events-none' : active === 0 }"
                                    @click="scrollPrevious"
                                >
                                    <?= $heroicons->chevronLeftHtml("w-5 h-5", 25, 25, ['aria-hidden' => 'true']) ?>
                                </button>
                                
                                <button
                                    aria-label="<?= $escaper->escapeHtmlAttr(__('Next slide')) ?>"
                                    :disabled="active >= itemCount-pageSize"
                                    class="bg-th-primary-lighter text-white flex-none p-1 md:p-3"
                                    :class="{ 'opacity-25 pointer-events-none' : active >= itemCount-pageSize }"
                                    @click="scrollNext"
                                 >
                                    <?= $heroicons->chevronRightHtml("w-5 h-5", 25, 25, ['aria-hidden' => 'true']) ?>
                                </button>
                            </div>
                        </template>
                    </div>
                </div>
            <?php endif; ?>
            <span
                class="sr-only"
                :id="$id('slider-desc')"
                tabindex="-1"
            >
                <?= $escaper->escapeHtml(__('Navigating through the elements of the carousel is possible using the tab key. You can skip the carousel or go straight to carousel navigation using the skip links.')) ?>
            </span>

            <a
                href="#<?= $escaper->escapeHtmlAttr($name) ?>-slider-end"
                class="action skip sr-only"
            >
                <?= $escaper->escapeHtml(__('Press to skip carousel')) ?>
            </a>
            <button
                x-show="itemCount > pageSize"
                type="button"
                class="action skip sr-only "
                @click.prevent="skipCarouselToNavigation($id('slider-nav'))"
            >
                <?= $escaper->escapeHtml(__('Press to go to carousel navigation')) ?>
            </button>
            
            <div class="flex-none relative w-full ring-offset-2 active:ring-0 ring-blue-500/50">
                <div class="relative flex flex-nowrap -mx-1 overflow-auto js_slides snap"
                     @scroll.debounce="calcActive"
                >
                    <?php foreach ($items as $product): ?>
                        <div class="js_slide flex shrink-0 w-full p-1 <?= $escaper->escapeHtmlAttr($slideItemClasses) ?>"
                             role="group"
                             aria-label="<?= $escaper->escapeHtmlAttr(__('Item %1', $sliderIndex++)) ?>"
                             :aria-describedby="`slide-desc-<?= $escaper->escapeHtmlAttr($product->getId()) ?>-${$id('slider-id')}`"
                        >
                            <?= /** @noEscape */ $productListItemViewModel->getItemHtmlWithRenderer(
                                $sliderItemRenderer,
                                $product,
                                $block,
                                $viewMode,
                                ProductReviewRenderer::SHORT_VIEW,
                                $imageDisplayArea,
                                $showDescription
                            ) ?>
                        </div>
                    <?php endforeach; ?>
                    <?php for ($i = 0; $i < $maxVisibleSlides; $i++): /* Add empty filler slides in case the number of items is not dividable by the pagesize */ ?>
                        <div :class="{
                        'js_dummy_slide w-full flex-none <?= $escaper->escapeJs($slideItemClasses) ?>' : pageFillers > <?= (int) $i ?>
                        }"></div>
                    <?php endfor; ?>
                </div>
            </div>
            
            <span id="<?= $escaper->escapeHtmlAttr($name) ?>-slider-end" tabindex="-1"></span>
        </div>
    <?php endif; ?>
</section>
<?php

if ($sharedItemRendererTemplate) {
    $sliderItemRenderer->setTemplate($sharedItemRendererTemplate);
}

?>

After applying this code, the recently viewed product section will look something like this:

Recently viewed product section after adding second code

These templates can be tailored to implement your preferred layout, styling, and behavior, using Hyvä’s Alpine.js and Tailwind CSS approach.

By leveraging Hyvä’s flexible structure and modern tools like Tailwind CSS, you can tailor this feature to match your store’s design and functionality needs. 

With just a few template overrides, your storefront can provide a seamless, mobile-optimized view that encourages conversions and keeps customers engaged.

Also looking for a ready to use template? Try out Hyvä Werra theme templates—carefully crafted for modern design and optimal user experience.

Sanjay Jethva

Article by

Sanjay Jethva

Sanjay is the co-founder and CTO of Meetanshi with hands-on expertise with Magento since 2011. He specializes in complex development, integrations, extensions, and customizations. Sanjay is one the top 50 contributor to the Magento community and is recognized by Adobe. His passion for Magento 2 and Shopify solutions has made him a trusted source for...