Skip to content

Image ​

Experimental Alpha

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 ​

Vue
Lit
React
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.

πŸ“·
Loading beautiful scenery...

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 &lt;picture&gt;</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 &lt;picture&gt;</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 &lt;picture&gt;</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>
  );
}
Open in StackBlitz

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:

bash
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add Image

The 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
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
tsx
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>
  );
}
css
.responsive-image {
  width: 100%;
  max-width: 800px;
}
Lit (Web Components)
html
<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 ​

  1. Omit width and height Props: Do not pass these props, as they are used to opt-into a fixed, non-responsive size.
  2. Set a Fluid Width: Use CSS to make the image's width responsive. This is best done with a className.
  3. Provide the aspectRatio Prop: 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;
}
html
<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.

html
<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 src and all images in the sources array) share the same aspect ratio. Also, consider serving modern, highly-compressed image formats like AVIF and WebP in your sources list 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.

  • placeholder slot: 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.
  • error slot: Content in this slot is displayed if the image (and any fallback) fails to load.
html
<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.
css
.custom-styled-image::part(ag-img) {
  border-radius: 12px;
  border: 2px solid var(--ag-primary-light);
}

Props ​

PropTypeDefaultDescription
srcstring''Required. The primary image source URL.
altstring''Required. Alternative text for accessibility.
aspectRatiostringundefinedThe image's aspect ratio (e.g., "16/9"). Used to prevent layout shift on responsive images.
sourcesAgImageSource[][]An array of source objects to create a <picture> element for responsive images.
widthnumberundefinedSets a fixed width in pixels, opting out of responsiveness.
heightnumberundefinedSets a fixed height in pixels.
fit'cover' | 'contain' | 'fill' | 'none' | 'scale-down''cover'Defines how the image fits its container. Corresponds to object-fit.
positionstring'center'Defines the image's position within its container. Corresponds to object-position.
loading'lazy' | 'eager''lazy'Sets the native browser loading strategy.
fadebooleanfalseIf true, the image will fade in smoothly on load.
durationnumber200The duration of the fade-in transition in milliseconds.
fallbackSrcstringundefinedA 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:

  1. Intrinsic Sizing (The Classic Method): The simplest approach is to set width and height attributes 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.

  2. CSS aspect-ratio (The Modern Component Method): For dynamic or component-based designs, the more powerful method is to apply the aspect-ratio CSS property directly to the image container. This is the primary method used by the ag-image component.

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.

  1. When you provide the aspectRatio prop, the <ag-image> container gets the aspect-ratio style, reserving the correct space on the page.
  2. The inner <img> element is styled with width: 100% and height: 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.