---
name: image
title: Image
url: https://ui.frontic.com/components/ui/image.md
tier: ui
install: npx @frontic/ui add image
files:
  - components/ui/image/Image.vue
  - components/ui/image/ImageFallback.vue
  - components/ui/image/index.ts
dependencies:
  - "@lucide/vue"
  - class-variance-authority
---

# Image

Shows an image with a loading effect and a placeholder while it loads, and a fallback when it fails, such as a product shot.

## Install

```sh
npx @frontic/ui add image
```

Used by: [Cart Summary](https://ui.frontic.com/components/sections/cart-summary.md), [Navigation Shop](https://ui.frontic.com/components/sections/navigation-shop.md), [Order Summary](https://ui.frontic.com/components/sections/order-summary.md), [Product Card Basic](https://ui.frontic.com/components/sections/product-card-basic.md), [Product Card Featured](https://ui.frontic.com/components/sections/product-card-featured.md), [Product Card Gallery](https://ui.frontic.com/components/sections/product-card-gallery.md), [Product Card Horizontal](https://ui.frontic.com/components/sections/product-card-horizontal.md), [Product Card Hover](https://ui.frontic.com/components/sections/product-card-hover.md), [Product Card Minimal](https://ui.frontic.com/components/sections/product-card-minimal.md), [Product Options](https://ui.frontic.com/components/sections/product-options.md), [Search Suggestions](https://ui.frontic.com/components/sections/search-suggestions.md), [Cart Item Compact](https://ui.frontic.com/components/templates/cart-item-compact.md), [Cart Item Detailed](https://ui.frontic.com/components/templates/cart-item-detailed.md), [Cart Item Minimal](https://ui.frontic.com/components/templates/cart-item-minimal.md), [Checkout Page](https://ui.frontic.com/components/templates/checkout-page.md)

npm packages: `@lucide/vue`, `class-variance-authority`

## Examples

```vue
<script setup lang="ts">
import { Image } from '@/components/ui/image'
</script>

<template>
  <Image
    src="https://static.frontic.com/cdn-cgi/image/format=auto/ui/banners/mens-jackets.png"
    placeholder-src="https://static.frontic.com/cdn-cgi/image/width=24,format=auto/ui/banners/mens-jackets.png"
    loading-effect="blur"
    alt="A man in a colour-block ski jacket in front of snowy mountains"
    class="aspect-video w-full max-w-sm"
  />
</template>
```

## Anatomy

```vue
<Image>
  <ImageFallback />
</Image>
```

## API reference

### Image

Shows an image with a loading state and a fallback if it fails to load. Renders a `<span>`.

`data-slot="image"` · `Image.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `src` | `string` |  | URL of the image to show. |
| `alt` | `string` |  | Text alternative read by screen readers; leave empty for a purely decorative image. |
| `loadingEffect` | `LoadingEffect`: `'skeleton' \| 'blur' \| 'none'` | `'skeleton'` | What shows until the image has loaded: a pulsing placeholder, the `placeholderSrc` preview blurred, or nothing. Blur without a `placeholderSrc` shows the pulsing placeholder. |
| `fallbackSrc` | `string` |  | URL tried when `src` fails to load; the fallback slot shows when both fail. |
| `placeholderSrc` | `string` |  | URL of a tiny version of the image, e.g. 24px wide, shown blurred while `src` loads when `loadingEffect` is `blur`. |
| `priority` | `boolean` | `false` | Loads the image first, eagerly and at high fetch priority, instead of when it nears the viewport. Set it on the one image that is the page's largest paint, such as a hero or the first gallery image. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `fallback` |  | Replaces the placeholder shown when the image fails to load and no fallback URL loads either. |

### ImageFallback

Shows a placeholder in place of an image that failed to load. Renders a `<span>`.

`data-slot="image-fallback"` · `ImageFallback.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

## Styling

| Part | Recipe |
| --- | --- |
| `default` | [`image`](https://ui.frontic.com/DESIGN.md#image) |
| `loading` | [`image-loading`](https://ui.frontic.com/DESIGN.md#image-loading) |
| `fallback` | [`image-fallback`](https://ui.frontic.com/DESIGN.md#image-fallback) |

## Source

`components/ui/image/Image.vue`

```vue
<script lang="ts">
/** Shows an image with a loading state and a fallback if it fails to load. */
export default {}
</script>

<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { onMounted, ref, useTemplateRef, watch } from 'vue'
import { cva } from 'class-variance-authority'
import ImageFallback from './ImageFallback.vue'

type LoadingEffect = 'skeleton' | 'blur' | 'none'

const image = cva('fui-image relative overflow-hidden')

const imageLoading = cva('fui-image-loading absolute inset-0 z-10 overflow-hidden', {
  variants: {
    effect: {
      skeleton: 'fui-image-loading-skeleton',
      blur: 'fui-image-loading-blur',
    },
  },
  defaultVariants: { effect: 'skeleton' },
})

defineOptions({ inheritAttrs: false })

defineSlots<{
  /** Replaces the placeholder shown when the image fails to load and no fallback URL loads either. */
  fallback?: () => unknown
}>()

const props = withDefaults(
  defineProps<{
    class?: HTMLAttributes['class']
    /** URL of the image to show. */
    src?: string
    /** Text alternative read by screen readers; leave empty for a purely decorative image. */
    alt?: string
    /** What shows until the image has loaded: a pulsing placeholder, the `placeholderSrc` preview blurred, or nothing. Blur without a `placeholderSrc` shows the pulsing placeholder. */
    loadingEffect?: LoadingEffect
    /** URL tried when `src` fails to load; the fallback slot shows when both fail. */
    fallbackSrc?: string
    /** URL of a tiny version of the image, e.g. 24px wide, shown blurred while `src` loads when `loadingEffect` is `blur`. */
    placeholderSrc?: string
    /** Loads the image first, eagerly and at high fetch priority, instead of when it nears the viewport. Set it on the one image that is the page's largest paint, such as a hero or the first gallery image. */
    priority?: boolean
  }>(),
  {
    loadingEffect: 'skeleton',
  },
)

const loaded = ref(false)
const errored = ref(false)
const fallbackErrored = ref(false)
const imgRef = useTemplateRef<HTMLImageElement>('imgRef')

watch(
  () => props.src,
  () => {
    loaded.value = false
    errored.value = false
    fallbackErrored.value = false
  },
)

onMounted(() => {
  const img = imgRef.value
  if (!img) return
  if (img.complete) {
    if (img.naturalWidth === 0) errored.value = true
    else loaded.value = true
  }
})

function onLoad() {
  loaded.value = true
}

function onError() {
  if (errored.value && props.fallbackSrc) {
    fallbackErrored.value = true
  } else {
    errored.value = true
  }
}
</script>

<template>
  <span data-slot="image" :class="[image(), props.class]">
    <template v-if="(errored && !fallbackSrc) || fallbackErrored">
      <slot name="fallback">
        <ImageFallback />
      </slot>
    </template>

    <template v-else>
      <span v-if="!loaded && loadingEffect === 'blur' && placeholderSrc" data-slot="image-loading" :class="imageLoading({ effect: 'blur' })">
        <img :src="placeholderSrc" alt="" aria-hidden="true" :loading="priority ? 'eager' : 'lazy'" class="block size-full scale-110 object-cover blur-lg" />
      </span>
      <span v-else-if="!loaded && loadingEffect !== 'none'" data-slot="image-loading" :class="[imageLoading({ effect: 'skeleton' }), 'animate-pulse motion-reduce:animate-none']" />

      <img
        ref="imgRef"
        :loading="priority ? 'eager' : 'lazy'"
        :decoding="priority ? 'auto' : 'async'"
        :fetchpriority="priority ? 'high' : undefined"
        v-bind="$attrs"
        :src="errored && fallbackSrc ? fallbackSrc : src"
        :alt="alt"
        data-slot="image-img"
        class="block size-full object-cover"
        @load="onLoad"
        @error="onError"
      />
    </template>
  </span>
</template>
```

`components/ui/image/ImageFallback.vue`

```vue
<script lang="ts">
/** Shows a placeholder in place of an image that failed to load. */
export default {}
</script>

<script setup lang="ts">
import { ImageOffIcon } from '@lucide/vue'

import type { HTMLAttributes } from 'vue'

const props = defineProps<{
  class?: HTMLAttributes['class']
}>()
</script>

<template>
  <span data-slot="image-fallback" :class="['fui-image-fallback', props.class]">
    <slot>
      <ImageOffIcon data-icon />
    </slot>
  </span>
</template>
```
