# Image

Displays an image. Renders a native `img`, defaults to `object-fit: cover`, and accepts every style prop.

Nothing extra happens by default. Lazy loading, a loading placeholder and an error placeholder are all opt-in.

Group: Basics

## Import

```tsx
import { Image } from "@indoku/ui"
```

## Examples

### Basic

```tsx
const art = (hue: number, w = 600, h = 400) =>
  `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 80% 62%)"/><stop offset="1" stop-color="hsl(${hue + 60} 70% 32%)"/></linearGradient></defs><rect width="${w}" height="${h}" fill="url(#g)"/><circle cx="${w * 0.3}" cy="${h * 0.32}" r="${h * 0.11}" fill="rgba(255,255,255,.6)"/><path d="M0 ${h}L${w * 0.32} ${h * 0.55}L${w * 0.52} ${h * 0.8}L${w * 0.74} ${h * 0.45}L${w} ${h}Z" fill="rgba(0,0,0,.3)"/></svg>`)}`


function ImageBasic() {
  return <Image rounded="md" src={art(210)} alt="Mountain landscape" maxW="sm" />
}
```

### Height

Use the `height` prop to change the height of the image.

```tsx
const art = (hue: number, w = 600, h = 400) =>
  `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 80% 62%)"/><stop offset="1" stop-color="hsl(${hue + 60} 70% 32%)"/></linearGradient></defs><rect width="${w}" height="${h}" fill="url(#g)"/><circle cx="${w * 0.3}" cy="${h * 0.32}" r="${h * 0.11}" fill="rgba(255,255,255,.6)"/><path d="M0 ${h}L${w * 0.32} ${h * 0.55}L${w * 0.52} ${h * 0.8}L${w * 0.74} ${h * 0.45}L${w} ${h}Z" fill="rgba(0,0,0,.3)"/></svg>`)}`


function ImageWithHeight() {
  return <Image height="200px" src={art(20)} alt="Sunset landscape" />
}
```

### Circular

Combine `boxSize` and `borderRadius="full"` for a round image.

```tsx
const art = (hue: number, w = 600, h = 400) =>
  `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 80% 62%)"/><stop offset="1" stop-color="hsl(${hue + 60} 70% 32%)"/></linearGradient></defs><rect width="${w}" height="${h}" fill="url(#g)"/><circle cx="${w * 0.3}" cy="${h * 0.32}" r="${h * 0.11}" fill="rgba(255,255,255,.6)"/><path d="M0 ${h}L${w * 0.32} ${h * 0.55}L${w * 0.52} ${h * 0.8}L${w * 0.74} ${h * 0.45}L${w} ${h}Z" fill="rgba(0,0,0,.3)"/></svg>`)}`


function ImageCircular() {
  return <Image src={art(150, 300, 300)} boxSize="150px" borderRadius="full" fit="cover" alt="Round avatar" />
}
```

### Aspect ratio

Use the `aspectRatio` prop to change the aspect ratio of the image.

```tsx
const art = (hue: number, w = 600, h = 400) =>
  `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 80% 62%)"/><stop offset="1" stop-color="hsl(${hue + 60} 70% 32%)"/></linearGradient></defs><rect width="${w}" height="${h}" fill="url(#g)"/><circle cx="${w * 0.3}" cy="${h * 0.32}" r="${h * 0.11}" fill="rgba(255,255,255,.6)"/><path d="M0 ${h}L${w * 0.32} ${h * 0.55}L${w * 0.52} ${h * 0.8}L${w * 0.74} ${h * 0.45}L${w} ${h}Z" fill="rgba(0,0,0,.3)"/></svg>`)}`


function ImageWithAspectRatio() {
  return <Image src={art(280)} alt="Landscape in 4:3" aspectRatio={4 / 3} width="300px" />
}
```

### Fit

By default the image applies `object-fit: cover`. Use `fit` to change it.

```tsx
const art = (hue: number, w = 600, h = 400) =>
  `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 80% 62%)"/><stop offset="1" stop-color="hsl(${hue + 60} 70% 32%)"/></linearGradient></defs><rect width="${w}" height="${h}" fill="url(#g)"/><circle cx="${w * 0.3}" cy="${h * 0.32}" r="${h * 0.11}" fill="rgba(255,255,255,.6)"/><path d="M0 ${h}L${w * 0.32} ${h * 0.55}L${w * 0.52} ${h * 0.8}L${w * 0.74} ${h * 0.45}L${w} ${h}Z" fill="rgba(0,0,0,.3)"/></svg>`)}`


function ImageWithFit() {
  return (
    <HStack gap="4" align="flex-start">
      {(["cover", "contain", "fill"] as const).map((fit) => (
        <Stack key={fit} gap="2" align="center">
          <Image border="1px solid" borderColor="border" rounded="md" h="150px" w="200px" fit={fit} src={art(190, 300, 300)} alt={`fit ${fit}`} />
          <Text fontSize="sm" color="fg.muted">{fit}</Text>
        </Stack>
      ))}
    </HStack>
  )
}
```

### Align

Use `align` to set `object-position` when the image is cropped.

```tsx
const art = (hue: number, w = 600, h = 400) =>
  `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 80% 62%)"/><stop offset="1" stop-color="hsl(${hue + 60} 70% 32%)"/></linearGradient></defs><rect width="${w}" height="${h}" fill="url(#g)"/><circle cx="${w * 0.3}" cy="${h * 0.32}" r="${h * 0.11}" fill="rgba(255,255,255,.6)"/><path d="M0 ${h}L${w * 0.32} ${h * 0.55}L${w * 0.52} ${h * 0.8}L${w * 0.74} ${h * 0.45}L${w} ${h}Z" fill="rgba(0,0,0,.3)"/></svg>`)}`


function ImageWithAlign() {
  return (
    <HStack gap="4" align="flex-start">
      {(["top", "center", "bottom"] as const).map((align) => (
        <Stack key={align} gap="2" align="center">
          <Image rounded="md" h="120px" w="120px" fit="cover" align={align} src={art(330, 300, 600)} alt={`align ${align}`} />
          <Text fontSize="sm" color="fg.muted">{align}</Text>
        </Stack>
      ))}
    </HStack>
  )
}
```

### HTML width and height

`htmlWidth` and `htmlHeight` set the native `width` and `height` attributes, while `width` and `height` are style props.

```tsx
const art = (hue: number, w = 600, h = 400) =>
  `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 80% 62%)"/><stop offset="1" stop-color="hsl(${hue + 60} 70% 32%)"/></linearGradient></defs><rect width="${w}" height="${h}" fill="url(#g)"/><circle cx="${w * 0.3}" cy="${h * 0.32}" r="${h * 0.11}" fill="rgba(255,255,255,.6)"/><path d="M0 ${h}L${w * 0.32} ${h * 0.55}L${w * 0.52} ${h * 0.8}L${w * 0.74} ${h * 0.45}L${w} ${h}Z" fill="rgba(0,0,0,.3)"/></svg>`)}`


function ImageWithHtmlSize() {
  return <Image htmlWidth="400px" htmlHeight="400px" src={art(100, 400, 400)} alt="Square with native size" />
}
```

### As child

Use `asChild` to apply the image styles to a single child, such as a framework image component.

```tsx
const art = (hue: number, w = 600, h = 400) =>
  `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 80% 62%)"/><stop offset="1" stop-color="hsl(${hue + 60} 70% 32%)"/></linearGradient></defs><rect width="${w}" height="${h}" fill="url(#g)"/><circle cx="${w * 0.3}" cy="${h * 0.32}" r="${h * 0.11}" fill="rgba(255,255,255,.6)"/><path d="M0 ${h}L${w * 0.32} ${h * 0.55}L${w * 0.52} ${h * 0.8}L${w * 0.74} ${h * 0.45}L${w} ${h}Z" fill="rgba(0,0,0,.3)"/></svg>`)}`


function ImageAsChild() {
  return (
    <Image asChild rounded="md" fit="cover" boxSize="160px">
      <img src={art(240, 320, 320)} alt="Rendered through asChild" width={320} height={320} />
    </Image>
  )
}
```

### Lazy loading

Not lazy by default. Add the native `loading="lazy"` so the browser downloads the image only when it is near the viewport.

```tsx
const art = (hue: number, w = 600, h = 400) =>
  `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 80% 62%)"/><stop offset="1" stop-color="hsl(${hue + 60} 70% 32%)"/></linearGradient></defs><rect width="${w}" height="${h}" fill="url(#g)"/><circle cx="${w * 0.3}" cy="${h * 0.32}" r="${h * 0.11}" fill="rgba(255,255,255,.6)"/><path d="M0 ${h}L${w * 0.32} ${h * 0.55}L${w * 0.52} ${h * 0.8}L${w * 0.74} ${h * 0.45}L${w} ${h}Z" fill="rgba(0,0,0,.3)"/></svg>`)}`


function ImageLazy() {
  return <Image loading="lazy" decoding="async" rounded="md" src={art(45)} alt="Loaded only when near the viewport" maxW="sm" />
}
```

### Loading placeholder

`fallback` is shown until the image has loaded. Give the image a size (`w`/`h`, `boxSize` or `aspectRatio`) so the placeholder has room. This demo delays the source to simulate a slow network.

```tsx
const art = (hue: number, w = 600, h = 400) =>
  `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 80% 62%)"/><stop offset="1" stop-color="hsl(${hue + 60} 70% 32%)"/></linearGradient></defs><rect width="${w}" height="${h}" fill="url(#g)"/><circle cx="${w * 0.3}" cy="${h * 0.32}" r="${h * 0.11}" fill="rgba(255,255,255,.6)"/><path d="M0 ${h}L${w * 0.32} ${h * 0.55}L${w * 0.52} ${h * 0.8}L${w * 0.74} ${h * 0.45}L${w} ${h}Z" fill="rgba(0,0,0,.3)"/></svg>`)}`


function ImageWithFallback() {
  const [src, setSrc] = React.useState<string>()
  React.useEffect(() => {
    if (src) return
    const timer = setTimeout(() => setSrc(art(170)), 1500)
    return () => clearTimeout(timer)
  }, [src])
  return (
    <Stack gap="3" align="flex-start">
      <Image src={src} alt="Slow image" w="240px" h="160px" rounded="md" fallback={<Skeleton />} />
      <Button size="sm" variant="outline" onClick={() => setSrc(undefined)}>Reload</Button>
    </Stack>
  )
}
```

### Error placeholder

When the image fails to load (404, bad URL, blocked), a built-in icon placeholder is shown. Replace it with `errorFallback`. Using `errorFallback` alone adds no loading placeholder.

```tsx
function ImageWithError() {
  return (
    <HStack gap="4" align="flex-start">
      <Stack gap="2" align="center">
        <Image src="/this-image-does-not-exist.png" alt="Missing image" w="160px" h="120px" rounded="md" fallback={<Skeleton />} />
        <Text fontSize="sm" color="fg.muted">default error icon</Text>
      </Stack>
      <Stack gap="2" align="center">
        <Image src="/this-image-does-not-exist.png" alt="Missing image" w="160px" h="120px" rounded="md" errorFallback={<Text fontSize="sm" color="fg.muted">No preview</Text>} />
        <Text fontSize="sm" color="fg.muted">custom errorFallback</Text>
      </Stack>
    </HStack>
  )
}
```

### Responsive sources

Native `srcSet` and `sizes` let the browser pick the smallest file that fits the layout.

```tsx
const art = (hue: number, w = 600, h = 400) =>
  `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 80% 62%)"/><stop offset="1" stop-color="hsl(${hue + 60} 70% 32%)"/></linearGradient></defs><rect width="${w}" height="${h}" fill="url(#g)"/><circle cx="${w * 0.3}" cy="${h * 0.32}" r="${h * 0.11}" fill="rgba(255,255,255,.6)"/><path d="M0 ${h}L${w * 0.32} ${h * 0.55}L${w * 0.52} ${h * 0.8}L${w * 0.74} ${h * 0.45}L${w} ${h}Z" fill="rgba(0,0,0,.3)"/></svg>`)}`


function ImageResponsive() {
  return (
    <Image
      rounded="md"
      maxW="sm"
      src={art(300, 600, 400)}
      srcSet={`${art(300, 300, 200)} 300w, ${art(300, 600, 400)} 600w, ${art(300, 1200, 800)} 1200w`}
      sizes="(max-width: 640px) 300px, 600px"
      loading="lazy"
      alt="Responsive image"
    />
  )
}
```

## Props

### Image

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `fit` | `CSSProperties['objectFit']` | `'cover'` | Maps to `object-fit`. |
| `align` | `CSSProperties['objectPosition']` |  | Maps to `object-position`. |
| `htmlWidth` | `string \| number` |  | Native `width` attribute. Use `width` for the CSS property. |
| `htmlHeight` | `string \| number` |  | Native `height` attribute. Use `height` for the CSS property. |
| `asChild` | `boolean` | `false` | Render the single child element with the image props and styles instead of an `img`. `fallback` and `errorFallback` are ignored. |
| `fallback` | `ReactNode` |  | Shown until the image has loaded. Setting it wraps the image in a frame. |
| `errorFallback` | `ReactNode` |  | Replaces the built-in error icon. Setting it (or `fallback`) enables the error placeholder. |

`*` = required. Style props and HTML attributes are accepted too.

## Accessibility

- Always set `alt`. Use `alt=""` for purely decorative images.
- The loading placeholder is hidden from assistive tech. The error placeholder is exposed as an image labelled with `alt`.
- Set `htmlWidth` and `htmlHeight` (or `aspectRatio`) to reserve space and avoid layout shift while loading.

## Notes

- All native `img` attributes are forwarded, including `loading`, `decoding`, `srcSet`, `sizes`, `onLoad` and `onError`.
- With `fallback` or `errorFallback`, size and style props apply to a wrapping `span` frame, and `fit`, `ref` and native attributes stay on the `img`. Without them there is no wrapper.
- Image optimization (smaller files in WebP or AVIF) happens at build time or on a CDN, not in this component. Use `srcSet` and `sizes` for responsive files, and wrap with `<picture>` and `<source type="image/webp">` to offer modern formats with a fallback.
- The Provider preflight already sets `display: block`, `max-width: 100%` and `height: auto` on images.

## See also

- Aspect Ratio: https://ui.indoku.org/docs/aspect-ratio
- Avatar: https://ui.indoku.org/docs/avatar
- Skeleton: https://ui.indoku.org/docs/skeleton
