Docs

This page isn't translated yet

next

Image

Images with aspect ratio, object fit and parallax.

On this page

Image renders a responsive image cropped to one of the Systhema aspect ratios, with the media border, radius and shadow from the tokens. It can also move against the scroll as a parallax layer. In @systhemaui/next it wraps next/image. Video and MediaWrapper share the same media props.

import { Image } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="w-full max-w-3xl">
      <Image
        src="/demo-assets/mountains.webp"
        alt="Pastel mountain ridges under a pale sky"
        width={1920}
        height={1080}
        aspectRatio="16/9"
      />
    </div>
  )
}

ImportLink to this section

import { Image } from '@systhemaui/next'

In a React app without Next.js, import it from @systhemaui/react. There it renders a plain <img> with loading="lazy" and decoding="async".

ExamplesLink to this section

Aspect ratioLink to this section

aspectRatio crops the image to a fixed ratio with an aspect-* class. It defaults to 'auto', the image's own ratio. The full list runs from 32/9 to 9/21, plus the golden ratios 1.618/1 and 1/1.618.

import { Image, Paragraph } from '@systhemaui/next'

const ratios = ['1/1', '4/3', '16/9', '21/9'] as const

export default function Demo() {
  return (
    <div className="grid w-full max-w-3xl grid-cols-2 gap-6 md:grid-cols-4">
      {ratios.map((ratio) => (
        <div key={ratio} className="flex flex-col gap-2">
          <Image
            src="/demo-assets/coast.webp"
            alt=""
            width={1800}
            height={1200}
            aspectRatio={ratio}
          />
          <Paragraph type="small">{ratio}</Paragraph>
        </div>
      ))}
    </div>
  )
}

21/9-md is the one responsive value: 4/3 on small screens and 21/9 from the md breakpoint up. Switch the preview between phone and desktop to see it change.

Responsive 21/9-md
import { Image } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="w-full max-w-3xl">
      <Image
        src="/demo-assets/city.webp"
        alt="A city skyline at night"
        width={1920}
        height={1080}
        aspectRatio="21/9-md"
      />
    </div>
  )
}

Object fitLink to this section

objectFit decides how the image fills its box: 'cover' (the default) crops to fill it, 'contain' shows the whole image and leaves space around it. Here a portrait image sits in square boxes.

cover and contain
import { Image, Paragraph } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="grid w-full max-w-xl grid-cols-2 gap-6">
      <div className="flex flex-col gap-2">
        <Image
          src="/demo-assets/dunes-portrait.webp"
          alt=""
          width={1200}
          height={1600}
          aspectRatio="1/1"
          objectFit="cover"
        />
        <Paragraph type="small">cover</Paragraph>
      </div>
      <div className="flex flex-col gap-2">
        <Image
          src="/demo-assets/dunes-portrait.webp"
          alt=""
          width={1200}
          height={1600}
          aspectRatio="1/1"
          objectFit="contain"
        />
        <Paragraph type="small">contain</Paragraph>
      </div>
    </div>
  )
}

Object positionLink to this section

With cover, objectPosition picks which part of the image stays in the crop. It takes a keyword ('top', 'bottom-left', 'center' and so on, default 'center') or an { x, y } pair, which is the shape the Payload image fields store.

Object position
import { Image, Paragraph } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="grid w-full max-w-3xl grid-cols-3 gap-6">
      <div className="flex flex-col gap-2">
        <Image src="/demo-assets/forest-portrait.webp" alt="" width={1200} height={1600} aspectRatio="16/9" objectPosition="top" />
        <Paragraph type="small">top</Paragraph>
      </div>
      <div className="flex flex-col gap-2">
        <Image src="/demo-assets/forest-portrait.webp" alt="" width={1200} height={1600} aspectRatio="16/9" />
        <Paragraph type="small">center</Paragraph>
      </div>
      <div className="flex flex-col gap-2">
        <Image
          src="/demo-assets/forest-portrait.webp"
          alt=""
          width={1200}
          height={1600}
          aspectRatio="16/9"
          objectPosition={{ x: 'center', y: 'bottom' }}
        />
        <Paragraph type="small">{'{ x: center, y: bottom }'}</Paragraph>
      </div>
    </div>
  )
}

ParallaxLink to this section

parallax wraps the image in a <figure class="media-wrapper parallax-wrapper"> that carries the aspect ratio, and adds the parallax class to the image. The ParallaxListener then moves the image inside the wrapper as the page scrolls. With parallax, className and the scroll-reveal classes go on the wrapper.

import { Image } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="w-full max-w-3xl">
      <Image
        src="/demo-assets/mountains.webp"
        alt="Pastel mountain ridges under a pale sky"
        width={1920}
        height={1080}
        aspectRatio="21/9"
        parallax
      />
    </div>
  )
}

The preview sizes itself to its content, so it does not scroll and the image stays at its resting position. On a page, the image moves as it passes through the viewport. Under prefers-reduced-motion: reduce it stays still. The travel range and scale are set with the parallax config (see Parallax).

parallaxNoWrapper adds the parallax class without the wrapper, for an image you place in your own overflow: hidden container.

PropsLink to this section

Image takes every <img> attribute (in Next.js, every next/image prop) on top of the media props below. width and height are not set on the element while parallax is on, since the wrapper decides the size. disableAnimation drops the configured animationClasses.

PropTypeDefaultDescription
classNamestring-
disableAnimationboolean-
aspectRatioAspectRatio (auto, 32/9, 21/9, 21/9-md, 14/3, 5/2, 3/1, 4/1, 2/1, 16/5, 8/3, 16/9, 16/10, 8/5, 5/4, 4/3, 3/2, 1.618/1, 1/1, 9/8, 2/3, 3/4, 4/5, 6/7, 9/16, 10/16, 1/2, 9/21, 1/1.618)-
parallaxboolean-
parallaxNoWrapperboolean-
objectFit'contain' | 'cover' | null-
objectPosition'top-left' | 'top' | 'top-center' | 'top-right' | 'center-left' | 'left' | 'center-center' | 'center' | 'center-right' | 'right' | 'bottom-left' | 'bottom' | 'bottom-center' | 'bottom-right' | { x?, y? } | null-
…and all <img> attributes

HTML and CSSLink to this section

Without React, combine media with an aspect-ratio and an object-fit class:

<img
  class="media aspect-16/9 object-cover object-center"
  src="/mountains.webp"
  alt="Pastel mountain ridges under a pale sky"
  width="1920"
  height="1080"
  loading="lazy"
  decoding="async"
/>

<figure class="media-wrapper parallax-wrapper aspect-21/9">
  <img class="media parallax object-cover" src="/mountains.webp" alt="" />
</figure>

The aspect classes are listed on Aspect ratio, and the parallax classes on Parallax. Without React, initParallax() from the vanilla JS bundle runs the parallax engine.

ClassStyles
media.hero-simple-media:is(.media) { max-width: unset; left: 50%; translate: -50% 0; width: var(--hero-simple-media-width, 100%); }.hero-background-media:is(.media) { z-index: 5; position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; border-radius: 0px; box-shadow: none; }.hero-background-media:is(.media) > * { width: 100%; height: 100%; border-radius: 0px; box-shadow: none; }+ 11 more rules
media-wrapper.hero-simple-media:is(.media-wrapper) { max-width: unset; left: 50%; translate: -50% 0; width: var(--hero-simple-media-width, 100%); }.hero-background-media:is(.media-wrapper) { z-index: 5; position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; border-radius: 0px; box-shadow: none; }.hero-background-media:is(.media-wrapper) > * { width: 100%; height: 100%; border-radius: 0px; box-shadow: none; }+ 12 more rules
media-overlay& { z-index: 1; position: absolute; inset: 0; opacity: 1; pointer-events: none; transition-property: opacity; transition-duration: 500ms; transition-timing-function: ease-out; color: var(--color-media-icon); }.media:has(video) .media-overlay { pointer-events: auto; }.media-wrapper:has(video) .media-overlay { pointer-events: auto; }[data-playing="true"] .media-overlay { opacity: 0; pointer-events: none; }+ 7 more rules
media-overlay-icon& { position: absolute; top: 50%; left: 50%; translate: -50% -50%; color: var(--color-media-icon); box-shadow: inset 0 0 0 100vmax var(--color-media-overlay), 0 0 0 100vmax var(--color-media-overlay); width: var(--media-icon-size); height: var(--media-icon-size); font-size: var(--media-icon-size); line-height: var(--media-icon-size); }+ 1 more rule

Next.jsLink to this section

The @systhemaui/next Image renders next/image, so images are resized, served in modern formats and lazy-loaded. It takes next/image props instead of <img> props and adds:

  • The media props above: aspectRatio, objectFit, objectPosition, parallax and parallaxNoWrapper.
  • sizes defaults to '100vw'. Pass the real display width (sizes="(min-width: 768px) 50vw, 100vw") for images narrower than the viewport, so the browser picks a smaller file.
  • .gif and .svg sources skip the optimizer automatically. Set unoptimized yourself only for other formats that must bypass it.
  • fill is turned off while parallax is on.
  • loading defaults to 'lazy', and is dropped when you pass priority, because next/image rejects both together. Pass priority for the largest image above the fold.
import { Image } from '@systhemaui/next'

export function HeroImage() {
  return (
    <Image
      src="/images/hero.webp"
      alt="Our studio in Ghent"
      width={1920}
      height={1080}
      aspectRatio="21/9"
      sizes="100vw"
      priority
    />
  )
}

Pass width and height (the source's intrinsic size, which sets the aspect ratio before the file loads) or fill.

PropTypeDefaultDescription
classNamestring-
disableAnimationboolean-
aspectRatioAspectRatio (auto, 32/9, 21/9, 21/9-md, 14/3, 5/2, 3/1, 4/1, 2/1, 16/5, 8/3, 16/9, 16/10, 8/5, 5/4, 4/3, 3/2, 1.618/1, 1/1, 9/8, 2/3, 3/4, 4/5, 6/7, 9/16, 10/16, 1/2, 9/21, 1/1.618)-
parallaxboolean-
parallaxNoWrapperboolean-
objectFit'contain' | 'cover'-
objectPosition'top-left' | 'top' | 'top-center' | 'top-right' | 'center-left' | 'left' | 'center-center' | 'center' | 'center-right' | 'right' | 'bottom-left' | 'bottom' | 'bottom-center' | 'bottom-right' | { x?, y? } | null-
…and props from next
…and all inherited HTML attributes

AccessibilityLink to this section

  • Describe what the image shows in alt. Leave alt empty (alt="", the default) for a decorative image or one whose content is already in the text next to it.
  • Parallax motion stops under prefers-reduced-motion: reduce, and the image rests filling its wrapper.

See Reduced motion.