Documentation

Image and Icon Components

Display images from a URL, a rooted asset path, or a bitmap, plus icons from any Iconify set.

Source

A rooted path (/x.jpg) loads from static/, checked at build time. A URL is fetched. A content:// URI from a picker passes through as is:

<Image src="/backgrounds/city.jpg" />
<Image src={url} />
<Image src={pickedUri} />

A path with no leading slash and no scheme is a build error — there is no relative asset.

Fit and size

fit is CSS's object-fit, and sizing is class, same as any element:

<Image src={url} fit="cover" class="w-full aspect-video" />
<Image src={url} fit="contain" class="w-full h-200" />
fitEffect
"cover"Fill the bounds, cropping overflow
"contain"Fit inside the bounds, keeping the aspect ratio
"fill"Stretch to fill both dimensions

Corner radius

<Image src={url} class="rounded-lg w-full aspect-square" />
<Image src={avatar} class="rounded-full w-48 h-48" />   // circle

Overlay gradients

Add a gradient over an image for text contrast, with overlay:

<Image
  src={poster}
  overlay="v-gradient(transparent 60%, #000)"
  class="w-full h-400"
/>

Or the same gradient on a <Box> stacked over the image, when something else needs to sit between them:

<Box>
  <Image src={photo} class="w-full h-full" />
  <Box background="v-gradient(transparent 60%, #000)" class="w-full h-full" />
  <Text color="#FFF" class="p-16">Caption</Text>
</Box>

Clickable images

onClick works on <Image> like any element:

<Image
  src={product.image}
  onClick={() => $navigate("/product/${product.id}")}
/>

Load callbacks

onError fires when a load fails outright. onLoad receives the decoded image's intrinsic size in pixels, the web's naturalWidth and naturalHeight:

<Image src={url}
       onLoad={(w, h) => report(w, h)}
       onError={() => failed = true} />

Both onLoad parameters are optional: (w) => and () => also work. onError takes none. Use onLoad for anything that depends on what actually arrived, such as an aspect-ratio layout or telling a placeholder image apart from a photograph.

UltraHDR

A JPEG carrying a gain map holds two pictures: a normal one, and a map of how much brighter each pixel goes on a display that can show it. Whitehall decodes it three ways.

<Image src="/photo.jpg" hdr />       // base × gain map — the point of the format
<Image src="/photo.jpg" sdr />       // the base picture alone
<Image src="/photo.jpg" gainmap />   // the gain map itself, as greyscale

Or bind it, when the mode is something the app decides:

var mode = $state("hdr")

<Image src="/photo.jpg" mode={mode} />   // "hdr" | "sdr" | "gainmap"

The three shorthands and mode= are one prop wearing four spellings. Writing two of them is a build error rather than a silent winner:

<Image src="/photo.jpg" hdr sdr />   // error: asks for 2 decode modes at once
It is an ordinary <Image>. The disk cache, the network path, fit, onClick, corner radius and overlays all work alongside it — decoding a gain map changes what the pixels are, not what the component is.
Gain map decoding needs API 34. Below that the platform has no Gainmap to read.

Icon Component

An icon names the set it comes from, and the set is declared in whitehall.toml — see Icons for the whole of it.

<Icon name="lucide:heart" />
<Icon name="lucide:star" size={24} color="primary" />
<Icon name="lucide:check" color="#00FF00" />

Common Patterns

Avatar

<Image src={user.avatar} class="w-40 h-40 rounded-full" />

Hero Image

<Box>
  <Image src={article.cover} fit="cover" class="w-full h-250" />
  <Box background="v-gradient(transparent 50%, #000)" class="w-full h-full" />
  <Column class="w-full h-full items-start justify-end p-20">
    <Text color="#FFF" class="text-3xl font-bold">
      {article.title}
    </Text>
  </Column>
</Box>

Product Card

<Card onClick={() => $navigate("/product/${product.id}")}>
  <Image src={product.image} fit="cover" class="w-full aspect-square" />
  <Column class="p-12 gap-4">
    <Text class="text-sm" maxLines={2}>{product.name}</Text>
    <Row class="gap-4 items-center">
      <Icon name="lucide:star" size={16} color="primary" />
      <Text class="text-xs">{product.rating}</Text>
    </Row>
    <Text class="text-base font-bold">${product.price}</Text>
  </Column>
</Card>

Icon with Label

<Column class="items-center gap-4">
  <Icon name="lucide:shopping-cart" size={32} color="primary" />
  <Text class="text-xs">Cart</Text>
</Column>

Icon Button

<Button onClick={toggleFavorite}>
  @if (isFavorite) {
    <Icon name="lucide:heart" color="error" />
  } else {
    <Icon name="lucide:heart" color="secondary" />
  }
</Button>

Rating Stars

val rating = 4.5

<Row class="gap-4">
  @for (i in 1..5) {
    @if (i <= rating) {
      <Icon name="lucide:star" color="primary" size={20} />
    } else {
      <Icon name="lucide:star" color="secondary" size={20} />
    }
  }
  <Text class="text-sm" color="secondary">({reviewCount})</Text>
</Row>

Image Gallery Grid

<LazyVerticalGrid columns={3} class="gap-2">
  @for (photo in photos, key = { it.id }) {
    <Image
      src={photo.thumbnail}
      fit="cover"
      class="aspect-square"
      onClick={() => $navigate("/photo/${photo.id}")}
      transition:crossfade="photo-${photo.id}"
    />
  }
</LazyVerticalGrid>

Background Image

<Box class="w-full h-full">
  <Image src={backgroundUrl} fit="cover" class="w-full h-full" />
  <Box background="v-gradient(transparent, #000000CC)" class="w-full h-full" />
  <Column class="w-full h-full items-center justify-center p-20">
    <Text color="#FFF" class="text-4xl font-bold">Welcome</Text>
  </Column>
</Box>

Profile Header

<Column>
  <Image src={user.coverPhoto} fit="cover" class="w-full h-200" />
  <Row class="p-16 gap-16">
    <Image src={user.avatar} class="w-80 h-80 rounded-full mt-[-40]" />   // overlaps the cover photo
    <Column class="flex-1">
      <Text class="text-2xl font-bold">{user.name}</Text>
      <Text class="text-sm" color="secondary">{user.bio}</Text>
    </Column>
  </Row>
</Column>

Prop Reference

Image Props

PropTypeDescription
srcStringRooted path, URL, or content:// URI
bitmapBitmapAn in-memory bitmap, drawn directly
fitString"cover", "contain", or "fill"
overlayStringA gradient drawn over the image
hdr / sdr / gainmap / modeBoolean / StringUltraHDR decode mode — one at a time
onLoadFunctionCalled with the decoded size in pixels, (w, h) =>
onErrorFunctionCalled when the load fails, no parameters
onClickFunctionTap handler, like any element

Icon Props

PropTypeDescription
nameStringset:kebab-name, or an imported set.PascalName
sizeNumberIcon size in dp
colorStringA theme token, hex, or Tailwind palette name

See Also