How to Build a Custom 'Product Quick View' Module in Magento 2

Want to add a fast, polished "Product Quick View" modal to your Magento 2 store? In this post I’ll walk you through building a custom, production-ready Quick View module étape-by-étape. I’ll explain the technical architecture, show the exact fichiers and code snippets you need, and cover advanced topics like working with Magento caches and indexeurs, accessibility, responsive design, CSS/JS personnalisation, and test/débogage conseils. Think of this as a friendly walkthrough you can follow with a terminal open and your IDE ready.

Why a custom Product Quick View?

Quick View is an important UX pattern for e-commerce: it lets shoppers pavis product details without leaving the category or recherche page. But if implemented naively it can hurt performance, break caching, or be inaccessible. A custom Magento 2 module gives us full control: we can make it cache-friendly, index-aware, cléboard accessible, and easy to style or extend.

High-level technical architecture

Here’s a practical architecture I use for a performant Quick View:

  • List page (category/recherche) loads product tiles normally. Each tile contains a lightweight "Quick View" button with a product ID.
  • Clicking the button triggers an AJAX call to a contrôleur that returns fully-rendered product HTML for the modal. The contrôleur is configured for cache friendliness (we’ll discuss cacheable fragments and hole-punching).
  • Frontend JavaScript injects the returned HTML into a modal composant and handles cléboard, focus trap, and responsive behavior.
  • Styling handled via scoped CSS (Less or Sass compiled to CSS) and optional thème overrides.
  • All product data access uses Magento repositories, respecting indexeurs and flat/indexed tables where needed.

Why AJAX? Parce que page de catégories peut être full-page cached (FPC) and we don't want to break full-page cache when adding dynamic product modals. Loading product contenu via AJAX lets the page de catégorie remain cacheable while still delivering up-to-date product details.

Module skeleton

We’ll create a module called MageFine_QuickView (adjust vendor name to your company). The minimal fichier structure:

app/code/MageFine/QuickView/
├── etc
│   ├── module.xml
│   └── frontend
│       └── routes.xml
├── registration.php
├── Controller
│   └── Ajax
│       └── Product.php
├── Block
│   └── QuickView.php
├── view
│   └── frontend
│       ├── layout
│       │   └── quickview_ajax_product.xml
│       ├── templates
│       │   └── quickview.phtml
│       └── web
│           ├── js
│           │   └── quickview.js
│           └── css
│               └── quickview.css
└── composer.json (optional)

Registration and module declaration

// app/code/MageFine/QuickView/registration.php

Route and contrôleur

We expose an AJAX endpoint to fetch product HTML. Using a dedicated contrôleur under /quickview/ajax/product makes it easy.


<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <route id="magefine_quickview" frontName="quickview">
        <module name="MageFine_QuickView" />
    </route>
</routes>

// app/code/MageFine/QuickView/Controller/Ajax/Product.php
productRepository = $productRepository;
        $this->pageFactory = $pageFactory;
        $this->resultRawFactory = $resultRawFactory;
    }

    public function execute()
    {
        $productId = (int)$this->getRequest()->getParam('id');
        if (!$productId) {
            return $this->resultRawFactory->create()->setContents('');
        }

        try {
            // Load product via repository (respects indexers)
            $product = $this->productRepository->getById($productId);

            // Render a small page with our quickview template
            $page = $this->pageFactory->create();
            $page->getConfig()->getTitle()->set($product->getName());

            // Pass product to block (we will map block to template in layout file)
            $page->getLayout()->getBlock('magefine.quickview.block')->setProduct($product);

            // Render layout and return HTML
            $html = $page->getLayout()->getOutput();

            // IMPORTANT: mark response as non-cacheable on Varnish if needed, or set proper headers in frontend
            return $this->resultRawFactory->create()->setContents($html);
        } catch (\Exception $e) {
            // Log and return empty (graceful)
            // Use logger in production; for brevity we skip it here
            return $this->resultRawFactory->create()->setContents('');
        }
    }
}

Note: Above we used ProductRepositoryInterface->getById which is index-friendly. Repository will use normal Magento mechanisms, respecting product visibility and other indexes.

Layout and template

We need a layout that maps to the contrôleur and provides a block with a template:


<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="empty" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <block class="MageFine\QuickView\Block\QuickView" name="magefine.quickview.block" template="MageFine_QuickView::quickview.phtml" />
    </body>
</page>

// app/code/MageFine/QuickView/Block/QuickView.php
product = $product;
        return $this;
    }

    public function getProduct()
    {
        return $this->product;
    }
}

// app/code/MageFine/QuickView/view/frontend/templates/quickview.phtml

<div class="magefine-quickview" role="dialog" aria-modal="true" aria-label="getProduct()->getName()); ?>">
    <div class="quickview-inner">
        <h2 class="product-name"><?php echo $block->escapeHtml($block->getProduct()->getName()); ?></h2>
        <div class="product-image"><?php // use gallery block or helper to render images ?></div>
        <div class="product-price"><?php echo $block->getProduct()->getPrice(); ?></div>
        <div class="product-short-description"><?php echo $block->getProduct()->getShortDescription(); ?></div>
        <button class="action primary to-cart" data-product-id="<?php echo $block->getProduct()->getId(); ?>">Add to Cart</button>
    </div>
</div>

Frontend JavaScript (modal + AJAX)

Keep the JS small and focused. Use Magento's requirejs setup and UI composants if you like. Here’s a simple AMD module to fetch contenu and show a modal. It also handles cléboard and focus trap basics.

// app/code/MageFine/QuickView/view/frontend/web/js/quickview.js
define(['jquery'], function($) {
    'use strict';
    var QuickView = function(config) {
        this.ajaxUrl = config.ajaxUrl; // /quickview/ajax/product
        this.init();
    };

    QuickView.prototype.init = function() {
        var self = this;
        $(document).on('click', '[data-quickview-id]', function(e) {
            e.preventDefault();
            var id = $(this).data('quickview-id');
            self.open(id);
        });
    };

    QuickView.prototype.open = function(id) {
        var self = this;
        $.ajax({
            url: this.ajaxUrl,
            method: 'GET',
            data: {id: id},
            beforeSend: function() {
                self.showLoading();
            }
        }).done(function(html) {
            self.showModal(html);
        }).fail(function() {
            // handle error silently
        }).always(function() {
            self.hideLoading();
        });
    };

    QuickView.prototype.showModal = function(html) {
        var $modal = $('
', {class: 'magefine-qv-overlay', 'aria-hidden': 'false'}).append( $('
', {class: 'magefine-qv-modal', role: 'dialog', 'aria-modal': 'true'}).append(html) ); $('body').append($modal); this.trapFocus($modal); this.bindClose($modal); }; QuickView.prototype.bindClose = function($modal) { var self = this; $modal.on('click', function(e) { if ($(e.target).is('.magefine-qv-overlay')) { self.closeModal($modal); } }); $(document).on('keydown.mf_qv', function(e) { if (e.key === 'Escape') { self.closeModal($modal); } }); }; QuickView.prototype.closeModal = function($modal) { $modal.remove(); $(document).off('keydown.mf_qv'); // restore focus if needed }; QuickView.prototype.trapFocus = function($modal) { // lightweight focus trap for accessibility; production: use a well-tested library var focusable = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]'; var $first = $modal.find(focusable).first(); var $last = $modal.find(focusable).last(); $first.focus(); $modal.on('keydown', function(e) { if (e.key === 'Tab') { if (e.shiftKey && document.activeElement === $first[0]) { e.preventDefault(); $last.focus(); } else if (!e.shiftKey && document.activeElement === $last[0]) { e.preventDefault(); $first.focus(); } } }); }; QuickView.prototype.showLoading = function() { /* show spinner */ }; QuickView.prototype.hideLoading = function() { /* hide spinner */ }; return QuickView; });

Assurez-vous to initialize this script in your thème or module layout using a small inline script or via a Knockout composant depending on your stack. Example inline init in your category template or via requirejs-config mapping:

<script>
require(['MageFine_QuickView/js/quickview'], function(QuickView) {
    new QuickView({ajaxUrl: '/quickview/ajax/product'});
});
</script>

Integnote with Magento cache and indexation

C'est the clé part to get right. You want page de catégories to be cached by FPC (Varnish or built-in FPC), but Quick View contenu must reflect the current product state (prix, stock, options). Best practice:

  • Keep the page de catégorie cacheable by loading the Quick View contenu via AJAX. The AJAX endpoint peut être non-cacheable, or use private contenu approchees.
  • Quand vous build HTML server-side for the modal, include product cache tags so the block or custom cache peut être invalidated when product changes. That way, if you cache the quick view responses, they become invalidated on product save.
  • Si vous use Varnish and want to cache quickview responses for anonymous utilisateurs while respecting product updates, ensure responses include correct cache tags and surrogate clés (for exemple, with Magento's FPC integration). Often it's easier to keep AJAX responses short-lived or uncached if they contain dynamic tarification or client-dependent data.
  • Use ProductRepositoryInterface to fetch product data rather than raw SQL — it respects indexeurs. Si vous read attributes directly from EAV tables without using indexeurs, you might see inconsistent data until réindexer.

Example: adding cache tags in your block to tie quick view output to product cache invalidation:

// app/code/MageFine/QuickView/Block/QuickView.php (additions)
public function getCacheKeyInfo()
{
    $product = $this->getProduct();
    return [
        'MAGEFINE_QUICKVIEW',
        $product ? 'PRODUCT_' . $product->getId() : 'NO_PRODUCT',
        // include store and currency if needed
        $this->_storeManager->getStore()->getId(),
    ];
}

public function getCacheTags()
{
    $tags = parent::getCacheTags();
    $product = $this->getProduct();
    if ($product) {
        $tags[] = 'catalog_product_' . $product->getId();
    }
    return $tags;
}

Important: Si vous want to support client-specific prixs or personalized contenu inside Quick View, consider using Magento's client-data sections (private contenu) and making the AJAX endpoint return only cacheable HTML and fill private parts via JS using sections. Another pattern is to fetch product skeleton via cached HTML and then correctif dynamic parts via a small JSON endpoint.

Indexing concerns

Magento indexeurs like catalog_product_prix, catalog_product_attribute, inventaire, etc., control what valeurs are served fast. Si vous rely on attribut produits that are indexed (prixs, stock in MSI), load them using repository or collection APIs so the module reads the indexed valeurs. Si vous add new attribut produits that you render in Quick View, ensure they are set to be used in liste de produits or that réindexer runs after changes.

Advanced CSS / JS personnalisation for a smooth UX

Voici practical conseils for a fluid, polished experience:

  • Keep CSS isolated: precorrectif your classes (magefine-qv-*) or scope rules inside the modal container. This prevents thème conflicts.
  • Render only essential markup in the AJAX response. Lazy-load heavy assets like gallery images after modal opens (use a small placeholder initially).
  • Animate smartly: use transform and opacity transitions to keep animations GPU-friendly. Avoid animating width/height where possible.
  • Debounce interactions: if the modal triggers additional API calls (configurable options), debounce or cancel previous requests for better responsiveness.
  • Use CSS variables for thèmeable valeurs (colors, spacing) so thèmes can override only the variables.
/* app/code/MageFine/QuickView/view/frontend/web/css/quickview.css */
.magefine-qv-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:10000;}
.magefine-qv-modal{background:#fff;border-radius:6px;max-width:980px;width:95%;max-height:90vh;overflow:auto;transform:translateY(-10px);opacity:0;transition:opacity .22s ease,transform .22s ease;}
.magefine-qv-overlay[aria-hidden='false'] .magefine-qv-modal{transform:translateY(0);opacity:1;}
.magefine-qv-modal img{max-width:100%;height:auto;display:block;}

Example lazy-load pattern for images inside quickview.phtml:

<img src="/static/frontend/MageFine/theme/en_US/images/placeholder.png" data-src="<?php echo $block->escapeUrl($product->getImageUrl()); ?>" class="qv-lazy" alt="..." />

// JS: after modal shown, swap data-src to src
$modal.find('img.qv-lazy').each(function(){
    var $img = $(this);
    $img.attr('src', $img.data('src')).removeAttr('data-src');
});

Accessibility and responsive design

Quick View modals doit être accessible and mobile-friendly. A few rules I always follow:

  • Use role="dialog" and aria-modal="true" on the dialog container.
  • Give the dialog an accessible label: aria-label or aria-labelledby pointing to the product name heading.
  • Implement focus trap. Ensure focus is restored to the triggering element when the modal closes.
  • Keyboard controls: Escape to close, Tab/Shift+Tab to navigate inside. Buttons and links doit être cléboard reachable.
  • Screen reader utilisateurs: hide background contenu using aria-hidden on the rest of the page when the modal is open (you can add aria-hidden="true" to main contenu nodes while modal open).
  • Mobile: the modal devrait être full-screen or bottom-sheet on small viewports. Use media queries to change layout for narligne widths.

Responsive exemple (CSS):

@media (max-width: 640px){
    .magefine-qv-modal{width:100%;height:100%;border-radius:0;max-height:100vh;}
    .magefine-qv-modal .product-image{display:block;}
}

Performance considerations

Small things that add up:

  • Serve quickview JS/CSS via static contenu déployer and leverage bligneser caching (proper max-age headers in CDN or webserver).
  • Minify and bundle JS/CSS with your déploiement pipeline.
  • If using Varnish, avoid bypassing cache for page de catégories. Keep quick view separate via AJAX endpoints.
  • Consider caching quickview HTML for anonymous utilisateurs with short TTL and product cache tags — so product updates clear it automatically.
  • Profichier the AJAX endpoint with xdébogage or Blackfire when you notice slowness; the server-side render devrait être fast because it's a small layout (layout=empty) and only a single block.

Testing and débogage

Testing is often overlooked. Here’s a set of techniques to ensure stability in production:

Local development

  • Developer mode: bin/magento déployer:mode:set développeur (so erreurs are visible and static fichiers are served dynamically).
  • Enable template hints in dev to ensure your templates are loaded where you expect.

Logging & monitoring

  • Log contrôleur exceptions to var/log/magefine_quickview.log and check var/rapport for production erreurs.
  • Monitor New Relic or similar APM for slow AJAX responses. Track both latency and erreur rates.

Unit and test d'intégrations

  • PHPUnit: write test unitaires for any entreprise logic you add inside helpers or services.
  • Integration tests (Magento framework): test your contrôleur loads and returns contenu for a product correctifture.
  • JS tests: use Jasmine or Jest for critical logic in your quickview.js (focus trap, opening/closing behavior).

End-to-end (E2E) tests

  • Use Cypress or Selenium to assert the utilisateur flow: open category, click Quick View, modal opens, cléboard navigation works, Add to Cart adds a product, modal closes and cart updated.
  • Test on both desktop and mobile viewport sizes.

Debugging conseils

  • Bligneser devtools network tab to inspect AJAX responses and cache headers.
  • Check Varnish hits/misses when test anonymous utilisateurs; ensure page de catégorie remains a cache hit.
  • When product data seems stale, check indexeur status: bin/magento indexeur:status and réindexer if needed.

Advanced patterns and extensibility

Si vous want to extend the Quick View later, plan for these hooks:

  • Events: discorrectif an event after product HTML is rendered so other modules can append contenu (e.g., upsell block).
  • Plugin points: allow plugins on your contrôleur service layer to modify product data before rendering.
  • JS events: trigger custom events on modal open and close (document.discorrectifEvent) so tiers scripts can react without modifying module code.
// Example: dispatch event in controller after rendering
$this->_eventManager->dispatch('magefine_quickview_after_render', ['product' => $product, 'html' => &$html]);

// JS: trigger event when modal opens
var event = new CustomEvent('magefineQuickviewOpen', {detail: {productId: id}});
document.dispatchEvent(event);

Practical checklist before déployering to production

  1. Run full test suite (unit, integration, E2E).
  2. Verify page de catégories remain FPC hits after module enabled (test with Varnish).
  3. Validate quick view AJAX response times under load.
  4. Ensure accessibility: cléboard navigation, screen reader labels, focus restore.
  5. Confirm mobile layout and touch interactions.
  6. Bundle and minify assets and run static contenu déployer in production mode.
  7. Monitor logs and APM for the first hours after launch.

Common pitfalls and comment avoid them

  • Breaking FPC by rendering quick view server-side inside page de catégorie: avoid this, use AJAX.
  • Loading heavy gallery composants synchronously: lazy-load images and heavy JS only when needed.
  • Not restoring focus on modal close: remember to save the element that opened the modal and restore focus.
  • Relying on unindexed attribute valeurs: always use repository/collection APIs that respect indexeurs.
  • Not including product cache tags if you cache quick view HTML: tie cache to product tags to allow automatic invalidation.

Wrap up

Building a custom Product Quick View for Magento 2 is straightforward if you separate concerns: keep the page de catégorie cacheable, fetch product contenu via AJAX, use repository APIs to respect indexation, and pay attention to accessibility and responsiveness. Start with the skeleton in this post, and iterate: add lazy-loading, cache tags, and tests. That approche will keep your store fast and reliable while improving conversion with a convenient Quick View UX.

Si vous want, I can generate a ready-to-install sample module archive or show comment adapt the Quick View to configurable products, MSI stock, or prix rules. Tell me which product types you need and I’ll extend the exemples.