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.
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.
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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | |
disableAnimation | boolean | - | |
aspectRatio | AspectRatio (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) | - | |
parallax | boolean | - | |
parallaxNoWrapper | boolean | - | |
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.
| Class | Styles |
|---|---|
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,parallaxandparallaxNoWrapper. sizesdefaults 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..gifand.svgsources skip the optimizer automatically. Setunoptimizedyourself only for other formats that must bypass it.fillis turned off whileparallaxis on.loadingdefaults to'lazy', and is dropped when you passpriority, becausenext/imagerejects both together. Passpriorityfor 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.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | |
disableAnimation | boolean | - | |
aspectRatio | AspectRatio (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) | - | |
parallax | boolean | - | |
parallaxNoWrapper | boolean | - | |
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. Leavealtempty (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.