Kosmesis
Components

Lens

A magnifying-glass image zoom that follows the pointer. Purely presentational — no Morphos equivalent.

Installation

npx kosmesis add lens
pnpm dlx kosmesis add lens
yarn dlx kosmesis add lens
bunx kosmesis add lens

Copy and paste the following code into your project.

lens.tsx
import { StatefulComponent } from "@praxisjs/core";import { Component, Prop, Ref, State, type Ref as RefType } from "@praxisjs/decorators";import { cn } from "@/lib/utils";export interface LensProps {  src: string;  alt?: string;  zoom?: number;  lensSize?: number;  class?: string;}// Lens position/zoom recomputed on every pointer move and written to `style` directly (not// `@State`) since the container can be responsive.@Component()export class Lens extends StatefulComponent {  @Prop() src!: string;  @Prop() alt = "";  @Prop() zoom = 2;  @Prop() lensSize = 150;  @Prop() class?: string;  @Ref<HTMLDivElement>()  containerRef!: RefType<HTMLDivElement>;  @Ref<HTMLDivElement>()  lensRef!: RefType<HTMLDivElement>;  @State() _visible = false;  private readonly _handlePointerMove = (event: PointerEvent) => {    const container = this.containerRef.current;    const lens = this.lensRef.current;    if (!container || !lens) return;    const rect = container.getBoundingClientRect();    const x = event.clientX - rect.left;    const y = event.clientY - rect.top;    lens.style.left = `${String(x - this.lensSize / 2)}px`;    lens.style.top = `${String(y - this.lensSize / 2)}px`;    lens.style.backgroundSize = `${String(rect.width * this.zoom)}px ${String(rect.height * this.zoom)}px`;    lens.style.backgroundPosition = `${String(-(x * this.zoom - this.lensSize / 2))}px ${String(-(y * this.zoom - this.lensSize / 2))}px`;  };  render() {    return (      <div        ref={this.containerRef}        data-slot="lens"        class={cn("relative overflow-hidden", this.class)}        onPointerEnter={() => { this._visible = true; }}        onPointerLeave={() => { this._visible = false; }}        onPointerMove={this._handlePointerMove}      >        <img src={this.src} alt={this.alt} class="block size-full object-cover" />        <div          ref={this.lensRef}          data-slot="lens-glass"          data-visible={() => (this._visible ? "" : undefined)}          class="pointer-events-none absolute rounded-full border-2 border-background bg-no-repeat opacity-0 shadow-lg data-[visible]:opacity-100"          style={{ width: `${String(this.lensSize)}px`, height: `${String(this.lensSize)}px`, backgroundImage: `url(${this.src})` }}        />      </div>    );  }}

Install the following dependencies:

npm install @praxisjs/css
pnpm add @praxisjs/css
yarn add @praxisjs/css
bun add @praxisjs/css

Copy and paste the following code into your project.

lens.tsx
import { StatefulComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component, Prop, Ref, State, type Ref as RefType } from "@praxisjs/decorators";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class LensStyles extends Stylesheet {  $container = this.css({ position: "relative", overflow: "hidden" });  $image = this.css({ display: "block", width: "100%", height: "100%", objectFit: "cover" });  $glass = this.css({    pointerEvents: "none",    position: "absolute",    borderRadius: "9999px",    border: `2px solid ${t.background}`,    backgroundRepeat: "no-repeat",    opacity: 0,    boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",  }).on("&[data-visible]", { opacity: 1 });}export interface LensProps {  src: string;  alt?: string;  zoom?: number;  lensSize?: number;  class?: string;}// Lens position/zoom recomputed on every pointer move and written to `style` directly (not// `@State`) since the container can be responsive.@Component()export class Lens extends StatefulComponent {  @Styled(LensStyles) $s!: LensStyles;  @Prop() src!: string;  @Prop() alt = "";  @Prop() zoom = 2;  @Prop() lensSize = 150;  @Prop() class?: string;  @Ref<HTMLDivElement>()  containerRef!: RefType<HTMLDivElement>;  @Ref<HTMLDivElement>()  lensRef!: RefType<HTMLDivElement>;  @State() _visible = false;  private readonly _handlePointerMove = (event: PointerEvent) => {    const container = this.containerRef.current;    const lens = this.lensRef.current;    if (!container || !lens) return;    const rect = container.getBoundingClientRect();    const x = event.clientX - rect.left;    const y = event.clientY - rect.top;    lens.style.left = `${String(x - this.lensSize / 2)}px`;    lens.style.top = `${String(y - this.lensSize / 2)}px`;    lens.style.backgroundSize = `${String(rect.width * this.zoom)}px ${String(rect.height * this.zoom)}px`;    lens.style.backgroundPosition = `${String(-(x * this.zoom - this.lensSize / 2))}px ${String(-(y * this.zoom - this.lensSize / 2))}px`;  };  render() {    return (      <div        ref={this.containerRef}        data-slot="lens"        class={cx(this.$s.$container, this.class)}        onPointerEnter={() => { this._visible = true; }}        onPointerLeave={() => { this._visible = false; }}        onPointerMove={this._handlePointerMove}      >        <img src={this.src} alt={this.alt} class={this.$s.$image} />        <div          ref={this.lensRef}          data-slot="lens-glass"          data-visible={() => (this._visible ? "" : undefined)}          class={this.$s.$glass}          style={{ width: `${String(this.lensSize)}px`, height: `${String(this.lensSize)}px`, backgroundImage: `url(${this.src})` }}        />      </div>    );  }}

Examples

About

Purely presentational — no Morphos equivalent. The lens is the same image as a background-image, sized to containerSize * zoom and offset so the point under the cursor stays centered under it — recomputed on every pointer move since the container can be responsive (written imperatively rather than through @State, same reasoning as Resizable).

Usage

import { Lens } from "@/components/ui/lens";

<Lens class="h-64 w-full rounded-lg" src="/product-photo.jpg" alt="Product" zoom={2} lensSize={150} />

Props

PropTypeDefault
srcstring
altstring""
zoomnumber2
lensSizenumber (px diameter)150
classstring

On this page