Skip to content
Twilight React Playground
ثيم رائدaren

Image

componentBeginnerserverbrowserlive demo

An img that loads lazily by default and asks the Salla CDN for a resized copy, with optional srcset, aspect ratio and mobile source.

import { Image } from '@salla.sa/twilight-theme-engine/components/common';

In plain words

A store page is full of pictures, and each one costs download time. Image renders a normal <img> tag that behaves well by default: the browser loads it only when the shopper scrolls near it, and for photos on Salla's CDN it asks for a copy at the size you display (width={300} downloads a 300-pixel-wide file, not the 2000-pixel original).

Give priority to the one large image at the top of a page, so it loads first instead of waiting.

Signature

const Image: MemoExoticComponent<(props: ImageProps) => JSX.Element | null>

// ImageProps is not exported. Every <img> attribute, plus:
interface ImageProps extends ImgHTMLAttributes<HTMLImageElement> {
  src?: string;                      // empty: renders nothing
  alt: string;                       // required
  priority?: boolean;                // default false
  aspectRatio?: string;              // '1/1', '16/9'…: adds a wrapper <div>
  objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';  // default 'cover'
  mobileSrc?: string;                // adds <picture> with a mobile <source>
  mobileBreakpoint?: number;         // default 768
  noWrapper?: boolean;               // default false
  srcSetWidths?: readonly number[];  // CDN widths for srcset
}

Try it live

Image with a real photo from the demo store (on the Salla CDN) in a 240px dashed frame. Below it, the markup Image rendered.Try this: change width and height and watch the cdn-cgi/image URL follow. Pick srcSetWidths: a srcset appears and the height leaves the URL. Then set aspectRatio 16/9 and turn off className="h-full w-full" to see the photo cropped instead of fitted.
Storefront canvas · ar · RTL

Loading a product from the demo store…

Controls
0 leaves width out.
0 leaves height out.
src keeps your width only if it is one of these; otherwise it uses the first.
Visible once the image fills a box with a different shape.
className="h-full w-full"Makes the img fill the aspectRatio box.
noWrapperDrops the aspectRatio wrapper (and style).
priorityFor the one image at the top of the page.
mobileSrc (the store logo)
What a theme writes
import { Image } from '@salla.sa/twilight-theme-engine/components/common';

export function Photo({ src, alt }: { src?: string; alt: string }) {
  return (
    <div style={{ width: 240 }}>
      <Image
        src={src}
        alt={alt}
        className="h-full w-full"
        width={240}
      />
    </div>
  );
}

Example

app/components/ProductThumb.tsx
import { Image } from '@salla.sa/twilight-theme-engine/components/common';
import type { Product } from '@salla.sa/twilight-theme-engine/types';

export function ProductThumb({ product }: { product: Product }) {
  return (
    <Image
      src={product.image?.url}
      alt={product.image?.alt ?? product.name}
      className="h-full w-full"
      width={300}
      srcSetWidths={[150, 300, 600]}
      sizes="(max-width: 640px) 50vw, 300px"
      aspectRatio="1/1"
    />
  );
}

How it behaves

  • Loading: loading="lazy" and decoding="async" by default. With priority: loading="eager", fetchPriority="high" and decoding="sync". Passing any of these props yourself overrides them.

  • Resizing applies only to URLs on cdn.salla.sa, cdn.salla.network and cdn.files.salla.network: src becomes https://<host>/cdn-cgi/image/fit=scale-down,onerror=redirect,format=auto,width=W,height=H/<path>, with any transform already in the URL removed first. fit=scale-down never enlarges an image.

  • srcSetWidths writes a srcset of <url> <width>w candidates; pass sizes too. src then carries a width only (no height): your width if it is one of the candidates, otherwise the first. An explicit srcSet prop wins over the generated one.

  • mobileSrc wraps the image in <picture> with a <source media="(max-width: 768px)"> (see mobileBreakpoint), resized the same way.

  • It always adds the Tailwind class object-<objectFit> (object-cover by default) to the <img>.

  • It is memoised (React.memo) and eager, so it needs no <Suspense>. The engine uses it for the header logo, product cards (srcSetWidths with aspectRatio="1/1"), the product gallery and cart items.

  • The URL helpers behind it are getCdnImageUrl and getCdnImageSrcSet from @salla.sa/twilight-theme-engine/utils/cdn-image, for places that need a URL rather than an element.

Gotchas

  • style goes to the wrapper <div>, which exists only with aspectRatio and without noWrapper. Without the wrapper the style prop is dropped entirely: Image.tsx takes it out of the props and never passes it to <img>. Style the image with className, or wrap it yourself.

  • aspectRatio shapes the wrapper, not the picture. The <img> keeps its own height and the wrapper (overflow-hidden) crops it, so objectFit has nothing to fit. Add className="h-full w-full" so the image fills the box, as the example does.

  • For images on any other host (your theme's /assets/…, a third-party URL) width, height and srcSetWidths change only the markup: the full-size file still downloads, and no srcset is written.

  • An empty or missing src renders nothing, not an empty box. Reserve the space around it yourself, or the layout jumps when the URL arrives.

Related

Source and docs