Skip to content
zabloo

Image

The node that shows a picture. Its file travels inside the envelope, so the game never fetches anything, and its natural size is the source's own.

primitivesince v1not focusable

An Image draws a picture inside a rectangle. You reach for one for icons, portraits and banners: the coin beside a price, the item art in an inventory slot, the header of a shop panel. The file is not fetched at runtime — zabloo export reads it off disk and packs its bytes into the envelope, so what the game receives is a picture it already holds. The node takes no children, because it is a rect with pixels in it and everything else is layout.

image-fit.viewIR v1
The same square star icon in four boxes of equal size: contain shows it whole and letterboxed, cover fills the box and crops it, stretch fills it and distorts it, and the fourth is the contained star tinted gold through style.color.
STATE
  • hover — off
  • pressed — off
  • focused — off
  • selected — off
  • disabled — off

Reported from the last painted frame

Not running — press Run to draw this on the GPU.

import { Column, Image, Row, Text } from "@zabloo/react";
import type { ReactNode } from "react";


const BOX = { width: 132, height: 84 } as const;

const FRAME = {
  background: "{color.slot}",
  radius: "{radius.md}",
  borderWidth: "{border.hairline}",
  borderColor: "{color.line}",
} as const;

function Sample({ caption, children }: { caption: string; children: ReactNode }) {
  return (
    <Column layout={{ gap: "{space.2}", align: "center" }}>
      {children}
      <Text style={{ color: "{color.faint}", fontSize: "{text.xs}" }}>{caption}</Text>
    </Column>
  );
}

export default function ImageFit() {
  return (
    <Column
      layout={{ grow: 1, justify: "center", align: "center", padding: "{space.6}" }}
      style={{ background: "{color.bg}" }}
    >
      <Row layout={{ gap: "{space.3}", align: "start" }}>
        <Sample caption="contain">
          {/* The whole image, undistorted, letterboxed in the box. */}
          <Image src="icons/star.png" fit="contain" layout={BOX} style={FRAME} />
        </Sample>

        <Sample caption="cover">
          {/* Fills the rect, undistorted, cropping the overflowing axis. */}
          <Image src="icons/star.png" fit="cover" layout={BOX} style={FRAME} />
        </Sample>

        <Sample caption="stretch">
          {/* Fills it exactly, aspect ratio and all. */}
          <Image src="icons/star.png" fit="stretch" layout={BOX} style={FRAME} />
        </Sample>

        <Sample caption="style.color">
          {/* The tint multiplies per channel — absent = white = the pixels as
              they are, so this needs no second image. */}
          <Image
            src="icons/star.png"
            fit="contain"
            layout={BOX}
            style={{ ...FRAME, color: "{color.gold}" }}
          />
        </Sample>
      </Row>
    </Column>
  );
}
One square source in four boxes of identical size. Look at what each one gives up: contain keeps the whole picture and leaves bars, cover fills the box and loses the edges, stretch fills it and distorts, and the fourth is the same file tinted through style.color. Notice too that nothing spills — every mode paints inside its own rect.

The two tables below are the clearest case of the two layers being genuinely different things. The prop you write is a path in your project; the prop that ships is a reference into the envelope, rewritten by zabloo export when it gathered the bytes. Same name, two meanings, and only one of them ever reaches a player’s machine.

Authoring props

What you write in @zabloo/react.

PropTypeDefaultDescription
srcstringPath inside the project's src/assets/ — "logo.png", "icons/coin.png".
fit"contain" | "cover" | "stretch""contain"How the source fills the layout rect.

On top of these, every component takes the node base props — id, visible, disabled, layout, style, states, transition, autofocus, clip — plus variant, which the theme resolves away at export time and which never appears in the IR.

IR props

What ships to the game, after the authoring layer is gone.

PropTypeDefaultDescription
srcAssetRef"asset:<id>" — an entry in the envelope's manifest. Never a binding.
fit"contain" | "cover" | "stretch""contain"How the source fills the layout rect.
src is static by design

An asset reference is collected at export time, when the bytes are gathered into the envelope — so there is nothing for a binding to point at at runtime. A per-row icon chosen by data is not expressible in IR v1; a view that needs one authors its slots statically, or the game swaps a whole view.

Fit

Every mode paints inside the rect. cover crops the source through its UVs rather than overflowing, so the invariant that makes hit-testing on layout rects honest holds without any clipping machinery.

ModeResult
"contain"The whole image, undistorted, centered — letterboxed.
"cover"Fills the rect, undistorted, cropping the overflowing axis evenly.
"stretch"Fills the rect exactly, distorting the aspect ratio.

Paint

There are no Image-specific style props. Everything is the ordinary style set, which is what makes an icon themeable and per-state for free:

  • style.color tints the image, multiplied per channel. Absent = white = the pixels as they are. It is the same “color of this node’s content” that colors glyphs, so states.*.style.color tints per state with nothing new.
  • style.radius rounds the painted image, matching the node’s own background.
  • style.background and borderWidth are the placeholder. An image paints nothing until its bytes are decoded, and the layout has already reserved the space from the manifest’s width/height.

There is no loading state: the placeholder is authored, not a runtime state the SDK enters.

Behavior

States

disabled only, inherited — an icon greys out with the control it belongs to. Inside a Button, the button’s own states can tint it: the style is the button’s, the image is its content, so a hover lights up both without the image declaring anything.

Focusable

No. An image takes no input and nothing the player does reaches it, so nothing beyond disabled applies.

Sizing

Intrinsic, from the manifest’s width/height — a leaf with a natural size, like Text. Give it layout.width/height to size it yourself, and fit to choose how the picture fills that box.

Actions

None, so the game hears nothing from an Image. A picture that reacts to a press is an Image inside a Button, and the press belongs to the button.

Degradation

On an older SDK

On an older SDK the box keeps its exact size and whatever background you gave it — the picture never appears inside it.

As an empty Container: it is a leaf, so what survives is its box, its background and its border — the placeholder you authored, permanently. Nothing about the layout moves, because the space was reserved from the manifest before the bytes were ever decoded.

Composition

An icon before a label is a Row; an icon-only button is a Button whose only child is an Image. The node has no opinion about either.

  • Intrinsic size — use it when the art was drawn at the size it should appear, which is the normal case for icons.
  • Sized and cropped — use fit="cover" with an explicit box for banners and portraits, where the slot’s shape matters more than the whole picture.
  • A tinted monochrome source — use it when the same shape needs several colours: one file in the envelope, every colour the theme has, and a state change costs no second asset.
// Intrinsic size, straight from the manifest.
<Image src="icons/coin.png" />

// Sized and cropped by you.
<Image src="banners/shop.png" fit="cover" layout={{ width: 320, height: 120 }} style={{ radius: 8 }} />

// Tinted per state: the same "color of this node's content" that colors glyphs.
<Image
src="icons/star.png"
layout={{ width: 20, height: 20 }}
style={{ color: "{color.faint}" }}
states={{ hover: { style: { color: "{color.gold}" } } }}
/>