Documentation

Image and Icon Components

Display images from URLs, resources, or bitmaps, plus Material Design icons.

Image Component

From URL

<Image src="https://example.com/photo.jpg" />

From Bitmap

val bitmap = loadBitmap()
<Image bitmap={bitmap} />

Size Control

<Image src={url} w={200} h={200} />
<Image src={url} fillMaxWidth h={300} />

Aspect Ratio

Maintain aspect ratio while fitting container:

<Image src={url} aspectRatio={16.0/9.0} />
<Image src={url} aspectRatio={1.0} />  // Square

Content Scale

Control how images fit within their bounds:

<Image src={url} fit="crop" />    // Fill, cropping if needed
<Image src={url} fit="fit" />     // Fit inside, may have empty space
<Image src={url} fit="fillWidth" />
<Image src={url} fit="fillHeight" />
Fit ValueEffect
"crop"Fill bounds, crop overflow
"fit"Fit inside bounds, maintain aspect ratio
"fillWidth"Fill width, scale height proportionally
"fillHeight"Fill height, scale width proportionally
"fillBounds"Fill both dimensions (may distort)

Corner Radius

<Image src={url} cornerRadius={8} />
<Image src={url} cornerRadius={100} />  // Circle

Circular Images

<Image src={avatar} w={48} h={48} cornerRadius={24} />

Overlay Gradient

Add gradient overlays for better text contrast:

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

Loading State

<Image
  src={url}
  placeholder={R.drawable.placeholder}
  error={R.drawable.error}
/>

Clickable Images

Images support onClick like any component:

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

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 = "hdr"

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

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

<Image src="/photo.jpg" hdr sdr />   // error
It is an ordinary <Image>. The disk cache, the network path, fit=, onClick, corner radius, overlays and morph bindings 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="ic:baseline-settings" />

Size

<Icon name="lucide:star" size={16} />
<Icon name="lucide:star" size={24} />
<Icon name="lucide:star" size={48} />

Color

<Icon name="lucide:heart" color="primary" />
<Icon name="ic:baseline-error" color="error" />
<Icon name="lucide:check" color="#00FF00" />

Rotation

<Icon name="ic:baseline-arrow-forward" rotate={90} />   // Down
<Icon name="ic:baseline-arrow-forward" rotate={180} />  // Back
<Icon name="ic:baseline-arrow-forward" rotate={270} />  // Up

Common Patterns

Avatar

<Image
  src={user.avatar}
  w={40} h={40}
  cornerRadius={20}
/>

Hero Image

<Box>
  <Image
    src={article.cover}
    fillMaxWidth
    h={250}
    fit="crop"
  />
  <Box
    background="v-gradient(transparent 50%, #000)"
    fillMaxSize
  />
  <Column
    fillMaxSize
    verticalArrangement="end"
    p={20}
  >
    <Text fontSize={28} fontWeight="bold" color="#FFF">
      {article.title}
    </Text>
  </Column>
</Box>

Product Card

<Card onClick={() => $navigate("/product/{product.id}")}>
  <Image
    src={product.image}
    fillMaxWidth
    aspectRatio={1.0}
    fit="crop"
  />
  <Column p={12} gap={4}>
    <Text fontSize={14} maxLines={2}>{product.name}</Text>
    <Row gap={4} verticalAlignment="center">
      <Icon name="lucide:star" size={16} color="primary" />
      <Text fontSize={12}>{product.rating}</Text>
    </Row>
    <Text fontSize={16} fontWeight="bold">${product.price}</Text>
  </Column>
</Card>

Icon with Label

<Column horizontalAlignment="center" gap={4}>
  <Icon name="lucide:shopping-cart" size={32} color="primary" />
  <Text fontSize={12}>Cart</Text>
</Column>

Icon Button

<Button onClick={toggleFavorite}>
  @if (isFavorite) {
    <Icon name="ic:baseline-favorite" color="error" />
  } @else {
    <Icon name="ic:baseline-favorite-border" color="secondary" />
  }
</Button>

Rating Stars

val rating = 4.5

<Row gap={4}>
  @for (i in 1..5) {
    @if (i <= rating) {
      <Icon name="ic:baseline-star" color="primary" size={20} />
    } @else {
      <Icon name="ic:baseline-star-border" color="primary" size={20} />
    }
  }
  <Text fontSize={14} color="secondary">({reviewCount})</Text>
</Row>

Image Gallery Grid

<LazyVerticalGrid columns={3} gap={2}>
  @for (photo in photos) {
    <Image
      src={photo.thumbnail}
      aspectRatio={1.0}
      fit="crop"
      key={photo.id}
      onClick={() => $navigate("/photo/{photo.id}")}
      transition:crossfade="photo-{photo.id}"
    />
  }
</LazyVerticalGrid>

Background Image

<Box fillMaxSize>
  <Image
    src={backgroundUrl}
    fillMaxSize
    fit="crop"
  />
  <Box
    background="v-gradient(transparent, #000000CC)"
    fillMaxSize
  />
  <Column fillMaxSize p={20} verticalArrangement="center">
    <Text fontSize={32} color="#FFF" fontWeight="bold">
      Welcome
    </Text>
  </Column>
</Box>

Profile Header

<Column>
  <Box h={200}>
    <Image src={user.coverPhoto} fillMaxSize fit="crop" />
  </Box>
  <Row p={16} gap={16}>
    <Image
      src={user.avatar}
      w={80} h={80}
      cornerRadius={40}
      mt={-40}  // Overlap cover photo
    />
    <Column weight={1}>
      <Text fontSize={24} fontWeight="bold">{user.name}</Text>
      <Text fontSize={14} color="secondary">{user.bio}</Text>
    </Column>
  </Row>
</Column>

Prop Reference

Image Props

PropTypeDescription
srcStringImage URL
bitmapBitmapBitmap object
fitStringContent scale mode
aspectRatioNumberWidth/height ratio
cornerRadiusNumberCorner radius in dp
overlayStringGradient overlay
placeholderResourceLoading placeholder
errorResourceError fallback

Icon Props

PropTypeDescription
nameStringIcon name (kebab-case)
sizeNumberIcon size in dp
colorStringIcon color
tintStringAlias for color
rotateNumberRotation in degrees

See Also