Unplanned
Last Updated: 07 Jan 2026 14:02 by ADMIN
Isaac
Created on: 05 Jan 2026 14:33
Category: UI for Blazor
Type: Feature Request
1
Magnifiable Image Component

Magnifiable Image Component

A Blazor component designed to provide an interactive image magnification experience, similar to popular eCommerce websites.

Features

  • Interactive Magnifier: On mouse hover, a magnifier appears beside the image, following the cursor and displaying a zoomed-in portion of the image. At the same time an overlay appears on the image indicating the zoomed-in portion of the image.
  • Screen Space Awareness: The magnifier dynamically stretches to fill the available space to the right or left of the image, ensuring optimal use of the viewport and consistent margins.
  • Popover Integration: Utilizes Telerik's Popover for the magnifier, ensuring it appears above all other UI elements and avoids clipping or stacking issues.
  • Configurable Magnification: The magnification scale is configurable via the MagnifyScale parameter.
  • Accessibility: The image is wrapped in a button for keyboard accessibility, and all images support alt text.
  • Full-Size View: Clicking the image opens a modal window displaying the image at its actual size.

Sample Code

MagnifiableImage.razor

@inject IJSRuntime JS
@using Microsoft.JSInterop

@* Container *@
<div @ref="_containerRef" class="@($"magnifiable-image-container {Class}")" style="height: @Height;  width: @Width;">
    
    @* Image *@
    <button @onclick="@OnClick" class="magnifiable-image-button">
        <img src="@Image.Src" alt="@Image.Alt" style="height: 100%; width: 100%;"
             @onmousemove="@OnMouseMove" @onmouseenter="@OnMouseEnterAsync" @onmouseleave="@OnMouseLeave"/>
    </button>
    
    @* Magnifier *@
    <TelerikPopover @ref="_popoverRef" AnchorSelector=".magnifiable-image-container" Position="@(_showOnRight ? PopoverPosition.Right : PopoverPosition.Left)" Offset="@MagnifierMargin"
                    Width="@($"{_magnifierWidth}px")" Height="@($"{_magnifierHeight}px")" Class="popover-magnifier" Collision="PopoverCollision.Fit">
        <PopoverContent>
            
            @* Magnified Image *@
            <img src="@Image.Src" alt="@Image.Alt" class="magnified-image"
                 style="@($"width: {_magnifiedImageWidth}px; height: {_magnifiedImageHeight}px; transform: translateX({_magnifiedImageTransformX}px) translateY({_magnifiedImageTransformY}px); left: {_magnifiedImageLeft}px; top: {_magnifiedImageTop}px;")"/>
        
        </PopoverContent>
    </TelerikPopover>
    
    @* Magnifier Overlay *@
    @if (_isMouseOver)
    {
        <div class="magnifier-overlay" 
             style="@($"width: {_magnifierOverlayWidth}px; height: {_magnifierOverlayHeight}px; transform: translateX({_magnifierOverlayTransformX}px) translateY({_magnifierOverlayTransformY}px);  left: {_magnifierOverlayLeft}px; top: {_magnifierOverlayTop}px;")))">
        </div>
    }
    
    @* Actual Image *@
    <TelerikWindow @bind-Visible="@_isClicked" Modal="true" CloseOnOverlayClick="true" Draggable="false" Resizable="false" Class="window-rounded">
        <WindowActions>
            <WindowAction Name="Close"/>
        </WindowActions>
        <WindowContent>
            <img src="@Image.Src" alt="@Image.Alt"/>
        </WindowContent>
    </TelerikWindow>
    
</div>

<style>
    .magnifiable-image-container {
        position: relative;
        display: inline-block;
        cursor: zoom-in;
    }
    
    .magnifiable-image-button {
        background: none;
        border: none;
        padding: 0;
        margin: 0;
        font: inherit;
        color: inherit;
        cursor: inherit !important;
        outline: none;
        box-shadow: none;
        appearance: none;
        -webkit-appearance: none;
        -moz-appearance: none;
        display: block;
        width: 100%;
        height: 100%;
    }
        .magnifiable-image-button:focus-visible {
            outline: none;
            box-shadow: 0 0 0 2px color-mix(in srgb, var(--kendo-color-on-app-surface, #424242) 50%, transparent);
        }
    
    .popover-magnifier {
        overflow: hidden;
        border-radius: 0;
        position: relative;
        top: @(_adjustForTelerikFit ? $"{(_showOnBottom ? "" : "-")}{MagnifierMargin/4}px" : $"{(_showOnBottom ? "" : "-")}{MagnifierMargin}px");
    }
    
    .magnified-image {
        position: absolute;
    }
    
    .magnifier-overlay {
        position: absolute;
        background: color-mix(in srgb, var(--kendo-color-primary, #1274AC) 15%, transparent);
        pointer-events: none;
        z-index: 5;
        box-sizing: border-box;
        display: block;
    }
</style>

@code 
{
    [Parameter] public required ImageInfo Image { get; set; }

    /// <summary>
    /// The height of the image (e.g., "200px", "100%", or "auto"). Default is "auto".
    /// </summary>
    [Parameter]
    public string Height { get; set; } = "auto";

    /// <summary>
    /// The width of the image (e.g., "200px", "100%", or "auto"). Default is "auto".
    /// </summary>
    [Parameter]
    public string Width { get; set; } = "auto";

    /// <summary>
    /// The magnification scale for the magnifier. Default is 3 (3x magnification).
    /// </summary>
    [Parameter] 
    public double MagnifyScale { get; set; } = 3;

    /// <summary>
    /// Applies additional CSS classes to the MagnifiableImage's root element for custom styling and visual modifications.
    /// </summary>
    [Parameter]
    public string Class { get; set; } = string.Empty;

    private const int MagnifierMargin = 24;
    
    // State for magnifier visibility and container reference
    private bool _isMouseOver;
    private bool _isClicked;
    private ElementReference _containerRef;
    private TelerikPopover? _popoverRef;

    // Image and magnified image dimensions
    private double _imageWidth;
    private double _imageHeight;
    private double _magnifiedImageWidth;
    private double _magnifiedImageHeight;
    
    // Magnifier position and size
    private bool _showOnRight = true;
    private bool _showOnBottom = true;
    private bool _adjustForTelerikFit;
    private double _magnifierWidth;
    private double _magnifierHeight;

    // Magnified image offset within the magnifier
    private double _magnifiedImageLeft;
    private double _magnifiedImageTop;
    
    // Mouse position clamping bounds
    private double _minMouseX;
    private double _minMouseY;
    private double _maxMouseX;
    private double _maxMouseY;
    
    // Mouse position and transform for magnified image
    private double _mouseX;
    private double _mouseY;
    private double _magnifiedImageTransformX;
    private double _magnifiedImageTransformY;

    // Magnifier overlay size and transform
    private double _magnifierOverlayWidth;
    private double _magnifierOverlayHeight;
    private double _magnifierOverlayLeft;
    private double _magnifierOverlayTop;
    private double _magnifierOverlayTransformX;
    private double _magnifierOverlayTransformY;

    private void OnClick()
    {
        _isClicked = true;
    }

    private async Task OnMouseEnterAsync()
    {
        _isMouseOver = true;
        _popoverRef?.Show();
        
        // Get layout info about the image container using getElementLayoutInfo from wwwroot/js/magnifiable-image.js
        var containerLayoutInfo = await JS.InvokeAsync<ElementLayoutInfo>("getElementLayoutInfo", _containerRef);
        
        // Store image size
        _imageWidth = containerLayoutInfo.Width;
        _imageHeight = containerLayoutInfo.Height;
        _magnifiedImageWidth = _imageWidth * MagnifyScale;
        _magnifiedImageHeight = _imageHeight * MagnifyScale;
        
        // Determine magnifier position based on available space
        _showOnRight = containerLayoutInfo.DistanceFromViewportRight >= containerLayoutInfo.DistanceFromViewportLeft;
        _adjustForTelerikFit = Math.Abs(containerLayoutInfo.DistanceFromViewportBottom - containerLayoutInfo.DistanceFromViewportTop) < 20;
        _showOnBottom = containerLayoutInfo.DistanceFromViewportBottom >= containerLayoutInfo.DistanceFromViewportTop;
        
        // Calculate magnifier size based on available space
        _magnifierWidth = _showOnRight 
            ? containerLayoutInfo.DistanceFromViewportRight - MagnifierMargin*2
            : containerLayoutInfo.DistanceFromViewportLeft - MagnifierMargin*2;
        _magnifierHeight = containerLayoutInfo.ViewportHeight - MagnifierMargin*2;
        
        // Center the magnified image in the magnifier
        _magnifiedImageLeft = (_magnifierWidth / 2) - (_imageWidth / 2); 
        _magnifiedImageTop = (_magnifierHeight / 2) - (_imageHeight / 2);
        
        // Calculate min and max mouse X/Y to prevent showing empty space in the magnifier
        _minMouseX = Math.Floor((_magnifierWidth / MagnifyScale) / 2);
        _minMouseY = Math.Floor((_magnifierHeight / MagnifyScale) / 2); 
        _maxMouseX = Math.Ceiling(_imageWidth - ((_magnifierWidth / MagnifyScale) / 2));
        _maxMouseY = Math.Ceiling(_imageHeight - ((_magnifierHeight / MagnifyScale) / 2));

        // Calculate magnifier overlay size and position
        _magnifierOverlayWidth = Math.Floor(Math.Clamp(_magnifierWidth / MagnifyScale, 0, _imageWidth)) - 1;
        _magnifierOverlayHeight = Math.Floor(Math.Clamp(_magnifierHeight / MagnifyScale, 0, _imageHeight)) - 1;
        _magnifierOverlayLeft = -((_magnifierOverlayWidth / 2) + 1);
        _magnifierOverlayTop = -((_magnifierOverlayHeight / 2) + 1);
    }

    private void OnMouseLeave()
    {
        _isMouseOver = false;
        _popoverRef?.Hide();
    }

    private void OnMouseMove(MouseEventArgs e)
    {
        // Clamp mouse X/Y to prevent showing empty space in the magnifier
        if (_minMouseX > _maxMouseX) _mouseX = _imageWidth / 2;
        else _mouseX = Math.Clamp(e.OffsetX, _minMouseX, _maxMouseX);
        if (_minMouseY > _maxMouseY) _mouseY = _imageHeight / 2;
        else _mouseY = Math.Clamp(e.OffsetY, _minMouseY, _maxMouseY);
        
        // Calculate the transform for the magnified image
        _magnifiedImageTransformX = -Math.Round((_mouseX * MagnifyScale) - (_imageWidth / 2));
        _magnifiedImageTransformY = -Math.Round((_mouseY * MagnifyScale) - (_imageHeight / 2));

        // Calculate the transform for the magnifier overlay
        _magnifierOverlayTransformX = Math.Round(_mouseX);
        _magnifierOverlayTransformY = Math.Round(_mouseY);
        
        _popoverRef?.Refresh();
    }

    private record ElementLayoutInfo(
        double Width, 
        double Height, 
        double ViewportHeight,
        double DistanceFromViewportLeft, // distance from viewport's left edge to element's left edge
        double DistanceFromViewportRight, // distance from element's right edge to viewport's right edge
        double DistanceFromViewportTop, // distance from viewport's top edge to element's top edge
        double DistanceFromViewportBottom); // distance from element's bottom edge to viewport's bottom edge
}

magnifiable-image.js

// Returns width, height, and the space to the left and right of the element relative to the viewport
window.getElementLayoutInfo = (element) => {
    if (!element) return null;
    const elementRect = element.getBoundingClientRect();
    return {
        width: elementRect.width,
        height: elementRect.height,
        viewportHeight: window.innerHeight,
        distanceFromViewportLeft: elementRect.left, // distance from viewport's left edge to element's left edge
        distanceFromViewportRight: window.innerWidth - elementRect.right, // distance from element's right edge to viewport's right edge
        distanceFromViewportTop: elementRect.top, // distance from viewport's top edge to element's top edge
        distanceFromViewportBottom: window.innerHeight - elementRect.bottom // distance from element's bottom edge to viewport's bottom edge
    };
};

Note

Only tested in Blazor WebAssembly. The component may see performance issues in Blazor Server.

3 comments
ADMIN
Hristian Stefanov
Posted on: 07 Jan 2026 14:02

Hi Isaac,

Thank you for clarifying the exact functionality you are looking for.

I have consulted with our Product Manager and can confirm that such a component would be a valuable addition to our UI components library. As a result, I am updating the status of this item to Unplanned, which indicates that the request is valid and will be considered for future development.

Regards,
Hristian Stefanov
Progress Telerik

Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.

Isaac
Posted on: 06 Jan 2026 16:01

Hi Hristian,

Thank you for the response!

To begin with, I don't think there is any missing functionality or limitation with the Carousel component. I understand that it is possible to scale up images within a carousel on click with some simple CSS and HTML event handling.

However, that is not the scenario I described. This feature request is for a new Blazor component that magnifies (or zooms) an image in two ways:

  1. On hover, a popover appears to the left or right of the actual image that displays a zoomed-in portion of the image that follows the user's cursor. At the same time, an overlay appears on the image indicating the zoomed-in portion of the image.
  2. On click, a window appears that shows the actual, unrestricted size of the image.

There is no single, existing Telerik Blazor component that fulfills this feature request. The sample code shows one way to implement this feature request in one Blazor component using Telerik Popover and Window components, HTML elements and events, CSS, JavaScript, and C#.

For reference, the scenario I described is common on eCommerce websites, namely Amazon.com. Spend less. Smile more. and Walmart | Save Money. Live better.. Also, DevExpress has an ASP.NET component for this scenario: Image Zoom | ASP.NET Web Forms Controls | DevExpress Documentation.

ADMIN
Hristian Stefanov
Posted on: 06 Jan 2026 14:43

Hi Isaac,

This scenario can currently be implemented using the Carousel component. You can render the images on the left side and, when an image is clicked, update the component’s @bind-Page parameter to change the selected item. For the magnifier, use a small CSS to achieve it like this:

@using Telerik.Blazor.Components

<TelerikCarousel Arrows="@ShowArrows"
                 Pageable="@ShowPager"
                 LoopPages="@LoopPages"
                 @bind-Page="@PageIndex"
                 AutomaticPageChange="@AutoChange"
                 AutomaticPageChangeInterval="@ChangeInterval"
                 Data="@CarouselData"
                 Width="512px"
                 Height="384px"
                 ThemeColor="@ThemeColor">

    <Template>
        <div class="zoom-container"
             @onclick="() => ToggleZoom(context.ImageID)">
            <img src="https://picsum.photos/512/384?random=@(context.ImageID)"
                 alt="Photograph"
                 class="zoom-img @(ZoomedId == context.ImageID ? "zoomed" : "")" />
        </div>
    </Template>

</TelerikCarousel>

<style>
    .k-scrollview {
        margin: 0 auto;
    }

    .zoom-container {
        width: 512px;
        height: 384px;
        overflow: hidden;
        cursor: zoom-in;
    }

    .zoom-img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        transition: transform 0.3s ease;
    }

        .zoom-img.zoomed {
            transform: scale(2);
            cursor: zoom-out;
        }
</style>

@code {
    public bool ShowArrows = true;
    public bool ShowPager = false;
    public bool LoopPages = false;
    public bool AutoChange = false;

    private int PageIndex = 1;
    private int ChangeIntervalSec = 3;

    public int ChangeInterval =>
        (ChangeIntervalSec >= 1 ? ChangeIntervalSec : 1) * 1000;

    public string ThemeColor { get; set; } =
        ThemeConstants.Carousel.ThemeColor.Light;

    public List<CarouselModel> CarouselData { get; set; }

    private int? ZoomedId { get; set; } = null;

    protected override Task OnInitializedAsync()
    {
        CarouselData = Enumerable.Range(1, 7)
            .Select(x => new CarouselModel { ImageID = x })
            .ToList();

        return base.OnInitializedAsync();
    }

    void ToggleZoom(int id)
    {
        ZoomedId = ZoomedId == id ? null : id;
    }

    public class CarouselModel
    {
        public int ImageID { get; set; }
    }
}

Since this functionality is already achievable with our existing components, I’m marking this item as completed. If you feel there is any missing functionality or limitation in the component that prevents your use case, please let me know so I can assist further.

Regards,
Hristian Stefanov
Progress Telerik

Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.