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" /> fit | Effect |
|---|---|
"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 <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.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
| Prop | Type | Description |
|---|---|---|
src | String | Rooted path, URL, or content:// URI |
bitmap | Bitmap | An in-memory bitmap, drawn directly |
fit | String | "cover", "contain", or "fill" |
overlay | String | A gradient drawn over the image |
hdr / sdr / gainmap / mode | Boolean / String | UltraHDR decode mode — one at a time |
onLoad | Function | Called with the decoded size in pixels, (w, h) => |
onError | Function | Called when the load fails, no parameters |
onClick | Function | Tap handler, like any element |
Icon Props
| Prop | Type | Description |
|---|---|---|
name | String | set:kebab-name, or an imported set.PascalName |
size | Number | Icon size in dp |
color | String | A theme token, hex, or Tailwind palette name |
See Also
- Icons - Declaring and importing icon sets
- <Audio> and <Video> - Media with the same
srcrules - $folder - A user-chosen directory;
file()gives acontent://source - Shaders and colors - The gradient and color grammar
- transition:crossfade - Shared element transitions with images