A responsive image in an optional fixed-ratio frame, with a loading placeholder, a failure fallback, hover zoom, parallax and scroll reveal.
<tp-image>
Image
Default density
Compact density
Restart from the initial state
Show the copyable source
<tp-image
ratio="1.7777777778"
zoom="in"
style="max-inline-size:40rem;margin-inline:auto;border-radius:var(--tp-radius-xl)"
src="https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=1200&q=75&auto=format&fit=crop"
srcset="https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=640&q=75&auto=format&fit=crop 640w, https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=1200&q=75&auto=format&fit=crop 1200w"
alt="A bright open-plan office with long shared desks"
></tp-image>
Usage
The browser still picks and loads the file: Image passes src, srcset, sizes and <source> children to a native <picture>, so lazy loading and responsive candidates work as usual. With a lazy srcset of width descriptors and no sizes, it sets sizes="auto, 100vw" so the browser fetches the laid-out width. Set alt="" for a decorative image.
An explicit sizes is passed through unchanged. An eager image without sizes keeps the browser default (100vw).
Ratio and fit
ratio (width ÷ height) reserves a fixed frame before the image loads, so nothing shifts. fit decides how the picture fills it, as object-fit does. Round the frame with border-radius on the host.
Setting an invalid ratio throws RangeError and keeps the previous value. Without ratio, the image keeps its own proportions. Give width and height (or set them on the selected <source>) so the space is reserved before the image loads. When the host has an explicit block size, fit applies within it.
Ratio and fit
Default density
Compact density
Restart from the initial state
Show the copyable source
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:var(--tp-space-3);max-inline-size:40rem;margin-inline:auto;inline-size:100%">
<tp-image ratio="1" style="border-radius:var(--tp-radius-lg)" src="https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=480&q=75&auto=format&fit=crop" alt="Desks by a window"></tp-image>
<tp-image ratio="0.75" style="border-radius:var(--tp-radius-lg)" src="https://images.unsplash.com/photo-1494337480532-3725c85fd2ab?w=480&q=75&auto=format&fit=crop" alt="A building facade"></tp-image>
<tp-image ratio="1" fit="contain" style="border-radius:var(--tp-radius-lg);background:var(--tp-muted)" src="https://images.unsplash.com/photo-1548516173-3cabfa4607e9?w=480&q=75&auto=format&fit=crop" alt="A rooftop terrace"></tp-image>
</div>
Placeholder and fallback
While loading, Image shows a pulsing skeleton; placeholder="spinner" adds a spinner and none shows nothing. If the image fails or has no source, it shows a muted surface with a broken-image icon, or your fallback slot content, which stays exposed to assistive technology.
Once loaded, the image fades in. A cached image appears without a loading phase or fade. The image stays hidden until it loads, so a broken-image glyph never appears.
The placeholder and the default fallback are hidden from assistive technology and never announce anything, so a gallery doesn’t create dozens of live regions. The frame reports aria-busy while loading.
Image paints no surface of its own. The placeholder and fallback surfaces are the composed Skeleton, so theme tokens such as --tp-muted apply.
Fallbacks
Default density
Compact density
Restart from the initial state
Show the copyable source
zoom="in" scales the picture up while the pointer hovers, and zoom="out" settles it from enlarged to natural size. The frame never changes size. Set zoomed to drive the effect from a surrounding card instead.
Hover zoom applies only to devices that can hover, and it never changes layout or the clipping frame. Tune it with --tp-image-zoom-duration and --tp-image-zoom-easing.
Hover zoom
Default density
Compact density
Restart from the initial state
Show the copyable source
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:var(--tp-space-3);max-inline-size:32rem;margin-inline:auto;inline-size:100%">
<tp-image ratio="1" zoom="in" style="border-radius:var(--tp-radius-lg)" src="https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=640&q=75&auto=format&fit=crop" alt="Desks by a window"></tp-image>
<tp-image ratio="1" zoom="out" style="border-radius:var(--tp-radius-lg)" src="https://images.unsplash.com/photo-1465869185982-5a1a7522cbcb?w=640&q=75&auto=format&fit=crop" alt="A bicycle in a wet alley"></tp-image>
</div>
Here the whole card drives the zoom, so the photo responds wherever the pointer enters the card.
Card hover
Default density
Compact density
Restart from the initial state
Show the copyable source
reveal animates an image into place the first time it scrolls into view, from tokens such as fade up or zoom-in. Wrap a set in tp-image-group to wait until every image has loaded, then reveal them in order, stagger milliseconds apart. A reveal that would start during a same-document view transition waits for it to finish.
The group starts fetching every member once it is within 25% of the viewport, so lazy members don’t hold the set back. It reveals nothing until every member has settled (loaded and decoded, or failed and showing its fallback). Members that have no reveal of their own use the group’s reveal. Inside a group, members never trigger their own reveal; the group owns their timing. Lay the group out like any block, for example as a grid.
Members’ events bubble through the group with the same names, so check event.target === group to handle only the group’s.
Staggered group
Default density
Compact density
Restart from the initial state
Show the copyable source
Images outside a group reveal on their own as each loads. Tune them with --tp-image-reveal-duration, --tp-image-reveal-easing and --tp-image-reveal-delay, set on a container to apply to every image inside; a growing delay staggers a grid. Add reveal-repeat to replay the reveal each time an image scrolls back into view. Scroll the box to see it.
Reveal timing and repeat
Default density
Compact density
Restart from the initial state
Show the copyable source
The reveal never starts before the image is ready. An image that enters while still loading stays in its start state until it has loaded and decoded, then reveals if it is still in view. An image that fails to load reveals its fallback, and one with no source reveals at once. Add reveal-repeat to play it again on every entry: once the image is fully out of view, it returns to its start state instantly, out of sight, and reveals again when it is back at least 10% inside the viewport. That margin stops it flickering at an edge.
To sequence images with surrounding text, wrap them in a Scroll trigger. It waits for the group’s images to load, then reveals the heading, the images and the caption in order.
Headline, group and caption
Default density
Compact density
Restart from the initial state
Show the copyable source
Spaces that work the way you do
Open desks, quiet rooms and a terrace, all bookable from the app.
Spaces that work the way you do
Open desks, quiet rooms and a terrace, all bookable from the app.
<tp-scroll-trigger stagger="200" style="display:grid;gap:var(--tp-space-4);max-inline-size:40rem;margin-inline:auto;inline-size:100%">
<h3 style="margin:0"><tp-mo-text-split split="words lines" mask="lines" reveal="up">Spaces that work the way you do</tp-mo-text-split></h3>
<tp-image-group reveal="fade up" stagger="120" style="display:grid;grid-template-columns:repeat(3,1fr);gap:var(--tp-space-3)">
<tp-image ratio="1" style="border-radius:var(--tp-radius-lg)" src="https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=400&q=75&auto=format&fit=crop" alt=""></tp-image>
<tp-image ratio="1" style="border-radius:var(--tp-radius-lg)" src="https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=400&q=75&auto=format&fit=crop" alt=""></tp-image>
<tp-image ratio="1" style="border-radius:var(--tp-radius-lg)" src="https://images.unsplash.com/photo-1548516173-3cabfa4607e9?w=400&q=75&auto=format&fit=crop" alt=""></tp-image>
</tp-image-group>
<p style="margin:0;color:var(--tp-muted-foreground)"><tp-mo-text-split reveal="fade">Open desks, quiet rooms and a terrace, all bookable from the app.</tp-mo-text-split></p>
</tp-scroll-trigger>
An image belongs to its nearest group or trigger, and a group inside a trigger or another group is one member of it, playing its own stagger after the delay it receives. Images added later join the set and reveal as soon as they settle. For other motion, hold the group, wait for what you need, then clear reveal-hold. Inside a scrubbing trigger the scroll position drives the image’s reveal (delay, duration and easing included) instead of time.
Under reduced motion, from motion-policy="reduce" or the system preference, images appear already revealed and zoom is instant.
Parallax
parallax moves the picture inside its frame as the page, or the nearest scrolling container, scrolls past. The image is enlarged by parallax-depth so the frame edges never show; add zoom-in or zoom-out to scale it as it travels.
Clipping wrappers that don’t scroll, such as a card with overflow: hidden, are skipped. Directions are physical and don’t flip for right-to-left text, and any direction works with either axis; left and right suit a horizontal strip. Parallax only moves while the container actually scrolls past the image. An image is placed shortly before it scrolls into view, so it never jumps when it appears.
Parallax in a scroller
Default density
Compact density
Restart from the initial state
Show the copyable source
Scroll zoom scales the image instead of moving it: zoom-in grows it as it passes through the box, and zoom-out settles it from enlarged to natural size. Combine zoom with a direction, such as up zoom-in, to zoom and drift at once.
Scroll zoom in a vertical scroller
Default density
Compact density
Restart from the initial state
Show the copyable source
parallax-smoothing, from 0 to 1, makes the picture trail the scroll and ease into place after it stops. In this scroller the first image is locked to the scroll and each following one is smoother.
Smoothing
Default density
Compact density
Restart from the initial state
Show the copyable source
The value is the share of the remaining distance still left after one 60 fps frame: 0.5 trails slightly, 0.9 glides slowly. It is clamped to 0.98, and it behaves the same at any frame rate.
Art direction
<source> children give the native picture other candidates, such as a tighter crop for narrow screens through media. For a hero that is the largest content on first paint, set loading="eager" and fetchpriority="high".
Use media to switch crops between layouts and type for AVIF or WebP with a fallback. The authored <source> elements never request anything themselves. Adding, removing or editing a source reloads the image.
When the browser switches candidates after loading, for example when you resize across a media breakpoint, currentSrc updates and the status stays loaded.
Hero with a narrow-screen crop
Default density
Compact density
Restart from the initial state
Show the copyable source
<tp-image
ratio="2.4"
parallax="up"
parallax-depth="0.3"
loading="eager"
fetchpriority="high"
style="border-radius:var(--tp-radius-xl)"
src="https://images.unsplash.com/photo-1465869185982-5a1a7522cbcb?w=1600&q=75&auto=format&fit=crop"
srcset="https://images.unsplash.com/photo-1465869185982-5a1a7522cbcb?w=900&q=75&auto=format&fit=crop 900w, https://images.unsplash.com/photo-1465869185982-5a1a7522cbcb?w=1600&q=75&auto=format&fit=crop 1600w"
sizes="100vw"
alt="A bicycle leaning on a wall in a wet alley"
>
<source media="(max-width: 600px)" srcset="https://images.unsplash.com/photo-1465869185982-5a1a7522cbcb?w=600&h=450&q=75&auto=format&fit=crop&crop=entropy 600w, https://images.unsplash.com/photo-1465869185982-5a1a7522cbcb?w=1000&h=750&q=75&auto=format&fit=crop&crop=entropy 1000w" sizes="100vw">
</tp-image>
Performance
Image is built for pages with hundreds of images. Placeholders that are off screen pause their animation, and no will-change is applied, so idle images create no compositing layers.
API Reference
Every element also accepts motion-policy.
Attributes
AttributePropertyTypeDefaultDescriptionsrcsrcstring''The image URL.altaltstring''What the image shows, for assistive technology; empty marks it decorative.srcsetsrcSetstring''Responsive image candidates; with width descriptors and lazy loading, sizes defaults to the rendered width.sizessizesstring''The image's display sizes for choosing among srcset candidates.crossorigincrossOrigin'' | 'anonymous' | 'use-credentials'''The image request's CORS mode.referrerpolicyreferrerPolicyReferrerPolicy''The referrer policy of the image request.fetchpriorityfetchPriority'' | 'high' | 'low' | 'auto'''A hint for how urgently to fetch the image: high for the largest image above the fold.widthwidthnumber | undefined—The intrinsic width in pixels, which reserves space before the image loads.heightheightnumber | undefined—The intrinsic height in pixels, which reserves space before the image loads.loadingloading'lazy' | 'eager''lazy'Defers loading until the image nears the viewport; eager for images visible on load.ratiorationumber | undefined—Width divided by height; unset uses the image's intrinsic geometry.fitfitImageFit'cover'How the image fills its frame: cropped, letterboxed, stretched or at its own size.placeholderplaceholderImagePlaceholder'skeleton'The surface shown while the image loads.zoomzoomImageZoom'none'Scales the image in or out on hover, inside its frame.zoomedzoomedbooleanfalseApplies the zoom without hover, for example while an enclosing card is hovered.parallaxparallaxImageParallax'none'Scroll-linked effects: one direction the image travels, a scroll zoom, or both, such as up zoom-in.parallax-depthparallaxDepthnumber0.3How strongly the parallax image is enlarged and travels, from 0 to 1.parallax-smoothingparallaxSmoothingnumber0How far parallax and scroll zoom trail the scroll, from 0 (locked to it) toward 1.revealrevealstring''Effects played as the image first enters the viewport: any of fade, up, down, left, right, zoom-in and zoom-out.reveal-repeatrevealRepeatbooleanfalseReveal again on every viewport entry instead of only the first.reveal-holdrevealHoldbooleanfalseWhile set, a ready reveal waits in its start state; clearing it plays the reveal.
Properties
PropertyTypeDefaultDescriptionimageLoadingStatusread-onlyImageLoadingStatus—Loading status of the current source.currentSrcread-onlystring—The candidate the browser selected for the current source.revealedread-onlyboolean—Whether the reveal has played (false again after a repeat reset).
Events
EventTypeDescriptiontp-loading-status-changeCustomEvent<{ status: ImageLoadStatus; }>The current source's loading status changed; detail.status is the new status.tp-reveal-changeCustomEvent<{ revealed: boolean; effect: string; }>The reveal started, or reset for a repeat; detail.revealed and the detail.effect played.tp-reveal-change-completeCustomEvent<{ revealed: boolean; }>The reveal finished settling; detail.revealed.
Slots
SlotDescriptiondefault<source> children (media, type, srcset, sizes, width, height), in priority order.placeholderReplaces the default loading placeholder.fallbackReplaces the default failure fallback.
CSS parts
PartDescription::part(frame)The clipping frame (Aspect-ratio box when ratio is set).::part(media)The layer that zoom and parallax transform.::part(picture)The native image.::part(placeholder)The loading surface.::part(fallback)The failure surface.
CSS custom properties
PropertyDefaultDescription--tp-image-position—Object position of the image. Default 50% 50%.--tp-image-zoom-scale—Hover zoom scale. Default 1.1.--tp-image-reveal-distance—Reveal travel. Default var(--tp-space-6).--tp-image-reveal-scale—Reveal scale offset for zoom-in/zoom-out. Default 0.08.--tp-image-reveal-delay—Reveal delay, for staggering. Default 0s.--tp-image-reveal-duration—Reveal duration. Default var(--tp-duration-slow).--tp-image-reveal-easing—Reveal easing. Default var(--tp-easing-enter).--tp-image-zoom-duration—Hover zoom duration. Default var(--tp-duration-slow).--tp-image-zoom-easing—Hover zoom easing. Default var(--tp-easing-enter).
<tp-image-group>
Attributes
AttributePropertyTypeDefaultDescriptionstaggerstaggernumber0Milliseconds between consecutive member reveals, in document order; 0 reveals in sync.stagger-fromstaggerFromStaggerFrom'first'The member the stagger starts from.revealrevealstring''Default reveal effect for members without their own.reveal-repeatrevealRepeatbooleanfalseReplay the group reveal each time the group re-enters view.reveal-holdrevealHoldbooleanfalseWhile set (or while any member holds), the group waits in its start state.
Properties
PropertyTypeDefaultDescriptionloadingStatusread-onlyImageGroupLoadingStatus—Aggregate loading status of the members.imagesread-onlyHTMLElement[]—The member images, in document order.revealedread-onlyboolean—Whether the group sequence has played (false again after a repeat reset).
Events
EventTypeDescriptiontp-loading-status-changeCustomEvent<{ status: string; loaded: number; failed: number; total: number; }>{ status, loaded, failed, total } when the aggregate status changes.tp-reveal-changeCustomEvent<{ revealed: boolean; }>{ revealed } when the group sequence starts or resets.tp-reveal-change-completeCustomEvent<{ revealed: boolean; }>{ revealed } after the last member's reveal settles.
Slots
SlotDescriptiondefaultContent, including the member Images at any depth.
Related
Components, widgets and patterns that use this one.