Image β
This library is a work-in-progress. We are releasing it early to gather feedback, but it is not ready for production.
The AgnosticUI Image component is a powerful, performant replacement for the native <img> tag. It intelligently adapts to your needs: provide a simple src to render an <img>, or add a sources prop to automatically generate a responsive <picture> element.
Beyond its flexibility, it provides built-in solutions for common image-handling challenges, including responsive sizing, preventing layout shift, lazy loading, and graceful error handling.
Examples β
Live Preview
Basic Image
A standard image with a `src` and `alt` tag. This example also has the fade prop enabled. Click the button to reload the image and see the effect.
Responsive Image (No Layout Shift)
By omitting `width` and `height` props and providing `aspectRatio`, the image becomes responsive while preventing content layout shift. Fade is also enabled here.
Object Fit: Contain
Use `fit="contain"` to ensure the entire image is visible within its container without being cropped.
Responsive Sources with <picture>
Provide different images for different screen sizes using the `sources` prop. Resize your browser to see the image change.
Fallback Source
If the primary `src` is broken, the `fallbackSrc` will be loaded instead. The fallback will also fade in.
Custom Placeholder Slot
Provide custom content to be displayed while the image is loading.
Custom Error Slot
Display a custom message or UI when an image fails to load.
Oops! The image could not be loaded.
Styled with CSS Class and Parts
Use a CSS class to apply custom styles to the component, including its internal parts like `::part(ag-img)`.
View Vue Code
<script setup lang="ts">
import { ref } from "vue";
import VueImage from "agnosticui-core/image/vue";
import { VueIcon } from "agnosticui-core/icon/vue";
import { TriangleAlert } from "lucide-vue-next";
// Basic Image
const basicImageUrl =
"https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?w=545&h=487&fit=crop&q=80";
const basicImageSrc = ref(basicImageUrl);
const reloadBasicImage = () => {
basicImageSrc.value = `${basicImageUrl}&t=${new Date().getTime()}`;
};
// Responsive Image
const responsiveImageUrl =
"https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1200&h=800&fit=crop&q=80";
const responsiveImageSrc = ref(responsiveImageUrl);
const reloadResponsiveImage = () => {
responsiveImageSrc.value = `${responsiveImageUrl}&t=${new Date().getTime()}`;
};
// Object Fit Image
const objectFitImageUrl =
"https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=545&h=487&fit=crop&q=80";
const objectFitImageSrc = ref(objectFitImageUrl);
const reloadObjectFitImage = () => {
objectFitImageSrc.value = `${objectFitImageUrl}&t=${new Date().getTime()}`;
};
// Responsive Sources Image
const responsiveSourcesImageUrl =
"https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?w=545&h=487&fit=crop&q=80";
const responsiveSourcesImageSrc = ref(responsiveSourcesImageUrl);
const reloadResponsiveSourcesImage = () => {
responsiveSourcesImageSrc.value = `${responsiveSourcesImageUrl}&t=${new Date().getTime()}`;
};
const responsiveSources = [
{
srcset:
"https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?w=1400&h=787&fit=crop&q=80",
media: "(min-width: 1024px)",
},
{
srcset:
"https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?w=1000&h=562&fit=crop&q=80",
media: "(min-width: 768px)",
},
];
// Fallback Image
const fallbackImageUrl = "https://thissourcedoesnotexist.com/image.jpg";
const fallbackImageSrc = ref(fallbackImageUrl);
const reloadFallbackImage = () => {
fallbackImageSrc.value = `${fallbackImageUrl}?t=${new Date().getTime()}`;
};
// Placeholder Image
const placeholderImageUrl =
"https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=545&h=487&fit=crop&q=80&auto=format&ps=50";
const placeholderImageSrc = ref(placeholderImageUrl);
const reloadPlaceholderImage = () => {
placeholderImageSrc.value = `${placeholderImageUrl}&t=${new Date().getTime()}`;
};
// Error Image
const errorImageUrl =
"https://thissourcedoesnotexist.com/another-broken-image.jpg";
const errorImageSrc = ref(errorImageUrl);
const reloadErrorImage = () => {
errorImageSrc.value = `${errorImageUrl}?t=${new Date().getTime()}`;
};
// Styled Image
const styledImageUrl =
"https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=545&h=487&fit=crop&q=80";
const styledImageSrc = ref(styledImageUrl);
const reloadStyledImage = () => {
styledImageSrc.value = `${styledImageUrl}&t=${new Date().getTime()}`;
};
</script>
<template>
<div class="image-examples-container">
<h2>Basic Image</h2>
<p>A standard image with a `src` and `alt` tag. This example also has the <code>fade</code> prop enabled. Click the button to reload the image and see the effect.</p>
<VueImage
:src="basicImageSrc"
alt="A beautiful landscape with a river and mountains."
:width="545"
:height="487"
:fade="true"
style="max-width: 100%; height: auto;"
/>
<button
@click="reloadBasicImage"
class="reload-button"
>Reload Image</button>
<hr />
<h2>Responsive Image (No Layout Shift)</h2>
<p>
By omitting `width` and `height` props and providing `aspectRatio`, the image becomes responsive while preventing content layout shift. Fade is also enabled here.
</p>
<div class="responsive-image-container">
<VueImage
:src="responsiveImageSrc"
alt="A forest path leading to a mountain."
aspectRatio="16/9"
:fade="true"
></VueImage>
</div>
<button
@click="reloadResponsiveImage"
class="reload-button"
>Reload Image</button>
<hr />
<h2>Object Fit: Contain</h2>
<p>Use `fit="contain"` to ensure the entire image is visible within its container without being cropped.</p>
<div class="contain-fit-container">
<VueImage
:src="objectFitImageSrc"
alt="A dense forest with sunlight filtering through."
aspectRatio="4/3"
fit="contain"
:fade="true"
></VueImage>
</div>
<button
@click="reloadObjectFitImage"
class="reload-button"
>Reload Image</button>
<hr />
<h2>Responsive Sources with <picture></h2>
<p>Provide different images for different screen sizes using the `sources` prop. Resize your browser to see the image change.</p>
<div class="responsive-image-container">
<VueImage
:src="responsiveSourcesImageSrc"
alt="A responsive landscape that changes with screen size."
aspectRatio="16/9"
:sources="responsiveSources"
:fade="true"
></VueImage>
</div>
<button
@click="reloadResponsiveSourcesImage"
class="reload-button"
>Reload Image</button>
<hr />
<h2>Fallback Source</h2>
<p>If the primary `src` is broken, the `fallbackSrc` will be loaded instead. The fallback will also fade in.</p>
<VueImage
:src="fallbackImageSrc"
fallbackSrc="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=545&h=487&fit=crop&q=80"
alt="An image with a fallback source."
:width="545"
:height="487"
:fade="true"
style="max-width: 100%; height: auto;"
/>
<button
@click="reloadFallbackImage"
class="reload-button"
>Reload Image</button>
<hr />
<h2>Custom Placeholder Slot</h2>
<p>Provide custom content to be displayed while the image is loading.</p>
<VueImage
:src="placeholderImageSrc"
alt="A nature scene with a custom placeholder."
:width="545"
:height="487"
style="max-width: 100%; height: auto;"
>
<div
slot="placeholder"
class="custom-placeholder"
>
<div class="placeholder-icon">π·</div>
<div>Loading beautiful scenery...</div>
</div>
</VueImage>
<button
@click="reloadPlaceholderImage"
class="reload-button"
>Reload Image</button>
<hr />
<h2>Custom Error Slot</h2>
<p>Display a custom message or UI when an image fails to load.</p>
<div class="responsive-image-container">
<VueImage
:src="errorImageSrc"
alt="An image with a custom error message."
aspectRatio="4/3"
style="width: 100%;"
>
<div
slot="error"
class="custom-error"
>
<div class="error-icon">
<VueIcon
size="64"
type="error"
:noFill="true"
>
<TriangleAlert />
</VueIcon>
</div>
<h2 style="margin-top: 0;">Oops! The image could not be loaded.</h2>
</div>
</VueImage>
</div>
<button
@click="reloadErrorImage"
class="reload-button"
>Reload Image</button>
<hr />
<h2>Styled with CSS Class and Parts</h2>
<p>Use a CSS class to apply custom styles to the component, including its internal parts like `::part(ag-img)`.</p>
<div class="responsive-image-container">
<VueImage
class="custom-styled-image"
:src="styledImageSrc"
alt="A landscape with a custom border style."
aspectRatio="4/3"
:fade="true"
/>
</div>
<button
@click="reloadStyledImage"
class="reload-button"
>Reload Image</button>
</div>
</template>
<style>
.image-examples-container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.image-examples-container hr {
border: 0;
height: 1px;
background-color: var(--ag-border-color);
margin-top: 1rem;
}
.reload-button {
display: block;
margin-top: 1rem;
border: 1px solid var(--ag-border-color);
background-color: var(--ag-background);
color: var(--ag-text-color);
padding: 0.5rem 1rem;
border-radius: var(--ag-radius);
cursor: pointer;
width: fit-content;
}
.reload-button:hover {
background-color: var(--ag-background-secondary);
}
.responsive-image-container {
width: 100%;
max-width: 800px;
}
.contain-fit-container {
width: 100%;
max-width: 800px;
background-color: var(--ag-background-secondary);
padding: 1rem;
border-radius: var(--ag-radius);
}
.custom-placeholder,
.custom-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100%;
gap: 0.5rem;
font-family: var(--ag-font-family-body);
}
.placeholder-icon,
.error-icon {
font-size: 2.5rem;
line-height: 1;
}
.custom-error {
background: var(--ag-danger-background);
color: var(--ag-text-primary);
padding: 2rem 1rem;
}
.custom-styled-image::part(ag-img) {
border: 4px solid var(--ag-primary-light);
border-radius: 16px;
box-shadow: var(--ag-shadow-3);
}
</style>
Live Preview
View Lit / Web Component Code
import { LitElement, html, css } from 'lit';
import 'agnosticui-core/image';
import 'agnosticui-core/icon';
export class ImageLitExamples extends LitElement {
createRenderRoot() {
return this;
}
static properties = {
basicImageSrc: { type: String },
responsiveImageSrc: { type: String },
objectFitImageSrc: { type: String },
responsiveSourcesImageSrc: { type: String },
fallbackImageSrc: { type: String },
placeholderImageSrc: { type: String },
errorImageSrc: { type: String },
styledImageSrc: { type: String },
};
static styles = css`
.image-examples-container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.image-examples-container hr {
border: 0;
height: 1px;
background-color: var(--ag-border-color);
margin-top: 1rem;
}
.reload-button {
display: block;
margin-top: 1rem;
border: 1px solid var(--ag-border-color);
background-color: var(--ag-background);
color: var(--ag-text-color);
padding: 0.5rem 1rem;
border-radius: var(--ag-radius);
cursor: pointer;
width: fit-content;
}
.reload-button:hover {
background-color: var(--ag-background-secondary);
}
.responsive-image-container {
width: 100%;
max-width: 800px;
}
.contain-fit-container {
width: 100%;
max-width: 800px;
background-color: var(--ag-background-secondary);
padding: 1rem;
border-radius: var(--ag-radius);
}
.custom-placeholder,
.custom-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100%;
gap: 0.5rem;
font-family: var(--ag-font-family-body);
}
.placeholder-icon,
.error-icon {
font-size: 2.5rem;
line-height: 1;
}
.custom-error {
background: var(--ag-danger-background);
color: var(--ag-text-primary);
padding: 2rem 1rem;
}
.custom-styled-image::part(ag-img) {
border: 4px solid var(--ag-primary-light);
border-radius: 16px;
box-shadow: var(--ag-shadow-3);
}
`;
constructor() {
super();
const basicImageUrl = 'https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?w=545&h=487&fit=crop&q=80';
const responsiveImageUrl = 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1200&h=800&fit=crop&q=80';
const objectFitImageUrl = 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=545&h=487&fit=crop&q=80';
const responsiveSourcesImageUrl = 'https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?w=545&h=487&fit=crop&q=80';
const fallbackImageUrl = 'https://thissourcedoesnotexist.com/image.jpg';
const placeholderImageUrl = 'https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=545&h=487&fit=crop&q=80&auto=format&ps=50';
const errorImageUrl = 'https://thissourcedoesnotexist.com/another-broken-image.jpg';
const styledImageUrl = 'https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=545&h=487&fit=crop&q=80';
this.basicImageSrc = basicImageUrl;
this.responsiveImageSrc = responsiveImageUrl;
this.objectFitImageSrc = objectFitImageUrl;
this.responsiveSourcesImageSrc = responsiveSourcesImageUrl;
this.fallbackImageSrc = fallbackImageUrl;
this.placeholderImageSrc = placeholderImageUrl;
this.errorImageSrc = errorImageUrl;
this.styledImageSrc = styledImageUrl;
this.basicImageUrl = basicImageUrl;
this.responsiveImageUrl = responsiveImageUrl;
this.objectFitImageUrl = objectFitImageUrl;
this.responsiveSourcesImageUrl = responsiveSourcesImageUrl;
this.placeholderImageUrl = placeholderImageUrl;
this.styledImageUrl = styledImageUrl;
}
reloadBasicImage() {
this.basicImageSrc = `${this.basicImageUrl}&t=${new Date().getTime()}`;
}
reloadResponsiveImage() {
this.responsiveImageSrc = `${this.responsiveImageUrl}&t=${new Date().getTime()}`;
}
reloadObjectFitImage() {
this.objectFitImageSrc = `${this.objectFitImageUrl}&t=${new Date().getTime()}`;
}
reloadResponsiveSourcesImage() {
this.responsiveSourcesImageSrc = `${this.responsiveSourcesImageUrl}&t=${new Date().getTime()}`;
}
reloadFallbackImage() {
this.fallbackImageSrc = `https://thissourcedoesnotexist.com/image.jpg?t=${new Date().getTime()}`;
}
reloadPlaceholderImage() {
this.placeholderImageSrc = `${this.placeholderImageUrl}&t=${new Date().getTime()}`;
}
reloadErrorImage() {
this.errorImageSrc = `https://thissourcedoesnotexist.com/another-broken-image.jpg?t=${new Date().getTime()}`;
}
reloadStyledImage() {
this.styledImageSrc = `${this.styledImageUrl}&t=${new Date().getTime()}`;
}
render() {
const responsiveSources = [
{
srcset: 'https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?w=1400&h=787&fit=crop&q=80',
media: '(min-width: 1024px)',
},
{
srcset: 'https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?w=1000&h=562&fit=crop&q=80',
media: '(min-width: 768px)',
},
];
return html`
<div class="image-examples-container">
<h2>Basic Image</h2>
<p>A standard image with a <code>src</code> and <code>alt</code> tag. This example also has the <code>fade</code> attribute enabled. Click the button to reload the image and see the effect.</p>
<ag-image
.src=${this.basicImageSrc}
alt="A beautiful landscape with a river and mountains."
.width=${545}
.height=${487}
fade
style="max-width: 100%; height: auto;"
></ag-image>
<button @click=${this.reloadBasicImage} class="reload-button">Reload Image</button>
<hr />
<h2>Responsive Image (No Layout Shift)</h2>
<p>
By omitting <code>width</code> and <code>height</code> attributes and providing <code>aspectRatio</code>, the image becomes responsive while preventing content layout shift. Fade is also enabled here.
</p>
<div class="responsive-image-container">
<ag-image
.src=${this.responsiveImageSrc}
alt="A forest path leading to a mountain."
aspectRatio="16/9"
fade
></ag-image>
</div>
<button @click=${this.reloadResponsiveImage} class="reload-button">Reload Image</button>
<hr />
<h2>Object Fit: Contain</h2>
<p>Use <code>fit="contain"</code> to ensure the entire image is visible within its container without being cropped.</p>
<div class="contain-fit-container">
<ag-image
.src=${this.objectFitImageSrc}
alt="A dense forest with sunlight filtering through."
aspectRatio="4/3"
fit="contain"
fade
></ag-image>
</div>
<button @click=${this.reloadObjectFitImage} class="reload-button">Reload Image</button>
<hr />
<h2>Responsive Sources with <picture></h2>
<p>Provide different images for different screen sizes using the <code>sources</code> property. Resize your browser to see the image change.</p>
<div class="responsive-image-container">
<ag-image
.src=${this.responsiveSourcesImageSrc}
alt="A responsive landscape that changes with screen size."
aspectRatio="16/9"
.sources=${responsiveSources}
fade
></ag-image>
</div>
<button @click=${this.reloadResponsiveSourcesImage} class="reload-button">Reload Image</button>
<hr />
<h2>Fallback Source</h2>
<p>If the primary <code>src</code> is broken, the <code>fallbackSrc</code> will be loaded instead. The fallback will also fade in.</p>
<ag-image
.src=${this.fallbackImageSrc}
fallbackSrc="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=545&h=487&fit=crop&q=80"
alt="An image with a fallback source."
.width=${545}
.height=${487}
fade
style="max-width: 100%; height: auto;"
></ag-image>
<button @click=${this.reloadFallbackImage} class="reload-button">Reload Image</button>
<hr />
<h2>Custom Placeholder Slot</h2>
<p>Provide custom content to be displayed while the image is loading.</p>
<ag-image
.src=${this.placeholderImageSrc}
alt="A nature scene with a custom placeholder."
.width=${545}
.height=${487}
style="max-width: 100%; height: auto;"
>
<div slot="placeholder" class="custom-placeholder">
<div class="placeholder-icon">π·</div>
<div>Loading beautiful scenery...</div>
</div>
</ag-image>
<button @click=${this.reloadPlaceholderImage} class="reload-button">Reload Image</button>
<hr />
<h2>Custom Error Slot</h2>
<p>Display a custom message or UI when an image fails to load.</p>
<div class="responsive-image-container">
<ag-image
.src=${this.errorImageSrc}
alt="An image with a custom error message."
aspectRatio="4/3"
style="width: 100%;"
>
<div slot="error" class="custom-error">
<div class="error-icon">
<ag-icon size="64" type="error" no-fill>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</ag-icon>
</div>
<h2 style="margin-top: 0;">Oops! The image could not be loaded.</h2>
</div>
</ag-image>
</div>
<button @click=${this.reloadErrorImage} class="reload-button">Reload Image</button>
<hr />
<h2>Styled with CSS Class and Parts</h2>
<p>Use a CSS class to apply custom styles to the component, including its internal parts like <code>::part(ag-img)</code>.</p>
<div class="responsive-image-container">
<ag-image
class="custom-styled-image"
.src=${this.styledImageSrc}
alt="A landscape with a custom border style."
aspectRatio="4/3"
fade
></ag-image>
</div>
<button @click=${this.reloadStyledImage} class="reload-button">Reload Image</button>
</div>
`;
}
}
// Register the custom element
customElements.define('image-lit-examples', ImageLitExamples);
Interactive Preview: Click the "Open in StackBlitz" button below to see this example running live in an interactive playground.
View React Code
import { useState } from "react";
import { ReactImage } from "agnosticui-core/image/react";
import { ReactIcon } from "agnosticui-core/icon/react";
export default function ImageReactExamples() {
// Basic Image
const basicImageUrl =
"https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?w=545&h=487&fit=crop&q=80";
const [basicImageSrc, setBasicImageSrc] = useState(basicImageUrl);
const reloadBasicImage = () => {
setBasicImageSrc(`${basicImageUrl}&t=${new Date().getTime()}`);
};
// Responsive Image
const responsiveImageUrl =
"https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1200&h=800&fit=crop&q=80";
const [responsiveImageSrc, setResponsiveImageSrc] =
useState(responsiveImageUrl);
const reloadResponsiveImage = () => {
setResponsiveImageSrc(`${responsiveImageUrl}&t=${new Date().getTime()}`);
};
// Object Fit Image
const objectFitImageUrl =
"https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=545&h=487&fit=crop&q=80";
const [objectFitImageSrc, setObjectFitImageSrc] = useState(objectFitImageUrl);
const reloadObjectFitImage = () => {
setObjectFitImageSrc(`${objectFitImageUrl}&t=${new Date().getTime()}`);
};
// Responsive Sources Image
const responsiveSourcesImageUrl =
"https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?w=545&h=487&fit=crop&q=80";
const [responsiveSourcesImageSrc, setResponsiveSourcesImageSrc] = useState(
responsiveSourcesImageUrl
);
const reloadResponsiveSourcesImage = () => {
setResponsiveSourcesImageSrc(
`${responsiveSourcesImageUrl}&t=${new Date().getTime()}`
);
};
const responsiveSources = [
{
srcset:
"https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?w=1400&h=787&fit=crop&q=80",
media: "(min-width: 1024px)",
},
{
srcset:
"https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?w=1000&h=562&fit=crop&q=80",
media: "(min-width: 768px)",
},
];
// Fallback Image
const fallbackImageUrl = "https://thissourcedoesnotexist.com/image.jpg";
const [fallbackImageSrc, setFallbackImageSrc] = useState(fallbackImageUrl);
const reloadFallbackImage = () => {
setFallbackImageSrc(`${fallbackImageUrl}?t=${new Date().getTime()}`);
};
// Placeholder Image
const placeholderImageUrl =
"https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=545&h=487&fit=crop&q=80&auto=format&ps=50";
const [placeholderImageSrc, setPlaceholderImageSrc] =
useState(placeholderImageUrl);
const reloadPlaceholderImage = () => {
setPlaceholderImageSrc(`${placeholderImageUrl}&t=${new Date().getTime()}`);
};
// Error Image
const errorImageUrl =
"https://thissourcedoesnotexist.com/another-broken-image.jpg";
const [errorImageSrc, setErrorImageSrc] = useState(errorImageUrl);
const reloadErrorImage = () => {
setErrorImageSrc(`${errorImageUrl}?t=${new Date().getTime()}`);
};
// Styled Image
const styledImageUrl =
"https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=545&h=487&fit=crop&q=80";
const [styledImageSrc, setStyledImageSrc] = useState(styledImageUrl);
const reloadStyledImage = () => {
setStyledImageSrc(`${styledImageUrl}&t=${new Date().getTime()}`);
};
return (
<div className="image-examples-container">
<style>{`
.image-examples-container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.image-examples-container hr {
border: 0;
height: 1px;
background-color: var(--ag-border-color);
margin-top: 1rem;
}
.reload-button {
display: block;
margin-top: 1rem;
border: 1px solid var(--ag-border-color);
background-color: var(--ag-background);
color: var(--ag-text-color);
padding: 0.5rem 1rem;
border-radius: var(--ag-radius);
cursor: pointer;
width: fit-content;
}
.reload-button:hover {
background-color: var(--ag-background-secondary);
}
.responsive-image-container {
width: 100%;
max-width: 800px;
}
.contain-fit-container {
width: 100%;
max-width: 800px;
background-color: var(--ag-background-secondary);
padding: 1rem;
border-radius: var(--ag-radius);
}
.custom-placeholder,
.custom-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100%;
gap: 0.5rem;
font-family: var(--ag-font-family-body);
}
.placeholder-icon,
.error-icon {
font-size: 2.5rem;
line-height: 1;
}
.custom-error {
background: var(--ag-danger-background);
color: var(--ag-text-primary);
padding: 2rem 1rem;
}
.custom-styled-image::part(ag-img) {
border: 4px solid var(--ag-primary-light);
border-radius: 16px;
box-shadow: var(--ag-shadow-3);
}
`}</style>
<h2>Basic Image</h2>
<p>
A standard image with a <code>src</code> and <code>alt</code> tag. This
example also has the <code>fade</code> prop enabled. Click the button to
reload the image and see the effect.
</p>
<ReactImage
src={basicImageSrc}
alt="A beautiful landscape with a river and mountains."
width={545}
height={487}
fade={true}
style={{ maxWidth: "100%", height: "auto" }}
/>
<button onClick={reloadBasicImage} className="reload-button">
Reload Image
</button>
<hr />
<h2>Responsive Image (No Layout Shift)</h2>
<p>
By omitting <code>width</code> and <code>height</code> props and
providing <code>aspectRatio</code>, the image becomes responsive while
preventing content layout shift. Fade is also enabled here.
</p>
<div className="responsive-image-container">
<ReactImage
src={responsiveImageSrc}
alt="A forest path leading to a mountain."
aspectRatio="16/9"
fade={true}
/>
</div>
<button onClick={reloadResponsiveImage} className="reload-button">
Reload Image
</button>
<hr />
<h2>Object Fit: Contain</h2>
<p>
Use <code>fit="contain"</code> to ensure the entire image is visible
within its container without being cropped.
</p>
<div className="contain-fit-container">
<ReactImage
src={objectFitImageSrc}
alt="A dense forest with sunlight filtering through."
aspectRatio="4/3"
fit="contain"
fade={true}
/>
</div>
<button onClick={reloadObjectFitImage} className="reload-button">
Reload Image
</button>
<hr />
<h2>Responsive Sources with <picture></h2>
<p>
Provide different images for different screen sizes using the{" "}
<code>sources</code> prop. Resize your browser to see the image change.
</p>
<div className="responsive-image-container">
<ReactImage
src={responsiveSourcesImageSrc}
alt="A responsive landscape that changes with screen size."
aspectRatio="16/9"
sources={responsiveSources}
fade={true}
/>
</div>
<button onClick={reloadResponsiveSourcesImage} className="reload-button">
Reload Image
</button>
<hr />
<h2>Fallback Source</h2>
<p>
If the primary <code>src</code> is broken, the <code>fallbackSrc</code>{" "}
will be loaded instead. The fallback will also fade in.
</p>
<ReactImage
src={fallbackImageSrc}
fallbackSrc="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=545&h=487&fit=crop&q=80"
alt="An image with a fallback source."
width={545}
height={487}
fade={true}
style={{ maxWidth: "100%", height: "auto" }}
/>
<button onClick={reloadFallbackImage} className="reload-button">
Reload Image
</button>
<hr />
<h2>Custom Placeholder Slot</h2>
<p>Provide custom content to be displayed while the image is loading.</p>
<ReactImage
src={placeholderImageSrc}
alt="A nature scene with a custom placeholder."
width={545}
height={487}
style={{ maxWidth: "100%", height: "auto" }}
>
<div slot="placeholder" className="custom-placeholder">
<div className="placeholder-icon">π·</div>
<div>Loading beautiful scenery...</div>
</div>
</ReactImage>
<button onClick={reloadPlaceholderImage} className="reload-button">
Reload Image
</button>
<hr />
<h2>Custom Error Slot</h2>
<p>Display a custom message or UI when an image fails to load.</p>
<div className="responsive-image-container">
<ReactImage
src={errorImageSrc}
alt="An image with a custom error message."
aspectRatio="4/3"
style={{ width: "100%" }}
>
<div slot="error" className="custom-error">
<div className="error-icon">
<ReactIcon size="64" type="error" noFill>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</ReactIcon>
</div>
<h2 style={{ marginTop: 0 }}>
Oops! The image could not be loaded.
</h2>
</div>
</ReactImage>
</div>
<button onClick={reloadErrorImage} className="reload-button">
Reload Image
</button>
<hr />
<h2>Styled with CSS Class and Parts</h2>
<p>
Use a CSS class to apply custom styles to the component, including its
internal parts like <code>::part(ag-img)</code>.
</p>
<div className="responsive-image-container">
<ReactImage
className="custom-styled-image"
src={styledImageSrc}
alt="A landscape with a custom border style."
aspectRatio="4/3"
fade={true}
/>
</div>
<button onClick={reloadStyledImage} className="reload-button">
Reload Image
</button>
</div>
);
}
Usage β
TIP
The framework examples below import AgnosticUI as an npm package. Alternatively, you can use the CLI for complete control, AI/LLM visibility, and full code ownership:
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add ImageThe CLI copies source code directly into your project, giving you full visibility and control. After running npx ag add, you'll receive exact import instructions.
Vue
<template>
<section>
<!-- Basic responsive image -->
<VueImage
class="responsive-image"
src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1200&h=800"
alt="A beautiful landscape"
aspectRatio="16/9"
/>
<!-- Image with fallback -->
<VueImage
src="primary-image.jpg"
fallbackSrc="fallback-image.jpg"
alt="Image with fallback"
aspectRatio="4/3"
/>
<!-- Image with custom fit and position -->
<VueImage
src="portrait.jpg"
alt="Portrait"
aspectRatio="3/4"
fit="contain"
position="top"
/>
<!-- Fixed size image (non-responsive) -->
<VueImage
src="icon.png"
alt="Icon"
:width="100"
:height="100"
/>
</section>
</template>
<script setup lang="ts">
import { VueImage } from 'agnosticui-core/image/vue';
</script>
<style scoped>
.responsive-image {
width: 100%;
max-width: 800px;
}
</style>React
import { ReactImage } from 'agnosticui-core/image/react';
export default function Example() {
return (
<section>
{/* Basic responsive image */}
<ReactImage
className="responsive-image"
src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1200&h=800"
alt="A beautiful landscape"
aspectRatio="16/9"
/>
{/* Image with fallback */}
<ReactImage
src="primary-image.jpg"
fallbackSrc="fallback-image.jpg"
alt="Image with fallback"
aspectRatio="4/3"
/>
{/* Image with custom fit and position */}
<ReactImage
src="portrait.jpg"
alt="Portrait"
aspectRatio="3/4"
fit="contain"
position="top"
/>
{/* Fixed size image (non-responsive) */}
<ReactImage
src="icon.png"
alt="Icon"
width={100}
height={100}
/>
</section>
);
}.responsive-image {
width: 100%;
max-width: 800px;
}Lit (Web Components)
<script type="module">
import 'agnosticui-core/image';
</script>
<style>
.responsive-image {
width: 100%;
max-width: 800px;
}
</style>
<section>
<!-- Basic responsive image -->
<ag-image
class="responsive-image"
src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1200&h=800"
alt="A beautiful landscape"
aspectRatio="16/9"
></ag-image>
<!-- Image with fallback -->
<ag-image
src="primary-image.jpg"
fallbackSrc="fallback-image.jpg"
alt="Image with fallback"
aspectRatio="4/3"
></ag-image>
<!-- Image with custom fit and position -->
<ag-image
src="portrait.jpg"
alt="Portrait"
aspectRatio="3/4"
fit="contain"
position="top"
></ag-image>
<!-- Fixed size image (non-responsive) -->
<ag-image
src="icon.png"
alt="Icon"
.width="100"
.height="100"
></ag-image>
</section>Fixed vs. Responsive Images β
Some examples on this page use the width and height props to set a fixed size for the image. This is intentional to demonstrate how to opt-out of responsive behavior. When you provide these props, the image will always render at that specific size. To make an image responsive, you must omit these props and follow the pattern described in the "Preventing Layout Shift" section below.
Preventing Layout Shift β
Cumulative Layout Shift (CLS) is a common web performance issue where content jumps as images load. The AgnosticUI Image component provides a simple and effective solution by allowing you to specify an image's aspect ratio.
The key is to omit the width and height props (which set a fixed size) and instead provide the aspectRatio prop while controlling the size with CSS.
The Recipe for Responsive, No-Shift Images β
- Omit
widthandheightProps: Do not pass these props, as they are used to opt-into a fixed, non-responsive size. - Set a Fluid Width: Use CSS to make the image's width responsive. This is best done with a
className. - Provide the
aspectRatioProp: Set this to the image's natural ratio (e.g.,aspectRatio="16/9").
When you follow this pattern, the browser reserves the correct vertical space for the image before it loads, preventing any layout jump.
/* In your CSS file */
.responsive-image-container {
width: 100%;
max-width: 800px;
}
<ag-image
class="responsive-image-container"
src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1200&h=800"
alt="A beautiful landscape."
aspectRatio="16/9"
></ag-image>Object Fit and Position β
You can control how the image fits within its container using the fit and position props, which correspond to the object-fit and object-position CSS properties.
fit: Can be'cover','contain','fill','none', or'scale-down'. Defaults to'cover'.position: Any valid CSS position value, like'top','center','bottom left'. Defaults to'center'.
The "Contain" example below shows an image with fit="contain" inside a container with a background color to demonstrate how the image is contained without being cropped.
Responsive Sources β
For more advanced responsive use cases, you can provide different image sources for different screen sizes or formats using the sources prop. This creates a native <picture> element under the hood.
Each object in the sources array can have srcset, media, type, and sizes properties.
<ag-image
src="small-image.jpg"
alt="A responsive image."
aspectRatio="16/9"
:sources="[
{ srcset: 'large-image.webp', type: 'image/webp', media: '(min-width: 1024px)' },
{ srcset: 'large-image.jpg', media: '(min-width: 1024px)' },
{ srcset: 'medium-image.webp', type: 'image/webp', media: '(min-width: 768px)' },
{ srcset: 'medium-image.jpg', media: '(min-width: 768px)' }
]"
></ag-image>Best Practice: Consistent Aspect Ratios To prevent layout shifts when the browser switches between different sources, ensure that all images (the default
srcand all images in thesourcesarray) share the same aspect ratio. Also, consider serving modern, highly-compressed image formats like AVIF and WebP in yoursourceslist for improved performance.
Fallback Source β
If the primary src fails to load, you can provide a fallbackSrc. The component will automatically attempt to load this backup image, preventing a broken image icon from appearing.
Custom Placeholders and Errors β
For a more tailored user experience, you can provide custom content to be displayed during loading or on error using named slots.
placeholderslot: Content in this slot is displayed while the image is loading. This is perfect for implementing custom skeleton loaders or low-quality image placeholders (LQIP) that match your site's design tokens.errorslot: Content in this slot is displayed if the image (and any fallback) fails to load.
<ag-image src="..." alt="...">
<div slot="placeholder"></div>
</ag-image>
<ag-image src="broken-image.jpg" alt="...">
<div slot="error"></div>
</ag-image>Styling β
While inline style is supported, the recommended approach for styling is to use standard CSS with the class attribute.
CSS Parts β
For deeper customization, the Image component exposes several CSS Parts that allow you to style its internal elements from outside:
::part(ag-img): The<img>element itself.::part(ag-placeholder): The container for the placeholder content.::part(ag-error): The container for the error content.
.custom-styled-image::part(ag-img) {
border-radius: 12px;
border: 2px solid var(--ag-primary-light);
}Props β
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | '' | Required. The primary image source URL. |
alt | string | '' | Required. Alternative text for accessibility. |
aspectRatio | string | undefined | The image's aspect ratio (e.g., "16/9"). Used to prevent layout shift on responsive images. |
sources | AgImageSource[] | [] | An array of source objects to create a <picture> element for responsive images. |
width | number | undefined | Sets a fixed width in pixels, opting out of responsiveness. |
height | number | undefined | Sets a fixed height in pixels. |
fit | 'cover' | 'contain' | 'fill' | 'none' | 'scale-down' | 'cover' | Defines how the image fits its container. Corresponds to object-fit. |
position | string | 'center' | Defines the image's position within its container. Corresponds to object-position. |
loading | 'lazy' | 'eager' | 'lazy' | Sets the native browser loading strategy. |
fade | boolean | false | If true, the image will fade in smoothly on load. |
duration | number | 200 | The duration of the fade-in transition in milliseconds. |
fallbackSrc | string | undefined | A backup image URL to load if the primary src fails. |
Best Practices & Implementation Details β
For those curious about the web standards at play, this section explains the "why" behind the ag-image component's design and how it aligns with modern best practices for 2025.
Two Core Methods for Preventing CLS β
There are two primary, best-practice methods to prevent layout shifts from images:
Intrinsic Sizing (The Classic Method): The simplest approach is to set
widthandheightattributes directly on an<img>tag. The browser uses these to calculate the aspect ratio before the image loads. You then make it responsive with CSS:<img src="..." width="1200" height="800" style="width: 100%; height: auto;">. This is a great baseline for simple use cases.CSS
aspect-ratio(The Modern Component Method): For dynamic or component-based designs, the more powerful method is to apply theaspect-ratioCSS property directly to the image container. This is the primary method used by theag-imagecomponent.
The ag-image component embraces the modern aspect-ratio method because it provides more robust control within a component architecture, especially when dealing with frameworks like React, Vue, or Svelte.
Why the Component Uses height: 100% β
Internally, the <ag-image> component acts as a container.
- When you provide the
aspectRatioprop, the<ag-image>container gets theaspect-ratiostyle, reserving the correct space on the page. - The inner
<img>element is styled withwidth: 100%andheight: 100%.
This tells the image to completely fill the pre-sized "box" created by its container. This pattern is crucial for making the fit prop (object-fit) work correctly, giving you precise control over whether the image should cover the box (cropping itself) or be contained within it (letterboxing)βa level of flexibility not easily achieved with the older height: auto method.